1.1 如何进行垃圾回收
由于垃圾收集算法的实现涉及大量的程序细节,各家虚拟机厂商对其实现细节各不相同,因此本课时并不会过多的讨论算法的实现,只是介绍几种算法的思想以及优缺点。
1.1.1 标记清除算法(Mark and Sweep GC)
- Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
- Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。
- 优点:实现简单,不需要将对象进行移动。
- 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
1.1.2 复制算法(Copying)
-
复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:
标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
1.1.3 标记-压缩算法 (Mark-Compact)
- Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
- Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
- 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
- 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
1.2 JVM分代回收策略
Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般为新生代,老年代注意: 在 HotSpot 中除了新生代和老年代,还有永久代。
分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,次区域对象生命周期较短,多次回收不掉的就会转移大老年代中。
1.2.1 新生代
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。
新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。这 3 块区域的内存分配过程如下:
绝大多数刚刚被创建的对象会存放在 Eden 区。如图所示:
当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。如图所示:
下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden和 S0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。如图所示:
如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。如图所示:
1.2.2 老年代
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。
注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。
1.3 引用
上文中已经介绍过,判断对象是否存活我们是通过GC Roots的引用可达性来判断的。但是JVM中的引用关系并不止一种,而是有四种,根据引用强度的由强到弱,他们分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
平时项目中,尤其是Android项目,因为有大量的图像(Bitmap)对象,使用软引用的场景较多。所以重点看下软引用SoftReference的使用,不当的使用软引用有时也会导致系统异常。
1.3.1 引用软引用常规使用
public class SoftReferenceDemo {
static class SoftObject {
byte[] data = new byte[1*1200 * 1024 * 1024];//120M
}
public static void main(String[] args) throws InterruptedException{
//将缓存数据用软引用持有
SoftReference cacheRef = new SoftReference<>(new SoftObject());
System.out.println("第一次gc前 软引用" + cacheRef.get());
//进行一次gc后查看对象的回收情况
System.out.println("第一次gc后 软引用" + cacheRef.get());
//再分配一个120M的对象 看看缓存对象的回收情况
SoftObject newSo = new SoftObject();
System.out.println("再分配120M强引用对象之后 软引用" + cacheRef.get());
}
}
执行:java -Xmx200m SoftReferenceDemo
运行结果:
第一次gc前 软引用com.example.leetcode.SoftReferenceDemoSoftObject@66d3c617
再分配120M强引用对象之后 软引用
null
首先通过-Xmx将堆最大内存设置为200M。从日志中可以看出,当第一次GC时,内存中还有剩余可用内存,所以软引用并不会被GC回收。但是当我们再次创建一个120M的强引用时,JVM可用内存已经不够,所以会尝试将软引用给回收掉。
1.4 总结:
本课时着重讲解了 JVM 中有关垃圾回收的相关知识点,其中重点介绍了使用可达性分析来判断对象是否可以被回收,以及 3 种垃圾回收算法。
虚拟机垃圾回收机制很多时候都是影响系统性能、并发能力的主要因素之一。尤其是对于从事 Android 开发的工程师来说,有时候垃圾回收会很大程度上影响 UI 线程,并造成界面卡顿现象。