详解Java内存泄露的示例代码
作者:mrr 发布时间:2023-06-08 03:34:51
在定位JVM性能问题时可能会遇到内存泄露导致JVM OutOfMemory的情况,在使用Tomcat容器时如果设置了reloadable=”true”这个参数,在频繁热部署应用时也有可能会遇到内存溢出的情况。Tomcat的热部署原理是检测到WEB-INF/classes或者WEB-INF/lib目录下的文件发生了变更后会把应用先停止然后再启动,由于Tomcat默认给每个应用分配一个WebAppClassLoader,热替换的原理就是创建一个新的ClassLoader来加载类,由于JVM中一个类的唯一性由它的class文件和它的类加载器来决定,因此重新加载类可以达到热替换的目的。当热部署的次数比较多会导致JVM加载的类比较多,如果之前的类由于某种原因(比如内存泄露)没有及时卸载就可能导致永久代或者MetaSpace的OutOfMemory。这篇文章通过一个Demo来简要介绍下ThreadLocal和ClassLoader导致内存泄露最终OutOfMemory的场景。
类的卸载
在类使用完之后,满足下面的情形,会被卸载:
1.该类在堆中的所有实例都已被回收,即在堆中不存在该类的实例对象。
2.加载该类的classLoader已经被回收。
3.该类对应的Class对象没有任何地方可以被引用,通过反射访问不到该Class对象。
如果类满足卸载条件,JVM就在GC的时候,对类进行卸载,即在方法区清除类的信息。
场景介绍
上一篇文章我介绍了ThreadLocal的原理,每个线程有个ThreadLocalMap,如果线程的生命周期比较长可能会导致ThreadLocalMap里的Entry没法被回收,那ThreadLocal的那个对象就一直被线程持有强引用,由于实例对象会持有Class对象的引用,Class对象又会持有加载它的ClassLoader的引用,这样就会导致Class无法被卸载了,当加载的类足够多时就可能出现永久代或者MetaSpace的内存溢出,如果该类有大对象,比如有比较大的字节数组,会导致Java堆区的内存溢出。
源码介绍
这里定义了一个内部类Inner,Inner类有个静态的ThreadLocal对象,主要用于让线程持有Inner类的强引用导致Inner类无法被回收,定义了一个自定义的类加载器去加载Inner类,如下所示:
public class MemoryLeak {
public static void main(String[] args) {
//由于线程一直在运行,因此ThreadLocalMap里的Inner对象一直被Thread对象强引用
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//每次都新建一个ClassLoader实例去加载Inner类
CustomClassLoader classLoader = new CustomClassLoader
("load1", MemoryLeak.class.getClassLoader(), "com.ezlippi.MemoryLeak$Inner", "com.ezlippi.MemoryLeak$Inner$1");
try {
Class<?> innerClass = classLoader.loadClass("com.ezlippi.MemoryLeak$Inner");
innerClass.newInstance();
//帮助GC进行引用处理
innerClass = null;
classLoader = null;
Thread.sleep(10);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//为了更快达到堆区
public static class Inner {
private byte[] MB = new byte[1024 * 1024];
static ThreadLocal<Inner> threadLocal = new ThreadLocal<Inner>() {
@Override
protected Inner initialValue() {
return new Inner();
}
};
//调用ThreadLocal.get()才会调用initialValue()初始化一个Inner对象
static {
threadLocal.get();
}
public Inner() {
}
}
//源码省略
private static class CustomClassLoader extends ClassLoader {}
堆区内存溢出
为了触发堆区内存溢出,我在Inner类里面设置了一个1MB的字节数组,同时要在静态块中调用threadLocal.get(),只有调用才会触发initialValue()来初始化一个Inner对象,不然只是创建了一个空的ThreadLocal对象,ThreadLocalMap里并没有数据。
JVM参数如下:
-Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:+HeapDumpOnOutOfMemoryError最后执行了814次后JVM堆区内存溢出了,如下所示:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11824.hprof ...
Heap dump file created [100661202 bytes in 1.501 secs]
Heap
par new generation total 30720K, used 30389K [0x00000000f9c00000, 0x00000000fbd50000, 0x00000000fbd50000)
eden space 27328K, 99% used [0x00000000f9c00000, 0x00000000fb6ad450, 0x00000000fb6b0000)
from space 3392K, 90% used [0x00000000fb6b0000, 0x00000000fb9b0030, 0x00000000fba00000)
to space 3392K, 0% used [0x00000000fba00000, 0x00000000fba00000, 0x00000000fbd50000)
concurrent mark-sweep generation total 68288K, used 67600K [0x00000000fbd50000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3770K, capacity 5134K, committed 5248K, reserved 1056768K
class space used 474K, capacity 578K, committed 640K, reserved 1048576K
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.ezlippi.MemoryLeak$Inner.<clinit>(MemoryLeak.java:34)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at java.lang.Class.newInstance(Unknown Source)
at com.ezlippi.MemoryLeak$1.run(MemoryLeak.java:20)
at java.lang.Thread.run(Unknown Source)
可以看到JVM已经没有内存来创建新的Inner对象,因为堆区存放了很多个1MB的字节数组,这里我把类的直方图打印出来了(下图是堆大小为1024M的场景),省略了一些无关紧要的类,可以看出字节数组占了855M的空间,创建了814个 com.ezlippi.MemoryLeak$CustomClassLoader 的实例,和字节数组的大小基本吻合:
num #instances #bytes class name
----------------------------------------------
1: 6203 855158648 [B
2: 13527 1487984 [C
3: 298 700560 [I
4: 2247 228792 java.lang.Class
5: 8232 197568 java.lang.String
6: 3095 150024 [Ljava.lang.Object;
7: 1649 134480 [Ljava.util.HashMap$Node;
11: 813 65040 com.ezlippi.MemoryLeak$CustomClassLoader
12: 820 53088 [Ljava.util.Hashtable$Entry;
15: 817 39216 java.util.Hashtable
16: 915 36600 java.lang.ref.SoftReference
17: 543 34752 java.net.URL
18: 697 33456 java.nio.HeapCharBuffer
19: 817 32680 java.security.ProtectionDomain
20: 785 31400 java.util.TreeMap$Entry
21: 928 29696 java.util.Hashtable$Entry
22: 1802 28832 java.util.HashSet
23: 817 26144 java.security.CodeSource
24: 814 26048 java.lang.ThreadLocal$ThreadLocalMap$Entry
Metaspace溢出
为了让Metaspace溢出,那就必须把MetaSpace的空间调小一点,要在堆溢出之前加载足够多的类,因此我调整了下JVM参数,并且把字节数组的大小调成了1KB,如下所示:
private byte[] KB = new byte[1024];
-Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m
从 GC日志可以看出在Meraspace达到GC阈值(也就是MaxMetaspaceSize配置的大小时)会触发一次FullGC:
java.lang.OutOfMemoryError: Metaspace
<<no stack trace available>>
{Heap before GC invocations=20 (full 20):
par new generation total 30720K, used 0K [0x00000000f9c00000, 0x00000000fbd50000, 0x00000000fbd50000)
eden space 27328K, 0% used [0x00000000f9c00000, 0x00000000f9c00000, 0x00000000fb6b0000)
from space 3392K, 0% used [0x00000000fb6b0000, 0x00000000fb6b0000, 0x00000000fba00000)
to space 3392K, 0% used [0x00000000fba00000, 0x00000000fba00000, 0x00000000fbd50000)
concurrent mark-sweep generation total 68288K, used 432K [0x00000000fbd50000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 1806K, capacity 1988K, committed 2048K, reserved 1056768K
class space used 202K, capacity 384K, committed 384K, reserved 1048576K
[Full GC (Metadata GC Threshold) [CMS
Process finished with exit code 1
通过上面例子可以看出如果类加载器和ThreadLocal使用的不当确实会导致内存泄露的问题,完整的源码在github
来源:https://www.ezlippi.com/blog/2017/12/java-memory-leak-example.html?utm_source=tuicool&utm_medium=referral


猜你喜欢
- 写一个简单的mybatis plus插件自动生成代码的例子pom.xml 添加配置<!-- mybatis plus 插件-->
- 本文研究的主要是Java面试题中的一个比较常见的题目,判断及防止SQL注入的问题,具体介绍如下。SQL注入是目前黑客最常用的攻击手段,它的原
- 一、相关知识SearchView控件:以下是几个简单网址:SearchView简单用法:Android搜索框(SearchView)的功能和
- 大家对于 Spring 的 scope 应该都不会默认。所谓 scope,字面理解就是“作用域”、“范围”,如果一个 bean 的 scop
- 背景随着公司业务越来越复杂,在同一个列表中需要展示各种类型的数据。总体结构ItemViewAdapter: 每种类型的卡片分别都是不同的It
- java应用CPU有波动,事后怎么分析?目前我采用的方案是根据CPU负载自动执行jstack,并将文件上传到OSS。 环境:阿里云
- 今天老师想让我帮忙把她们200多张寸照换成白底的,这些寸照里面多为蓝色底,红色底。用ps?不!用java!!对,我第一反应就是用java,到
- 前言RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局。所以在某些情况,比如多行多列布局,RadioGroup就
- 本文实例讲述了微信js sdk invalid signature签名错误问题的解决方法。分享给大家供大家参考,具体如下:/**最近在做微信
- 一、 序列化和反序列化概念Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是
- 以最终客户的角度来看,JAR文件就是一种封装,他们不需要知道jar文件中有多少个.class文件,每个文件中的功能与作用,同样可以得到他们希
- 解决办法:1.VCS--->Enable Version Control Integration2.选择要关联的版本工具补充:git
- 上一篇文章谈到音频剪切、混音、拼接与转码,也详细介绍cMake配置与涉及FFmpeg文件的导入: android端采用FFmpeg进行音频混
- Android支持多屏幕机制即用为当前设备屏幕提供一种合适的方式来共同管理并解析应用资源。本文就介绍了4中Android屏幕自适应解决方案。
- System类的常用用法1,主要获取系统的环境变量信息public static void sysProp()throws Exceptio
- 本文以新建的CUDA的.cu程序来进行说明,同样也适用于C程序。一,发现问题1,首先我们在vs2019中创建了工程以后(我所创建的工程名称为
- 这篇文章主要介绍了JavaMail与Spring整合过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需
- 加密代码using System;using System.IO;using System.Security.Cryptography;pu
- xxx cannot be resolved to a type引言 eclipse新导入的项目经常可以
- 什么是异常?在程序的运行或者编译时,所产生的错误统称为异常 (也叫Bug)异常的存在形式异常在java中以类的形式存在,每一个异常类都可以创