目录
前言
c++和java之间有一堵由内存动态分配和垃圾收集技术所围成的墙,墙外面的人想进去墙里面的人想出来。
1、内存泄漏
内存泄漏的系统症状主要是:
-
内存的使用率逐渐增大;
-
系统运行的响应时间长;
-
系统CPU较高;
-
严重的直接早晨OOM,进程退出;
2、内存泄漏的原因
根本原因:
长生命周期的对象持有短生命周期的对象,导致本应该回收的对象继续占用内存空间没有被gc回收;
常见的内存泄漏的场景:
-
(1). 静态集合类的引用;
-
通常将一些对象的引用加入到集合中,当我们不需要该对象的时候没有从集合中删除,导致集合的对象越来越多,如果该集合还是static,那其生命周期将和应用一样长,导致内存的泄漏;
-
解决方案:用完后将引用从该集合删除并赋null值,或者使用弱引用;
-
-
(2). 单利模式的对象持有外部类的引用;
-
例如单例模式一般都是静态变量,如果持有的对象的生命周期不是和应用一样长,则会导致内存泄漏;
-
-
(3). 内部类持有外部类的引用造成的内存泄漏:
-
例如匿名内部类的线程,task执行的时间很长,且拥有着外部类的引用;
-
内部类中的静态变量拥着外部类的强引用,阻止垃圾回收器对其回收;
-
解决方案:
-
1)将内部类改为静态内部类;
-
2)内部类中使用弱引用WeakReference来引用外部类的成员变量;
-
-
-
(4). 连接未关闭;
-
造成一些缓冲区的数据一直存在;
-
注意点:
主要保持对生命周期敏感
-
特别是单例,静态对象,全局性集合等生命周期;
3、内存泄漏实例分析
解决办法:从内存dump文件入手,找到导致内存增大的原因:是对象泄漏还是持续有大对象产生?
分析工具: jps/top + jmap + MAT/JvisualVM
从线程堆栈入手,解决内存泄漏的步骤:
-
top查看内存确认是否在稳定增长?
-
使用jps查看java进程pid;
-
提前手动jmap来dump内存或者使用 HeapDumpOnOutOfMemoryError自动打印对内存文件;
-
使用 MAT工具分析查看;
-
根据结果分析报告结合业务判断,一般就可以初步定位占用的最大的内存的对象集合容器;
实例一:模拟内存泄漏代码如下:
/**
* @Author: king
*
* 记得设置参数比如-Xmx20M -Xms20M
* 堆溢出测试
*/
public class HeapOOM {
public static void main(String[] args) {
List<Object> list = new LinkedList<>();
int i = 0;
while (true) {
i++;
if (i % 1000 == 0) {
System.out.println("已存放数据:" + i);
}
list.add(new Object());
}
}
}
3.1、获取堆内存dump文件
方式一:手动dump java
内存 - 为了dump信息的可靠,这里会阻塞应用,可以通过配置机器节点的流量比例来控制,然后进行dump堆栈内存;
命令格式:
jmap -heap 76229//直接查看打印堆内存的信息
jmap -dump:format=b, file=dump_file_name pid //生成堆的转储文件
实例:dump pid 为76229的java进程的内存到app_mem_dump.bin文件
jmap -dump:format=b, file=app_mem_dump.bin 76229
方式二:自动打印
,设置堆在发生OOM的时候jvm自动打印出dump日志文件,添加一下参数:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
设置完参数,当出现OOM的时候,进程一般会退出,并输出以下错误日志信息:
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to heap.hprof ...
Heap dump file created [17606467 bytes in 0.226 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.LinkedList.linkLast(LinkedList.java:142)
at java.util.LinkedList.add(LinkedList.java:338)
at com.beijing.king.springtest.memoryOOM.HeapOOM.main(HeapOOM.java:31)
3.2、MAT工具的使用
得到heap内存的信息后我们可以通过MAT(Memory Analyzer Tools)工具来查看相应的信息:

从这里我们发现我们大约创建了22w个Object对象,其实这就是我们死循环向list中加入的Object对象。
但是这个原因是我们知道代码怎么写的,但是有时候我们并不知道我们的代码里有这样的死循环存在,该怎么去入手分析呢?
MAT工具也帮我们去生成了一个
内存检测报告:


从对内存的详细报告,然后再根据业务场景和自己写的代码应该就能很快定位到内存泄漏的地方了。
4、内存溢出
定义:内存不够,一般由内存泄漏导致。
原因:
循环,大对象
-
for(;;) {intern(); list.add()};
-
大对象申请内存不够;
解决办法:
-
按照类似内存泄漏的方法,通过打印分析堆内存,看看到底是那些对象占用了内存导致内存不足;
-
如果确实是内存不够而溢出,可以通过 -Xms 和 -Xmx 调整堆内存的大小;
4.1、方法区metaspace溢出
方法区线程共享,主要存储方法体的二进制代码。存储已被虚拟机加载的类元数据信息,常量,静态变量,动态生成的字节码等;
还包括静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。
在反射和动态代理-CGLIB - 字节码框架asm使用频繁的场景就需要注意方法区的溢出情况。
实例演示:
/**
* @Author: king
* 设置Metaspace的大小:
* -XX:MetaspaceSize=50M
* -XX:MaxMetaspaceSize=50M
* 方法区溢出测试
*/
public class NonHeapOOM {
List<Class<?>> list=new ArrayList<Class<?>>();
public String nonheap() throws Exception{
while(true){
list.addAll(MetaSpaceOOMTest.createClasses());
Thread.sleep(5);
} }
}
4.2、栈空间溢出
Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。
-
-Xss:设置每个线程的堆栈大小。
-
JDK 5以后每个线程堆栈大小为1M,
-
JDK 5以前每个线程堆栈大小为256K。
-
-
设置
-
根据应用的线 程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
-
但是操作系统对一个进程内的线程数还是有 限制的,不能无限生成,经验值在3000~5000左右。
-
-
线程栈的大小是个双刃剑:
-
如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大;
-
如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
-
/**
* @Author: king
* -Xss=1m : 栈的深度可达:7233
* -Xss=128k 栈深度可达:723
* 栈溢出测试
*/
public class StackOOM {
public static long count = 0;
public static void method(long i) {
System.out.println(count++);
method(i);
}
public static void main(String[] args) {
method(1);
}
}
结果:通过测试可知:
-
栈大小: -Xss=1M时,栈的深度可达:7233;
-
栈大小:-Xss=128k时,栈的深度可达723;
5、内存大小参数配置
内存参数设置
|
运行结果:
OutOfMemoryError异常
|
问题分析
|
堆溢出
-Xmx10 最大值;
-Xms10 最小值
-XX:+HeapDumpOnOutOfMemoryError
|
Java.lang.OutOnfMemoryError:
Java heap space
|
确定是内存泄漏还是溢出:
泄漏 (Memory leak):该回收的对象没有被回收
溢出(Memory overflow):内存不够,增大内存
|
虚拟机
栈
和本地方法栈
-Xss128k 栈的大小128k -栈深度:723
-Xoss:本地方法栈的大小
|
递归 + 循环
达到最大栈深度:
StackOverFlowError
没有足够的空间:
OutOfMemeoryError
|
单个线程都是:StackOverFlowError
多个线程出现:OutOfMemoryError
每个线程的栈分配的越大越容易出现内存溢出;
|
运行时
常量池
溢出
-XX: PermSize=10M
-XX: MaxPermSize=10M
|
Stirng.intern()
java.lang.OutOfMemoryError:
PermGenSpace
|
(分配在方法区)属于永久代的一部分
|
方法区
溢出
用于存放class的类信息:类名+字段描述符 + 方法描述
-XX:PermSize=10M
-XX:MaxPermSize=10M
|
Stirng.intern()
java.lang.OutOfMemoryError:
PermGenSpace
|
有反射/ CGlib /jdk动态代理的场景需要注意;
|
本机
直接内存溢出
-XX:MaxDirectMemmorySize
默认与java堆的最大值-Xmx一样大
-Xmx=20M
-XX:MaxDirectMemorySize=10m
|
java.lang.OutOfMemoryError
|
NIO中:
Bytebuffer bb = ByteBuffer.allocateDircet(1024);
堆内使用4 Byte,堆外使用1024个字节,当bb回收后才会回收堆外的内存,还没触发GC,此时容易造成内存泄漏,溢出;
|
6、小结
虚拟机内存包括:java虚拟机栈 + 本地方法栈 + 程序计数器 + 堆空间 + 方法区-元空间 + 虚拟机系统和进程本身。
程序计数器(Program Counter Register
): 唯一一个没有OutOfMemoryError的区域,
虽然说内存越来越大,但是内存资源十分宝贵,需要谨慎使用。
内存泄漏和内存溢出的区别?
-
内存泄漏:本该及时回收的内存没有被回收。
-
内存溢出:内存不够用。
两者关系:内存泄漏导致内存溢出,但是内存溢出也有可能是大对象导致的,单纯的内存不足,或者连续内存不足。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。