GC 回收机制与分代回收策略
所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作**可达性分析的算法**来决定对象是否可以被回收。 []( )可达性分析 ------------------------------------------------------------------- `JVM` 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”`GC Root`"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:![](https://static001.geekbang.org/infoq/df/df3e66fd5fd38a57e425aa9ad14c60e2.png) > **注意**:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。 []( )GC Root 对象 ------------------------------------------------------------------------ 在 Java 中,有以下几种对象可以作为 `GC Root`: 1. Java 虚拟机栈(局部变量表)中的引用的对象。 2. 方法区中静态引用指向的对象。 3. 仍处于存活状态中的线程对象。 4. Native 方法中 JNI 引用的对象。 > **注意**:全局变量同静态变量不同,它不会被当作 GC Root。 []( )什么时候回收 -------------------------------------------------------------------- 不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。 1. `Allocation Failure`:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。 2. `System.gc()`:在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。 []( )如何回收垃圾 -------------------------------------------------------------------- 由于垃圾收集算法的实现涉及大量的程序细节,各家虚拟机厂商对其实现细节各不相同,因此并不会过多的讨论算法的实现,只是介绍几种算法的思想以及优缺点。 ### []( )标记清除算法(Mark and Sweep GC) 从”`GC Roots`”集合开始,将内存整个遍历一次,保留所有可以被 `GC Roots` 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。 1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。 2. Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。 ![标记清除算法](https://static001.geekbang.org/infoq/7a/7a5937861fdb120a0f18da52ee231fae.png) * 优点:实现简单,不需要将对象进行移动。 * 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。 ### []( )复制算法(Copying) 将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。 1. 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:![](https://static001.geekbang.org/infoq/8f/8fd15dedcabc4a704f70fbb62bf0b345.png) 2. 标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:![](https://static001.geekbang.org/infoq/a5/a510bccf94834b1426b3c6fdf741a035.png) * 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 * 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 ### []( )标记-压缩算法 (Mark-Compact) 需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成: 1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。 2. Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。 ![](https://static001.geekbang.org/infoq/c7/c7d73103152e8ac2e1a5e99fb8d09ba3.png) * 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。 * 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。 []( )JVM 分代回收策略 ----------------------------------------------------------------------- Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代。 ### []( )新生代 新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是**复制算法**。 新生代又可以继续细分为 3 部分:`Eden`、`Survivor0(简称 S0)`、`Survivor1(简称 S1)`。这 3 部分按照 8:1:1 的比例来划分新生代。![](https://static001.geekbang.org/infoq/fc/fc59198d66707cec15a4340c0a90a769.png) * 绝大多数刚刚被创建的对象会存放在?Eden 区。 * 当?`Eden`?区第一次满的时候,会进行垃圾回收。首先将 `Eden` 区的垃圾对象回收清除,并将存活的对象复制到 `S0`,此时 `S1` 是空的。 * 下一次?`Eden`?区满时,再执行一次垃圾回收。此次会将?`Eden` 和 `S0` 区中所有垃圾对象清除,并将存活对象复制到 `S1`,此时 `S0` 变为空。 * 如此反复在 `S0` 和 `S1` 之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。 ### []( )老年代 一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。 我们可以使用?`-XX:PretenureSizeThreshold`?来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用**标记压缩的回收算法**。
评论