0
点赞
收藏
分享

微信扫一扫

诡异的JVM永久代溢出

内容简介

生产上两个应用无缘无故的出现Perm区OOM,近期也没变动,用VisualVM点垃圾回收也能对Perm区回收,所以很奇怪。后来才发现,原来是别人通过instrument方法attach了一个agent到JVM进程上,扫描了所有的class对象并且没释放,导致perm区溢出。本文详细介绍perm区为何持续增长,以及通过简单示例介绍instrument如何使perm区溢出的。


问题描述

很久没有变更的两个应用,生产上突然出现Perm区溢出了,使用的中间件是Weblogic 12.1.3,jdk是1.7.0.80,两个不同的应用最近都出了问题,最近的操作就是做了一次Weblogic漏洞的升级,但也是一个月之前了。
使用的jvm主要参数如下

-XX:+CMSParallelRemarkEnabled
-XX:CMSFullGCsBeforeCompaction=0
-XX:PermSize=1024m
-XX:MaxPermSize=1024m
-Xms5120m
-Xmx5120m
-XX:CMSInitiatingOccupancyFraction=65
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:+CMSClassUnloadingEnabled
-XX:+ExplicitGCInvokesConcurrent

按理说上述配置是可以回收Perm区的,但是看GC日志发现FullGC也回收不了。奇怪的是,我在VisualVM上点一下”垃圾回收“,就回收了。

诡异的JVM永久代溢出_动态代理

生产上点一下垃圾回收,就把Perm区回收了

初步分析

使用jmap -permstat查看Perm区的东西,都是WSServiceDelegate$DelegatingLoader加载的类,而且都是dead的。

诡异的JVM永久代溢出_动态代理_02

使用arthas进行跟踪是谁调用了WSServiceDelegate的类加载方法,发现是有两个WebService在调用的时候每次都new客户端实例,而每new一个,就新加载一个代理类。
我们的客户端是基于Sun的jax-ws的实现。

在 JAX-WS中,一个远程调用可以转换为一个基于XML的协议例如SOAP。在使用JAX-WS过程中,开发者不需要编写任何生成和处理SOAP消息的代码。JAX-WS的运行时实现会将这些API的调用转换成为对应的SOAP消息。

在服务器端,用户只需要通过Java语言定义远程调用所需要实现的接口SEI (service endpoint interface),并提供相关的实现,通过调用JAX-WS的服务发布接口就可以将其发布为WebService接口。

在客户端,用户可以通过JAX-WS的API创建一个代理(用本地对象来替代远程的服务)来实现对于远程服务器端的调用。

在客户端,我们不需要自己写代码,使用wsimport工具自动根据wsdl生成客户端代码,比如下面这个就是一个生成的客户端类。

/**
* This class was generated by the JAX-WS RI.
* JAX-WS RI 2.2.4-b01
* Generated source version: 2.2
*
*/
@WebServiceClient(name = "HelloImplService", targetNamespace = "http://ws.test.com/", wsdlLocation = "http://localhost:8080/testjws/service/sayHi?wsdl")
public class HelloImplService
extends Service
....

出现问题的直接原因是WSServiceDelegate$DelegatingLoader加载了太多类没有被回收,最后perm区溢出。

问题还原

下面是我写的一个示例,代码可在https://gitee.com/ifool123/webservice_demo上下载,使用jdk1.7, 同时,使用2.2.10的jaxws-rt,与生产环境一致。

<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.2.10</version>
</dependency>

Server类是启动服务端,Client类是用客户端调用服务端。
![image-20220313161149847](jdk动态代理导致perm区溢出问题分析/image-20220313161149847.png)
下面是调用服务端的代码,不是生产上出问题的代码,重点就是 HelloImpl service = new HelloImplService().getHelloImplPort();

package com.test.webservice.client;

public class Client {
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 10000000; i++) {
HelloImpl service = new HelloImplService().getHelloImplPort();
String a = service.sayHello1();
String b = service.sayHello("test");
Thread.sleep(1000);
}
}
}

