1.JVM的内存模型
JVM的堆内存被分为两个部分:年轻代(Young Generation)和老年代(Old Generation)
年轻代:
年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。
年轻代是所有新对象产生的地方。当年轻代的内存空间被用完时,就会触发垃圾回收,这个垃圾回收叫做Minor GC。
年轻代的特点:
- 大多数新建的对象都位于Eden区
- 当Eden区被填满时,就会执行Minor GC,并且把存活下来的对象转移到其实一个Survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,依然存活下来的对象会被转移到老年代的内存空间,通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
老年代:老年代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
老年代的垃圾回收叫做Major GC,通常与Full GC是等价的,收集整个GC堆
2.内存溢出的演示与分析
这里我们需要用到两个工具:jvisualvm与mat
jvisualvm是一个JDK自带的JVM分析工具,这里放一个mat的下载地址:下载
我们编写一段代码来测试,想要让堆内存溢出,最简单的办法就是写一个循环往List里面丢数据,然后又不使用它。
import java.util.ArrayList;
import java.util.List;
public class Test {
private static final Integer k = 1024;
public static void main(String[] args) {
int size = k * k * 8;
List<Byte[]> list = new ArrayList<>();
for (int i = 0; i < k; i++) {
System.out.println("JVM写入数据" + (i + 1) + "M");
try{
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Byte[size]);
}
}
}
然后我们查看监视页面:
发现堆内存一直在升高,此时我们执行堆Dump,用mat开始分析究竟是什么原因产生的内存溢出
我们用mat打开刚才生成的hprof快照,生成位置在这里可以看到:
我们用Mat查看,会发现它已经帮我们定位了一个问题:
我们先点击See stacktrace,查看有问题的代码片段:
出现问题的地方都有了,那么我们排除问题也就是时间的问题了。