本文主要为《深入理解Java虚拟机》第三章的读书记录笔记,同时伴有一些网络上资料的总结。
Java堆是JVM主要的内存管理区域,里面存放着大量的对象实例和数组。在垃圾回收算法和垃圾收集器之前,首先要做的就是判断哪些对象已经“死去”,需要进行回收即不可能再被任何途径使用的对象。
引用计数法是这样:给对象中添加一个引用计数器,每当有一个地方使用它时,计数器值就加1。当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不可能再被使用的。
现在主流的Java虚拟机都没有使用引用计数法,最主要的原因就是它很难解决对象之间互相循环引用的问题。
可达性分析的基本思路:通过一系列称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,如果从GC Roots到一个对象不可达,则证明此对象是不可用的,如下图所示。
Java语言中,可作为GC Roots的对象包括下面几种:
对于Java程序而言,对象基本都位于堆内存中,简单来说GC Roots就是有被堆外区域引用的对象。
在JDK 1.2
以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于(reachable
)可达状态,程序才能使用它。
从JDK 1.2
版本开始,对象的引用被划分为4
种级别,从而使程序能更加灵活地控制对象的生命周期。这4
种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用是使用最普遍的引用,如下的方式就是强引用:
Object strongReference = new Object(); 复制代码
举例来说,
public void test() { Object strongReference = new Object(); // 省略其他操作 } 复制代码
strongReference = null
后。这个对象不再被GC Roots可达,那么这个对象在下次GC时就会被回收。class Obj { pulic static Object strongReference = new Object(); } 复制代码
如果对象只具有软引用,则
// 强引用 String strongReference = new String("abc"); String str = new String("abc"); // 软引用 SoftReference<String> softReference = new SoftReference<String>(str); 复制代码
软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>(); // 强引用 String str = new String("abc"); SoftReference<String> softReference = new SoftReference<>(str, referenceQueue); // 消除强引用 str = null; // Notify GC System.gc(); System.out.println(softReference.get()); // abc Reference<? extends String> reference = referenceQueue.poll(); System.out.println(reference); //null 复制代码
注意:
也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽可能保留。
应用场景:
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
这时候就可以使用软引用,很好的解决了实际的问题:
// 获取浏览器对象进行浏览 Browser browser = new Browser(); // 从后台程序加载浏览页面 BrowserPage page = browser.getPage(); // 将浏览完毕的页面置为软引用 SoftReference softReference = new SoftReference(page); // 消除强引用 page = null; // 回退或者再次浏览此页面时 if(softReference.get() != null) { // 内存充足,还没有被回收器回收,直接获取缓存 page = softReference.get(); } else { // 内存不足,软引用的对象已经回收 page = browser.getPage(); // 重新构建软引用 softReference = new SoftReference(page); } 复制代码
相比较软引用,只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它锁管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
String str = new String("abc"); WeakReference<String> weakReference = new WeakReference<>(str); // 消除强引用 str = null; 复制代码
同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用的对象被垃圾回收,JVM就会把这个弱引用加入到与之关联的引用队列中。
ReferenceQueue<String> queue = new ReferenceQueue<>(); String str = new String("abc"); WeakReference<String> weakReference = new WeakReference<>(str, queue); str = null; System.gc(); try { // 休息几分钟,等待上面的垃圾回收线程运行完成 Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(weakReference.get()); // null System.out.println(queue.poll()); // java.lang.ref.WeakReference@22a71081 复制代码
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
应用场景:
虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
String str = new String("abc"); ReferenceQueue queue = new ReferenceQueue(); // 创建虚引用,要求必须与一个引用队列关联 PhantomReference pr = new PhantomReference(str, queue); 复制代码
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
标记-清除算法分为“标记”和“清除”两个阶段,执行过程如下图所示:
标记-清除算法主要有两个不足:
复制算法的大致思路如下,其执行过程如下图所示:
复制算法的代价就是将内存缩小为原来的一半。
现在的商业虚拟机都是采用复制算法来回收新生代。
标记-整理算法分为“标记”和“整理”两个阶段,执行过程如下图所示:
分代收集算法就是降Java堆分为新生代和老年代,根据其各自的特点采用最适当的收集算法。
JVM垃圾收集器发展历程大致可以分为以下四个阶段: Serial(串行)收集器 -> Parallel(并行)收集器 -> CMS(并发)收集器 -> G1(并发)收集器
下图展示了7种作用域不同分代的收集器,如果两个收集器之间存在连续,就说明它们可以搭配使用。下面逐一介绍这些收集器的特性、基本原理和使用场景。
Serial类收集器是一个单线程的收集器:
它只会用单个收集线程去进行垃圾回收的工作
它在进行垃圾收集的时候会“Stop The World”暂停其他所有的工作表线程,直到它收集结束
Serial收集器采取复制算法在新生代进行单线程的回收工作
Serial Old收集器采取标记-整理算法在老年代进行单线程的回收工作
Parallel类收集器就是Serial收集器的多线程版本:
Parallel Scavenge收集器还有一个开关参数-XX: UseAdaptiveSizePolicy,打开这个开关后就不用手动指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio)等细节参数了,JVM会动态调整这些参数已提供最合适的停顿时间或者最大吞吐量。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是一个基于标记-清除算法实现的,运作过程分为4个步骤:
初始标记(CMS initial mark): 需要“Stop The World”,仅仅只是标记下GC Roots能直接关联到的对象,速度很快
并发标记(CMS concurrent mark): CMS线程与应用线程一起并发执行,从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长
重新标记(CMS remark):重新标记就是为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,可以多线程并行
并发清除(CMS concurrent sweep):CMS线程与应用线程一起并发执行,进行垃圾清除
CMS收集器优点:并发收集、低停顿
CMS的三个明显的缺点:
同优秀的CMS一样,G1也是关注最小停顿时间的垃圾回收器,也同样适合大尺寸堆内存,官方也推荐用G1来代替选择CMS。
G1之前的JVM堆内存模型,堆被分为新生代,老年代,永久代(1.8之前,1.8之后是元空间),新生代中又分为Eden和两个Survivor区。
G1收集器的堆内存模型,堆被分为很多个大小连续的区域(Region),Region的大小可以通过-XX: G1HeapRegionSize参数指定,大小区间为[1M,32M]。
每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor,老年代和巨型区(Humongous Region)。巨型区域是为了存储超过50%标准region大小的巨型对象。
G1可以在新生代和老年代使用,而CMS只能在老年代使用。
G1是复制+标记-整理算法,CMS是标记清除算法。
G1收集器的工作流程大致分为如下几个步骤:
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一次mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region。这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制
G1没有fullGC概念,需要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)。