调用栈如下,每次getPort都会最终走到创建类,这是因为webservice是基于spi实现的,我本地的代码跟weblogic中的调用栈不一样,weblogic中间有一些自己的实现,但是开始的部分和最后走到WSServiceDelegate的方法是一样的。

at java.lang.reflect.Proxy.newInstance()
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:755)
at com.sun.xml.ws.client.WSServiceDelegate$3.run(WSServiceDelegate.java:742)
at java.security.AccessController.doPrivileged(AccessController.java:-2)
at com.sun.xml.ws.client.WSServiceDelegate.createProxy(WSServiceDelegate.java:738)
at com.sun.xml.ws.client.WSServiceDelegate.createEndpointIFBaseProxy(WSServiceDelegate.java:820)
at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:451)
at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:419)
at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:401)
at javax.xml.ws.Service.getPort(Service.java:119)
at com.test.webservice.client.HelloImplService.getHelloImplPort(HelloImplService.java:72)
at com.test.webservice.client.Client.main(Main.java:8)

循环的不停调用这个WebService,会不停的产生com.sun.proxy.$ProxyXXX类,XXX是一个递增的序列。

[Loaded com.sun.proxy.$Proxy721 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy722 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy723 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy724 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy725 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy726 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy727 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy728 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy729 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy730 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy731 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]
[Loaded com.sun.proxy.$Proxy732 from com.sun.xml.ws.client.WSServiceDelegate$DelegatingLoader]

这个就是java中的动态代理类。
同时,Perm区会一直增长,但是我在本地不管怎么试,都是能回收的,所以这个程序并没有复现问题。如何复现,在最后面再说。

诡异的JVM永久代溢出_动态代理_03


我们先分析一下为什么产生这么多代理类。

为什么会产生这么多代理类

首先,对于上面的客户端代码,如果getPort只调用一次,就不会创建多个类了,代码如下:

public static void main(String[] args) throws InterruptedException {    
HelloImpl service = new HelloImplService().getHelloImplPort();
for(int i = 0; i < 10000000; i++) {
String a = service.sayHello1();
String b = service.sayHello("test");
}
}

但是,官方没有说明获取实例的方法是线程安全的,所以多线程的情况下有可能有问题,不过我们用ThreadLocal解决这个问题应该也可以。我们主要看一下,在每次都调用getPort的时候,为什么会产生多个代理类。
问题原因比较复杂,主要有两个

  1. jax-ws rt 2.2.6中引入了一个bug,导致在jdk1.6中,每次都会new一个instance,在2.2.7中做了修复
  2. jdk1.7中,升级了动态代理的缓存机制,导致2.2.7中又出现了这个问题。

在WSServiceDelegate中,会使用JDK动态代理为ServicePort提供一个动态类,代码如下,

private <T> T createProxy(final Class<T> portInterface, final InvocationHandler pis) {

// When creating the proxy, use a ClassLoader that can load classes
// from both the interface class and also from this classes
// classloader. This is necessary when this code is used in systems
// such as OSGi where the class loader for the interface class may
// not be able to load internal JAX-WS classes like
// "WSBindingProvider", but the class loader for this class may not
// be able to load the interface class.
final ClassLoader loader = getDelegatingLoader(portInterface.getClassLoader(),
WSServiceDelegate.class.getClassLoader());

// accessClassInPackage privilege needs to be granted ...
RuntimePermission perm = new RuntimePermission("accessClassInPackage.com.sun." + "xml.internal.*");
PermissionCollection perms = perm.newPermissionCollection();
perms.add(perm);

return AccessController.doPrivileged(
new PrivilegedAction<T>() {
@Override
public T run() {
Object proxy = Proxy.newProxyInstance(loader,
new Class[]{portInterface, WSBindingProvider.class, Closeable.class}, pis);
return portInterface.cast(proxy);
}
},
new AccessControlContext(
new ProtectionDomain[]{
new ProtectionDomain(null, perms)
})
);
}

