Java相对于C/C++语言来说,最明显的特点在于Java引入了自动垃圾回收。垃圾回收(Garbage Collection简称GC)可以使程序员不在需要关心JVM内存管理的问题,专注于写程序本身。平时程序员是很难感知到GC的存在,但是如果涉及到一些性能调优,线上的问题排查等等,深入地了解GC是必不可少的。往往通过一些JVM参数的设置能就使系统性能提高不少。
要深入了解GC,首先要明白GC会回收哪些数据,数据位于哪个区域。接着我们看一下JVM的内存区域。
从图中可以看出,内存区域分为五个:
上面讲了GC主要作用的区域是在堆中,那么又是怎么判断是否可以回收的呢?在GC里面有两种算法来判断,一种是引用计数,对象引用的次数为0就是垃圾,另一种是可达性算法,如果一个对象不在以GC Root根节点为起点的引用链中,则视为垃圾。
首先看引用计数法,简单点说对象被引用,就会在此对象的对象头上计数器加一,每当有一个引用失效时计数器的值减一,如果没有引用(引用次数为0)则此对象可回收。但是这种算法很难解决对象之间互相循环引用的问题。
所谓的GC Roots就是一组必须活跃的引用,基本思路就是从一系列的GC Root一直往下搜索,通过GC Root串成的一条线称为引用链,如果有对象不在任何一条以GC Root为起点的引用链中,则此对象就会被GC回收,这就是可达性算法。
哪些对象可作为GC Root对象呢:
上面已经讲了如何判断哪些对象时可回收的。那么判断完是否可回收后,GC又是使用什么算法进行回收的呢?这就要讲一讲垃圾回收的几种方式:
其实很简单,分为标记和清除两个步骤。第一步根据可达性算法标记被回收的对象,第二步回收被标记的对象。
明显这种垃圾回收算法的缺点是很容易产生内存碎片。
前面两个步骤和标记清除算法一样,而不同的是在标记清除算法的基础上多了一步整理的过程。如图所示,整理步骤的时候,将所有存活的对象都往左边移动,然后清理另一端的所有区域,这样就不会产生内存碎片。
虽然不会产生内存碎片,但是由于频繁地移动存活的对象,所以效率十分低下。
把内存分成两份,分别是A区域和B区域,第一步根据可达性算法把存活的对象标记出来,第二步把存活的对象复制到B区域,第三步把A区域全部清空。这就是复制算法。
复制算法不会产生内存碎片,并且不需要频繁移动存活的对象,而缺点就是内存利用不充分,比如一块500M的内存,要分成两份,只能利用到250M。
分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代收集算法是第四个算法,不如说它是对前三个算法的实际应用。
首先我们先探讨一下对象的不同特性,内存中的对象其实可以根据生命周期的长短大致分为三种:
上述的对象对应在内存中的区域就是,夭折对象和持久对象在Java堆中,永久对象在方法区。
分代算法的原理就是根据对象的存货周期不同将堆分为年轻代和老年代。新生代又分为Eden 区,from Survivor 区(S0区),to Survivor 区(S1区),比例为8:1:1。
先看年轻代的GC,年轻代采用的回收算法是复制算法。新建的对象被创建后就会分配在Eden 区,当Eden区将满时,就会触发GC。
在这一步GC会把大部分夭折对象回收,根据可达性算法标记出存活的对象,把存活对象复制到S0区,然后清空Eden 区。
接着继续到下一次触发GC时,就会把Eden区和S0区的存活对象复制到S1区,然后清空Eden区和S0区。每次垃圾回收后S0和S1区的角色互换。每次GC后,如果对象存活下来则年龄加一。
我们知道在年轻代中存活得越久的对象,年龄会越大,如果存活对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代。由于老年代的对象一般不会经常回收,所以采用的算法是标记整理法,老年代的回收次数相对较少,每次回收时间比较长。
Java中Stop The World机制简称STW,执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集器之外),当垃圾回收完成后,再继续运行,所以尽量减少STW的时间,就是优化JVM的主要目标。
垃圾收集器其实就是上面讲的算法的具体实现,目前没有说哪个垃圾收集器是最好的,只有根据应用的特点选择最合适的,所以说合适的才是最好的。
常见的垃圾收集器除了G1垃圾收集器外,都是只作用于一个区域,要么年轻代要么老年代,所以一般是配合使用,总共有7种,怎么配合使用,请看下面这张图,有连线的就是可以配合使用的。
Serial收集器作用于年轻代,单线程的垃圾收集器,单线程意味着它只会使用一个CPU或者一个线程去完成垃圾回收的工作,当它在垃圾回收时,由于SWT机制,其他工作线程都会被暂时挂起,直到垃圾回收完成。这种垃圾收集器适用于Client模式的应用,在单CPU的环境下,由于没有和其他线程交互的开销,可以专心垃圾回收的工作,能够把单线程的优势发挥到极致,简单高效。通过-XX:+UseSerialGC可以开启这种回收模式。
ParNew 收集器是Serial收集器的多线程版本,作用于年轻代,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改ParallelGCThreads设定。
Parallel Scavenge收集器也被称为吞吐量优先收集器,作用于年轻代,多线程采用复制算法的垃圾收集器,跟ParNew 收集器有些类似。和ParNew 收集器不同的是,Parallel Scavenge收集器关注的是吞吐量,它提供了两个参数来控制吞吐量,分别是-XX:MaxGCPauseMillis(控制最大的垃圾收集停顿时间)、 -XX:GCTimeRatio(直接设置吞吐量大小)。
如果设置了-XX:+UseAdaptiveSizePolicy参数,虚拟机就会根据系统的运行情况收集监控信息,动态调整新生代的大小,Eden,Survivor比例等,以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标,这种调节方式称为GC的自适应调节策略。这也是Parallel Scavenge收集器和ParNew 收集器最大的区别。
Serial Old 收集器是工作在老年代的单线程垃圾收集器,采用的算法是标记整理算法。在Client模式下可以和Serial收集器配合使用,如果在Server模式的应用,在JDK1.5之前可以和Parallel Scavenge收集器配合使用,另一种使用场景则是CMS垃圾收集器的后备预案,在发生Concurrent Mode Failure使用。
Parallel Old 收集器是Parallel Scavenge收集器的老年代版本,多线程收集,采用标记整理算法。下图是Parallel Scavenge收集器和Parallel Old 收集器配合工作的过程图。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,采用标记-清除算法。适用于希望系统停顿时间短,给用户更好的体验的场景。
CMS收集器运行时主要分为四个步骤:
CMS收集器的缺点在于:
G1垃圾回收器主要是面向服务端的垃圾回收器,年轻代和老年代都可使用。运作时,整体上采用标记整理算法,局部上看是采用复制算法,两种算法都不会产生内存碎片,所以回收器在回收后能产生连续的内存空间。
它是专门针对以下场景设计的:
G1垃圾回收器的内存分区不再采用传统的内存分区,将新生代,老年代的物理空间划分取消了。
取而代之的是,把堆内存分成若干个Region(区域),每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的STW。G1垃圾回收器和传统的垃圾回收器的最大区别就在于,弱化了分代概念,引入了分区的思想。
G1中每代的存储地址都不是连续的,而是使用了不连续的大小相同的Region。除此之外G1中还多了一个H,H代表Humongous,用于存储巨大对象(humongous object),当对象大小大于等于region一半的对象,就直接分配到了老年代,防止了反复拷贝移动。
G1垃圾回收过程可分为四步:
本文的简述了JVM的垃圾回收的理论知识,思路是先搞懂GC作用的区域是在堆中,然后介绍可达性算法的作用是为了标记存活的对象,知道哪些是可回收对象,接着就是使用垃圾回收算法进行回收,然后介绍了常见的几种垃圾回收算法(标记清除,复制算法,标记整理),最后再介绍常见的几种垃圾回收器。
对于垃圾回收器的介绍,这里只是简单的描述,并没有深入地讲解,因为每一个垃圾回收器如果展开细述都能讲上半天,所以有兴趣的话,可以自己再去探索一下,个人认为CMS和G1垃圾回收器是比较重要的两种。
这篇文章就讲到这里了,希望看完之后能对你有所帮助,感谢大家的阅读。
觉得有用就点个赞吧,你的点赞是我创作的最大动力~
我是一个努力让大家记住的程序员。我们下期再见!!!
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!