目录
GC 回收机制
垃圾回收(Garbage Collection, GC)
垃圾回收算法
JVM 分代回收策略
1. 新生代
2. 老年代
GC Log 分析
引用
GC 回收机制
垃圾回收(Garbage Collection, GC)
垃圾就是内存中已经没有用的对象,JVM 中的垃圾回收器(Garbage Collector)会自动回收,Java 虚拟中使用“可达性分析”算法来决定对象是否可以被回收。如下图:
以 GC Root 作为起始点, 从这些节点开始向下搜索,所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。例如,上图中对象 A, B, C, D, E 与 GC Root 之间都存在一条直接或间接的引用链,这也代表他们与 GC Root 之间是可达的,因此它们是不能被 GC 回收掉的。而对象 M, K 虽然被对象 J 引用到,但是并不存在一条引用链连接 GC Root,所以 GC 回收时,只要遍历到 J, K, M 这三个对象,就会将它们回收。
在 Java 中,有以下几种对象可以作为 GC Root:
1. Java 虚拟机栈(局部变量表)中引用的对象;
2. 方法区中静态引用指向的对象;
3. 仍处于存活状态中的线程对象;
4. Native 方法中 JNI 引用的对象;
一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收:
1. Allocation Failure:在堆内存分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
2.
System.gc()
在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
垃圾回收算法
1. 标记清除算法(Mark and Sweep GC)
从 “GC Roots” 集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾并回收。
过程分两步:
Mark 标记阶段:找到内存中所有的 GC Root 对象,只要和 GC Root 直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象);
Sweep 清除阶段:遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接删除。
优点:实现简单,不需要将对象进行移动;
缺点:需要中断进程内其它组件的执行(即,Stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
2. 复制算法(Copying)
将现有的内存空间分为两块,每次只使用其中一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存的角色,完成垃圾回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片;
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
3. 标记-压缩算法(Mark-Compact)
需要先从根节点开始对所有可达对象做一次标记,之后并不简单的清除未标记的对象,而是将所有的存活对象压缩到内存的一端,最后清理边界外所有的空间。
过程分两步:
Mark 标记阶段:找到内存中所有的 GC Root 对象,只要和 GC Root 直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象);
Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
优点:即避免了碎片的产生,又不需要两块相同的内存空间,其性价比比较高;
缺点:所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。
JVM 分代回收策略
Java 虚拟机根据对象存活的周期不同,把堆内存划分为新生代、老年代,这就是 JVM 的内存分代策略。
1. 新生代
新生成的对象优先存放在新时代中,存活率很低。新生代中,常规的一次 GC 一般可以回收 70%~ 90% 的空间,回收率很高。所有,在新时代中采用的GC 回收算法是复制算法。
新生代细分为3个部分:Eden, Survivor0(简称 S0), Survivor1(简称 S1),这三部分按照 8 : 1 : 1的比例来划分新生代。
2. 老年代
一个对象如果在新生代存活了足够长的时间(15次 GC 后仍存活)而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代。可以使用
-XX:PretenureSizeThreshold
来控制直接升入老年代的对象大小。因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。
GC Log 分析
为了让上层应用开发人员更加方便的调试 Java 程序,JVM 提供了相应的 GC 日志。在 GC 执行垃圾回收事件的过程中,会有各种相应的 log 被打印出来。其中新生代和老年代所打印的日志是有区别的:
通过终端运行如下程序:(初始堆内存大小20M,新生代10M,则老年代也为10M)
package software_test;/*** @author 别偷我的猪* VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8*/
public class java_GCLog {private static final int _1MB = 1024 * 1024;public static void testAllocation() {byte[] a1, a2, a3, a4;a1 = new byte[2 * _1MB];a2 = new byte[2 * _1MB];a3 = new byte[2 * _1MB];a4 = new byte[1 * _1MB];}public static void main(String[] args) {// TODO Auto-generated method stubtestAllocation();}}
设置 JVM 运行参数:
VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
打印日志如下:
在给a4分配内存之前,Eden区已经被占用6M,因此会执行一次MinorGC,并尝试将存活的al、a2、a3复制到S1区,但是S1区只有1M空间,所以没有办法存储a1、a2、 a3任意一个对象,在这种情况下al、a2、a3将被转移到老年代,最后将a4保存在Eden区
日志中各个字段的意义如下:
新生代 GC:
这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较块。
老年代 GC:
发生在这一区域的 GC 也叫做 Major GC 或者 Full GC。当出现 Major GC,经常会伴随至少一次的 Minor GC。
引用
通过 GC Roots 的引用可达性来判断对象是否存活。
JVM 中的引用关系根据引用强度的由强到弱,可分为:
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)