生成代理类的代码就是

Object proxy = Proxy.newProxyInstance(loader,
new Class[]{portInterface, WSBindingProvider.class, Closeable.class}, pis);

会调用java.lang.reflect.Proxy里的newProxyInstance,这里面有三个参数:

loader : 用来加载动态代理类的ClassLoader
interfaces: 这个动态代理类要实现的接口,可以有多个
invocationhandler : 这个是就是动态生产一个类的时候,对应的类里的函数的实现体

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

这个方法并不是每次都会生成类,而是有缓存的。
对于动态代理类,缓存的原理大致如下,就相当于redis的hset,一级key为classloader,二级key为实现的接口拼成的字符串(排序过的),也就是说,你要是持续的用同一个类加载器生成同样接口的代理类,不会每次都创建的,而是有缓存。

//缓存是一个二级的map,其中第一级key是classloader,然后第二级key是实现的接口的组合
Map<ClassLoader, Map<Object, Class>> cache;

//获取缓存的过程
Object subKey = Arrays.asList(interfaceNames); //把接口的名字数组转换成一个list,作为次级key
Map<Object, Class> valueMap = cache.get(classloader);
if(valueMap == null) {
valueMap = new HashMap<Object,Class>();
cache.put(classloader, valueMap);
Class clazz = proxy.newInstance(); //生成类
valueMap.put(subKey, clazz);
return clazz;
} else {
Class clazz = valueMap.get(subKey);
if(clazz == null) {
clazz = proxy.newInstance(); //生成类
valueMap.put(subKey, clazz);
}
return clazz;
}

以上只是伪代码,在1.6和1.7中,实现发生了很大改变。
在jdk1.6中,一级Map是一个线程不安全的WeakHashMap,访问的时候需要上锁。

/** maps a class loader to the proxy class cache for that loader */
private static Map loaderToCache = new WeakHashMap();

次级key使用的是要实现的接口名字为key

/*
* Using string representations of the proxy interfaces as
* keys in the proxy class cache (instead of their Class
* objects) is sufficient because we require the proxy
* interfaces to be resolvable by name through the supplied
* class loader, and it has the advantage that using a string
* representation of a class makes for an implicit weak
* reference to the class.
*/
Object key = Arrays.asList(interfaceNames);

使用的次级Map是一个经过synchronized的WeakHashMap

/** set of all generated proxy classes, for isProxyClass implementation */
private static Map proxyClasses =
Collections.synchronizedMap(new WeakHashMap());

上述实现有什么问题呢?
很明显,两层上锁,在高并发的时候会有性能问题,所以有人就给Oracle提了bug,说Proxy在高并发的时候获取缓存性能太差(bug链接:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7123493)

诡异的JVM永久代溢出_jvm_04

于是,在1.7中,缓存机制做了修改,还是hset的形式,但是自己定义了一个WeakCache,可以实现高并发。

/**
* a cache of proxy classes
*/
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

具体的实现细节不做详细介绍,只介绍一个跟我们问题相关的点,那就是这里一级CacheKey的生成规则。

