前言
垃圾回收主要针对于堆内存空间,从前面学习了jvm的内存结构,我们知道:
程序计数器是不会造成内存溢出的,不需要考虑GC。
虚拟机栈存在于线程中,线程一死亡,内存就会自动回收,也不需要考虑GC。
本地方法栈也不需要考虑GC。
堆内存会存在内存溢出,因此需要GC管理。
元空间使用的是本地内存,即指的是JVM外的由操作系统控制的内存区域,是否会被GC?
一、如何判断对象可以回收
1.1 引用计数法
当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。
1.2 可达性分析算法
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法
所谓“GC roots”或者说tracing GC的“根集合”就是一组必须活跃的引用。
基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。
1、JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
2、扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
3、可以作为 GC Root 的对象
- 虚拟机栈(栈帧中的本地变量表,也叫做局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的Native方法)引用的对象
public static void main(String[] args) throws IOException {
ArrayList<Object> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add(1);
System.out.println(1);
System.in.read();
list = null;
System.out.println(2);
System.in.read();
System.out.println("end");
}
对于以上代码,可以使用如下命令将堆内存信息转储成一个文件,然后使用
Eclipse Memory Analyzer 工具进行分析。
第一步:
使用 jps 命令,查看程序的进程
第二步:
使用 jmap -dump:format=b,live,file=1.bin 16104 命令转储文件
dump:转储文件
format=b:二进制文件
file:文件名
16104:进程的id
第三步:打开 Eclipse Memory Analyzer 对 1.bin 文件进行分析。
分析的 gc root,找到了 ArrayList 对象,然后将 list 置为null,再次转储,那么 list 对象就会被回收。
二、垃圾回收算法
2.1 标记清除
定义:Mark Sweep
- 速度较快
- 会产生内存碎片
清除:不会真正的对对象做清零操作,只需要将标志好的对象占用内存的起始地址放到一个空闲的地址列表就可以,
下次分配新对象的时候直接到空闲列表里找,看有没有一块足够大的空间容纳我的新对象,若有就进行空间分配。
优点:不需要做内存清理操作,直接覆盖,因此速度快
缺点:会产生内存碎片,内存不连续。当分配一个较大的新对象时,在空闲列表中找不到足够大的内存,就无法分配,造成内存溢出。
2.2 标记整理
Mark Compact
- 速度慢
- 没有内存碎片
2.3 复制
Copy
- 不会有内存碎片
- 需要占用两倍内存空间
2.4 分代垃圾回收
分代回收:将内存划分为新生代和老年代,新生代存放一些用完就可以丢弃的对象,老年代存放一些可以存活很久的对象,如此,就可以根据两者不同的情况,采用合理的垃圾回收算法,从而提高回收效率。新生代执行回收频率比较高,老年代的较底。
工作过程:
1、新创建的对象会先分配在伊甸园区(伊甸园命名来源于神话亚当夏娃)。
2、伊甸园区空间不足时,触发minor gc,通过垃圾回收-复制算法,将“伊甸园存活的对象”和“from区的全部对象”复制到幸存区to中,并为这些对象年龄加一,然后清理伊甸园和from区。
3、幸存区from和幸存区to的引用地址互相交换,即from重新指向to原地址,to重新指向from原地址。
4、重复着1、2、3的步骤。
5、当幸存区to对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit);或者当新生代空间不足时,部分对象也会晋升到老年代。
6、当新生代和老年代空间都不足时,会先触发 minor gc,如果空间仍然不足,此时就会触发full gc对老年代做一个清理,空间足够则存入,反之则就抛出内存溢出异常
注意:minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行,避免出现地址混乱,但暂停的时间不会太长。而full gc的暂停时间则较长
三、GC分析
3.1 相关 JVM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
3.2 演示内存分配策略
3.2.1 初始堆空间信息
vm参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
空方法情况下运行:
/**
* 演示内存的分配策略
*/
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// 初始堆 堆最大 新生代 垃圾回收器 打印GC信息
public static void main(String[] args) throws InterruptedException {
}
}
得到结果:
新生代总大小9M,以使用2M多
伊甸园大小8M,使用内存比例28%
幸存区from大小1M,使用0
幸存区to大小1M,使用0
老年代总大小10M,使用0
元空间内存信息也打印了,但要注意其并不属于堆的一部分
1、vm参数新生代分配了10M,为什么打印信息只有9M?
因为新生代分为三个区,默认内存比例为8:1:1,而幸存区to被认为一直是空的,因此不计算在内。
3.2.2 伊甸园空间不足触发垃圾回收
vm参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
修改main方法运行:
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
已知伊甸园大小8M,使用内存比例28%,因此会触发垃圾回收:
结合前面分代回收过程,我们可以看到,伊甸园和from的内存占用情况都发生了变化。
3.2.3 新生代空间不足晋升老年代
vm参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
修改main方法运行:
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_1MB]);
}
已知伊甸园大小8M,上一个实验新生代total 9216K, used 8235K,因此会触发二次垃圾回收:
第二次垃圾回收,新生代total 9216K, used 1191K;老年代total 10240K, used 7812K;_7MB对象晋升老年代了。
3.2.4 大对象直接晋升老年代
vm参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
修改main方法运行:
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
未创建对象时,eden space 8192K, 28% used ,此时加入一个8M的对象,而伊甸园是放不下的,新建的对象会直接晋升老年代,不需GC操作。
3.2.5 堆内存不够触发发老年代Full GC
vm参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
修改main方法运行:
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
分析:第一次加入8M对象时,老年代total 10240K, used 8192K;新生代total 9216K, used 2498K;因此第二次加入8M对象时,会出现内存溢出。
发生了两次GC,因为第二次加入的是8M对象,因此虚拟机已经直到新生代容不下了,就直接晋升老年代,但老年代空间不足,于是发起第一次GC对老年代进行垃圾回收;但空间仍旧不足,于是再发起Full GC回收尝试补救一下;仍不行 抛异常。
3.2.6 线程内存溢出不会导致进程提前结束
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
Thread.sleep(1000L);
System.out.println("sleep....");
}