诡异的JVM永久代溢出

Posted nxlhero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了诡异的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区溢出

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

初步分析

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

诡异的JVM永久代溢出_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永久代溢出_perm区溢出_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 五种内存溢出案例总结:涵盖栈深度溢出永久代内存溢出本地方法栈溢出JVM栈内存溢出和堆溢出

JVM记一次PermGen space内存溢出实战案例

JVM内存分区和各分区溢出测试

JVM内存分区和各分区溢出测试

jvm 内存溢出

永久代溢出(java.lang.OutOfMemoryError: PermGen space )