private static final class CacheKey<K> extends WeakReference<K> {

...省略...
private final int hash;

private CacheKey(K key, ReferenceQueue<K> refQueue) {
super(key, refQueue);
this.hash = System.identityHashCode(key); // compare by identity
}

@Override
public int hashCode() {
return hash;
}
...省略...

注意上述代码,hashCode函数里的hash值,是用System.identityHashCode(key)实现的。

/**
* Returns the same hash code for the given object as
* would be returned by the default method hashCode(),
* whether or not the given object's class overrides
* hashCode().
* The hash code for the null reference is zero.
*
* @param x object for which the hashCode is to be calculated
* @return the hashCode
* @since JDK1.1
*/
public static native int identityHashCode(Object x);

这个地方太关键了,这个函数的意思就是,不管你如何重写hashcode函数,这个方法获取对象 的hash值都用未重写的版本。

诡异的JVM永久代溢出_perm区溢出_05


在jdk1.6中,如果你new多个ClassLoader去加载代理类,如果重写它们的hashCode方法,只要hashCode的值一样,那么缓存认为这些都是一个ClassLoader,会命中缓存。如果实现的接口名也一致的话,就不用重新加载一个新类了。如果没有重写hashCode,那每个ClassLoader都需要缓存。

诡异的JVM永久代溢出_java agent_06


在jdk1.7中,即使重写了hashcode,只要不是相同的对象,就认为不是一个ClassLoader,会重新加载类。
来验证一下,先用jdk1.6,如下代码,jvm参数上加上-verbose:class,用来观察类加载情况。

package com.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;

public class TestUnloadClass {

public static void main(String[] args) throws Exception {
while(true) {
proxy();
Thread.sleep(1000);
}
}
public static void proxy() throws Exception{
MyClassLoader l = new MyClassLoader();
Class clazzProxy = Proxy.getProxyClass(l, Collection.class);
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy = (Collection) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
}

/<strong> classloader不重写hashcode </strong>/
class MyClassLoader extends ClassLoader {
// @Override
// public int hashCode() {
// return 1;
// }
// @Override
// public boolean equals(Object obj) {
// return true;
// }
}

在不重写hashCode和equals的时候,不停的加载新类,输出如下:

......
[Loaded com.sun.proxy.$Proxy1 from com.test.MyClassLoader]
[Loaded com.sun.proxy.$Proxy2 from com.test.MyClassLoader]
[Loaded com.sun.proxy.$Proxy3 from com.test.MyClassLoader]
[Loaded com.sun.proxy.$Proxy4 from com.test.MyClassLoader]
[Loaded com.sun.proxy.$Proxy5 from com.test.MyClassLoader]
[Loaded com.sun.proxy.$Proxy6 from com.test.MyClassLoader]
......

接下来把注释打开,重写hashCode,这时在运行,就不会一致加载类了,因为命中了缓存。但是如果把jdk换成jdk1.7的话,不管怎么重写,都会重新加载。

JDK版本

classloader是否重写hashcode

不同classloader加载的类能否缓存

1.6

重写

hashcode一样的classloader就能缓存

1.6

不重写

不能缓存

1.7

重写

不能缓存

1.7

不重写

不能缓存

接下来我们在看WSServiceDelegate中加载动态代理类用的ClassLoader,每次都调用getDelegatingLoader获取ClassLoader,这个类是WSServiceDelegate的一个内部类。

// When creating the proxy, use a ClassLoader that can load classes
// from both the interface class and also from this classes
// classloader. This is necessary when this code is used in systems
// such as OSGi where the class loader for the interface class may
// not be able to load internal JAX-WS classes like
// "WSBindingProvider", but the class loader for this class may not
// be able to load the interface class.
ClassLoader loader =
getDelegatingLoader(portInterface.getClassLoader(),
WSServiceDelegate.class.getClassLoader());
T proxy = portInterface.cast(Proxy.newProxyInstance(loader,
new Class[]{portInterface, WSBindingProvider.class, Closeable.class}, pis));

获取classloader函数实现如下:

private static ClassLoader getDelegatingLoader(ClassLoader loader1, ClassLoader loader2) {
if (loader1 == null) return loader2;
if (loader2 == null) return loader1;
return new DelegatingLoader(loader1, loader2);
}

loader1和loader2一般不会为null,因为portInterface和WSServiceDelegate的加载器不是bootstrap classloader,这样对于jdk1.7来说,如果loader1和loader2都不是Null,就每次都new一个,每次都不会命中缓存。
看jax-ws rt代码,2.2.6到2.2.7就是重写了DelegatingLoader的hashcode和equals,解决了1.6下无法命中缓存的问题,但是到了1.7后,这问题又存在了,后面一直没解决。
以下是2.2.6版本的:

private static ClassLoader getDelegatingLoader(ClassLoader loader1, ClassLoader loader2) {
if (loader1 == null) return loader2;
if (loader2 == null) return loader1;
return new DelegatingLoader(loader1, loader2);
}

private static final class DelegatingLoader
extends ClassLoader
{
private final ClassLoader loader;

DelegatingLoader(ClassLoader loader1, ClassLoader loader2)
{
super(loader2);
this.loader = loader1;
}

protected Class findClass(String name)
throws ClassNotFoundException
{
return loader.loadClass(name);
}

protected URL findResource(String name)
{
return loader.getResource(name);
}
}

以下是2.2.7版本的:

private static ClassLoader getDelegatingLoader(ClassLoader loader1, ClassLoader loader2) {
if (loader1 == null) return loader2;
if (loader2 == null) return loader1;
return new DelegatingLoader(loader1, loader2);
}

private static final class DelegatingLoader extends ClassLoader {
private final ClassLoader loader;

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((loader == null) ? 0 : loader.hashCode());
result = prime * result
+ ((getParent() == null) ? 0 : getParent().hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DelegatingLoader other = (DelegatingLoader) obj;
if (loader == null) {
if (other.loader != null)
return false;
} else if (!loader.equals(other.loader))
return false;
if (getParent() == null) {
if (other.getParent() != null)
return false;
} else if (!getParent().equals(other.getParent()))
return false;
return true;
}

DelegatingLoader(ClassLoader loader1, ClassLoader loader2) {
super(loader2);
this.loader = loader1;
}

protected Class findClass(String name) throws ClassNotFoundException {
return loader.loadClass(name);
}

protected URL findResource(String name) {
return loader.getResource(name);
}
}
}

为什么不回收

我们看一下类在什么情况下可以被回收:
在网上看到一张图,能很好的表达了类什么时候可以回收。

诡异的JVM永久代溢出_perm区溢出_07


也就是上面的连线全都没了,类就可以被卸载,具体来说:
1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例,也就是obj没了
2.加载该类的ClassLoader已经被回收,也就是loader1没了
3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法,也就是objClass没了
满足以上几个条件,在Perm区里就能回收了。这也说明,如果你的类是系统加载器加载的,比如Bootstrap加载器,AppClassLoader,那它永远不会卸载,因为这些加载器不会被回收。所以只有自定义的类能回收。
但是并不是符合回收条件,就一定会回收,因为大部分情况下默认不回收,甚至可以指定noclassgc来让类不被回收。
在CMS回收机制下,指定下列参数就能卸载类

-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled

我们用前面的代码试一下:

public class TestUnloadClass {

public static void main(String[] args) throws Exception {
while(true) {
proxy();
//Thread.sleep(1000);
}
}
public static void proxy() throws Exception{
MyClassLoader l = new MyClassLoader();
Class clazzProxy = Proxy.getProxyClass(l, Collection.class);
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy = (Collection) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
}
class MyClassLoader extends ClassLoader {}

上述代码因为每执行一次proxy(),产生的ClassLoader, Class, Proxy都没了,所以能回收。

诡异的JVM永久代溢出_perm区溢出_08


如果把每次生成的ClassLoader存起来,那就会Perm区溢出了。

public class TestUnloadClass {

public static void main(String[] args) throws Exception {
while(true) {
proxy();
//Thread.sleep(1000);
}
}
public static List list = new ArrayList();
public static void proxy() throws Exception{
MyClassLoader l = new MyClassLoader();
list.add(l);
Class clazzProxy = Proxy.getProxyClass(l, Collection.class);
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy = (Collection) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
}

class MyClassLoader extends ClassLoader {}

诡异的JVM永久代溢出_instrument_09

同样,把Class存起来也会溢出,假如我们把ClassLoader用SoftReference或者WeakReference存起来,也是能回收的。
那么WebService产生的动态代理类是可以回收的吗? 答案是肯定的。
1.动态代理是局部变量,用完一次就不需要了。
2.动态代理的class文件,用一次之后就不用了,可以回收
3.动态代理的classloader,每次都是new的,也是一次性的,可以回收。
所以,对应的perm区是可以回收的。
那为啥生产上的无法回收呢?

问题的根因是外部因素

上班后重新看了一下heapdump,发现原来有一个线程把所有加载的类都给引用了,这个线程是用来扫描JVM里有没有被植入muma(拼音)的,而且是通过instrument动态加载的,所以我为什么我点用visualvm点一下回收就能把内存回收了,就是因为那时候这个扫描的类没有被加载到jvm里,所以那个时候即使不用我点,也能回收。 动态加载并启动这个方法后,把类都引用了,就没法回收了。

诡异的JVM永久代溢出_perm区溢出_10

Instrument简介与实例

instrument的核心是java.lang.instrument.Instrumentation类,jdk中它的注释如下:

Instrumentation类提供控制Java语言程序代码的服务。Instrumentation可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。由于插入的字节码是附加的,这些更变不会修改原来程序的状态或者行为。通过这种方式实现的良性工具包括监控代理、分析器、覆盖分析程序和事件日志记录程序等等。

目前主流的APM开源框架如Pinpoint、SkyWalking等等都是通过java.lang.instrument包提供的字节码增强功能来实现的。
Instrumentation这个类无法在正常代码中获取,只有在启动时或者使用其他手段从JVM获取,JVM会提供一个实例。可以说一旦获取了Instrumentation的实例,就能干很多事情,包括好的还有坏的,我们常用的英文单词,叫manipulate,就是操纵。

诡异的JVM永久代溢出_instrument_11


获取instrumentation实例的方式有两种,

