诡异的jvm永久代溢出

nxlhero      2022-03-31     606

关键词:

内容简介

生产上两个应用无缘无故的出现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内存溢出

...堆内存的最大值,就会出现内存溢出OutOfMemory:Javaheapspace永久代溢出  类的一些信息,如类名、访问修饰符、字段描述、方法描述等,所占空间大于永久代最大值,就会出现OutOfMemoryError:PermGenspace内存溢出的检测方法Jdk/bin目录... 查看详情

jvm内存分区和各分区溢出测试(代码片段)

...态变量,即时编译器编译后的代码缓存等数据。方法区!=永久代,只是在永久代这个概念还存在的时候,为了方法区能像堆一样进行分代收集,将方法区采用永久代实现。永久代的概念被抛弃后(JDK8),方法区采用元空间来实现(Met... 查看详情

永久代溢出(java.lang.outofmemoryerror:permgenspace)

jstat命令简介:Jstat是JDK自带的一个轻量级小工具。全称“JavaVirtualMachinestatisticsmonitoringtool”,它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heapsize和垃圾回收状况... 查看详情

(转)jvm各种内存溢出是否产生dump

...MemoryError,很明确的知道堆内存溢出时会生成dump文件。但永久代内存溢出不明确是否会生成,今天来做一个实验:永久代内存溢出,有dump文件。JVM的参数是-XX:PermSize=10m -XX:MaxPermSize=10m -XX:+HeapDumpOnO 查看详情

jvm各种情况内存溢出分析(代码片段)

目录1直接内存溢出2内存溢出2.1堆溢出2.2.1堆溢出案例2.3永久代或元空间溢出2.3.1永久代或元空间溢出案例2.4栈溢出2.4.1栈溢出案例2.5非常规溢出1直接内存溢出直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分ÿ... 查看详情

五种内存溢出案例总结:涵盖栈深度溢出永久代内存溢出本地方法栈溢出jvm栈内存溢出和堆溢出(代码片段)

...拟机栈就不断深入不断深入,栈深度就这样溢出了。永久代内存溢出publicstaticvoidtestPergemOutOfMem 查看详情

jvm--内存区域与内存溢出异常

...、即时编译后的代码等数据;在hotspot虚拟机中又被称为永久代,此外字符串常量池已经在java7版本后移除永久代。    运行时常量池是方法区的一部分,具有动态性,用于存放编译器生成的各种字面量和符号引用。堆   ... 查看详情

性能测试三十六:内存溢出和jvm常见参数及jvm参数调优

...堆内存的最大值,就会出现内存溢出OutOfMemory:Javaheapspace永久代溢出如果发生,则是在初始化的时候,空间太小,解决办法,扩大空间类的一些信息,如类名、访问修饰符、字段描述、方法描述等,所占空间大于永久代最大值,... 查看详情

性能测试三十六:内存溢出和jvm常见参数

...堆内存的最大值,就会出现内存溢出OutOfMemory:Javaheapspace永久代溢出如果发生,则是在初始化的时候,空间太小,解决办法,扩大空间类的一些信息,如类名、访问修饰符、字段描述、方法描述等,所占空间大于永久代最大值,... 查看详情

storm程序永久代内存溢出

在集群中部署Storm应用程序的时候报错,并通过jdk自带的jconsole监控发现,永久代内存瞬间爆炸了org.springframework.beans.factory.BeanCreationException:Errorcreatingbeanwithname‘cacheService‘definedinresourceloadedfrombytearray:Invocationofini 查看详情

jvm:6jvm分代模型:年轻代老年代永久代

1.背景引入JVM内存的分代模型:年轻代、老年代、永久代。我们在代码里创建的对象,都会进入到Java堆内存中,方法的栈帧都会压入到Java虚拟机栈里,而方法如果有局部变量,该局部变量就会在方法所对应栈... 查看详情

jvm内存:年轻代老年代永久代(推荐转)

...;2.Java内存与垃圾回收调优 3.方法区的Class信息,又称为永久代,是否属于Java堆?Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象,如下图所示: 在Java中,堆被划分成两个不同的区域:新生代(Y... 查看详情

jvm年轻代年老代永久代

年轻代:  HotSpotJVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫From和To),每次新创建对象时,都会分配到Eden区,当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC。这些对象经过第一次MinorGC后,如果仍然... 查看详情

jvm中的堆的新生代老年代永久代

文章目录概述具体说明新生代老年代永久代采用元空间而不用永久代的原因:概述JVM堆内存在物理上分为三个区:伊甸园eden:最初对象都分配到这里,与幸存区合称新生代幸存区survivor:当伊甸园内存不足ÿ... 查看详情

jvm灵性一问——为什么用元空间替换永久代?

...a;blog.csdn.net/qq_33591903JVM灵性一问——为什么用元空间替换永久代?前言首先需要明确的是,以下我们讨论的HotSpot虚拟机,其他类型的虚拟机,例如JRockit与J9等,压根就没有永久代的概念。因此,下面所说... 查看详情

jvm之年轻代(新生代)老年代永久代以及gc原理详解gc优化

...JVM,也许你听过这些术语:年轻代(新生代)、老年代、永久代、minorgc(younggc)、majorgc、fullgc不要急,先上图,这是jvm堆内存结构图  仔细的你发现了图中有些分数8/10和1/10,这是默认配置下各个代内存分配比例。举个栗... 查看详情

jvm性能优化

...内存分为三部分:NEW(年轻代)、Tenured(年老代)、Perm(永久代)。   (1)年轻代:用来存放java分配的新对象。   (2)年老代:经过垃圾回收没有被回收掉的对象被复制到年老代   (3)永久代... 查看详情