  1. JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
  2. JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。

上面扫描muma(拼音)的方式,就是第二种。
我们实现一个这样的agent。
实现一个agent类

package com.test;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agent {
public static Class[] classes = null;
public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
final Instrumentation i = inst;

//每10秒钟,获取所有加载的类,赋值到classes
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
while(true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
classes = i.getAllLoadedClasses();
}

}});
thread.setName("hacker");
thread.start();
}
}

使用maven打包

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test.agent</groupId>
<artifactId>Agent</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<dependencies>
</dependencies>

<build>
<defaultGoal>package</defaultGoal>
<plugins>
<!-- specify target Java version -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.test.Agent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Main-Class>NotSuitableAsMain</Main-Class>
<Agent-Class>com.test.Agent</Agent-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

</project>

我们在maven文件里有manifest文件的内容,最后打出来的jar包里面的manifest.mf文件如下:

Manifest-Version: 1.0
Built-By: Administrator
Build-Jdk: 1.7.0_80
Agent-Class: com.test.Agent
Premain-Class: com.test.Agent
Created-By: Apache Maven 3.6.3
Can-Redefine-Classes: true
Main-Class: NotSuitableAsMain
Can-Retransform-Classes: true

里面Agent-Class,就是指定agent被注入时执行的agentmain函数,编译生产Agent-1.0.jar。
再写一个attach函数。

package com.test.attach;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach {
public static void main(String[] args) throws IOException, AttachNotSupportedException,
AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("27100");
vm.loadAgent("D:\\workspace\\eclipse-workspace\\agent\\target\\Agent-1.0.jar", null);
}
}

注意需要把jdk里的tools.jar加入path。诡异的JVM永久代溢出_java agent_12
启动之前的client和server,查看client的进程号,改到attach的代码里,然后执行,就把agent注入进去了。
在注入之前,perm区一直能正常回收,但是注入后,内存逐渐变少,最终没有内存可用了,因为我用一个静态变量把所有的class类都引用了。

诡异的JVM永久代溢出_jvm_13

举报

相关推荐

0 条评论