JVM垃圾回收机制是java程序员必须要了解的知识,对于程序调优具有很大的帮助(同时也是大厂面试必问题)。
整个篇章会比较长,但很清晰明了,我觉得如果想要了解垃圾收集机制这篇文章基本上可以了,也是我在多次学习之后记录下来的笔记。废话不多说,开始!
要了解垃圾回收机制,主要从三个方面:
(1)垃圾回收面向的对象是谁?
(2)垃圾回收算法有哪些?
(3)垃圾收集器有哪些?每个收集器有什么特点。
接下来一一讲解清楚:
一、垃圾回收面向的对象
也就是字面意思,垃圾回收嘛,重要的是垃圾,那什么对象是垃圾呢,简单来说就是无用的或已死的对象。这样又引申出来什么对象是已死的,怎么判断对象是否已死?
判断对象是否已死有两种算法和对象引用分类:
(1)引用计数算法:
也是字面意思,通过给对象添加引用计数器,添加引用+1,反之-1。当引用为0的时候,此时对象就可判断为无用的。
优点:实现简单,效率高。
缺点:无法解决循环引用(就是A引用B,B也引用A的情况)的问题。
(2)根搜索算法(也就是GC Roots):
通过一系列称为GC Roots的对象,向下搜索,路径为引用链,当某个对象无法向上搜索到GC Roots,也就是成为GC Roots不可达,则为无用对象。
如果一个对象是GC Roots不可达,则需要经过两次标记才会进行回收,第一次标记的时候,会判断是否需要执行finalize方法(没必要执行的情况:没实现finalize方法或者已经执行过)。如果需要执行finalize方法,则会放入一个回收队列中,对于回收队列中的对象,如果执行finalize方法之后,没法将对象重新跟GC Roots进行关联,则会进行回收。
很抽象,对吧,来一个明了的解释?
比如手机坏了(不可达对象),有钱不在乎就直接拿去回收(这就是没实现finalize方法),如果已经修过但是修不好了(已经执行过finalize方法),就直接拿去回收站回收掉。如果没修过,就会拿去维修店(回收队列)进行维修,实在维修不好了(执行了finalize方法,但是无法连上GC Roots),就会拿去回收站回收掉了。
那什么对象可以成为GC Roots呢?
1>虚拟机栈中的引用对象
2>本地方法栈中Native方法引用的对象
2>方法区静态属性引用对象
3>方法区常量引用对象
(3)对象引用分类
1>强引用:例如实例一个对象,就是即使内存不够用了,打死都不回收的那种。
2>软引用:有用非必须对象,内存够,则不进行回收,内存不够,则回收。例如A借钱给B,当A还有钱的时候,B可以先不还,A没钱了,B就必须还了。
3>弱引用:非必须对象,只能存活到下一次垃圾回收前。
4>虚引用:幽灵引用,必须跟引用队列配合使用,目的是回收前收到系统通知。
下面是java的引用类型结构图:
(1)软引用示例
内存够用的情况:
运行结果:
内存不够用的情况:
运行结果:
(2)弱引用示例结果:
无论如何都会被回收
(3)虚引用示例:
运行结果:
解释:为什么2和5的输出为null呢,如下
3为null是因为还没有进行gc,所以对象还没加入到引用队列中,在gc后就加入到了引用队列中,所以6有值。
这个虚引用在GC后会将对象放到引用队列中,所以可以在对象回收后做相应的操作,判断对象是否在引用队列中,可以进行后置通知,类似spring aop的后置通知。
二、垃圾回收发生的区域
垃圾回收主要发生在堆内存里面,而堆内存又细分为年轻代和老年代,默认情况下年轻代和老年代比例为1:2,比如整个堆内存大小为3G,年轻代和老年代分别就是1G和2G,想要更改这个比例需要修改JVM参数-XX:NewRatio,
比如-XX:NewRatio=4,那老年代:年轻代=4:1。而年轻代又分为Eden区,S0(Survivor From)和S1(Survivor To)区,一般Eden:S0:S1=8:1:1,如果想要更改此比例,则修改JVM参数-XX:SurvivorRatio=4,此时就是Eden:S0:S1=4:1:1。
在年轻代发生GC称为Young GC,老年代发生GC成为Full GC,Young GC比Full GC频繁。
解析Young GC:
JVM启动后,第一次GC,就会把Eden区存活的对象移入S0区;第二次GC就是Eden区和S0一起GC,此时会把存活的对象移入S1区,S0清空;第三次GC就是Eden区和S1区进行GC,会把存活的对象移入S0区,如此往复循环15次(默认),就会把存活的对象存入老年区。
类似与如果有三个桶,编号分别为1(1号桶内的沙子是源源不断的,就像工地上。你们没去工地搬过砖可能不知道,但是我真的去工地上搬过啊),2,3。1里面装有沙子,需要将沙子筛为细沙。首先将桶1内的沙子筛选一遍过后的放置于桶2,第二次筛选就会将桶1和桶2里面的沙子一起筛,筛完之后放到桶3内,桶2清空。第三次筛选就会将桶1和桶3的沙子一起筛选,晒完放到桶2内,桶3清空。如此往复循环15次,桶2或桶3里面的沙子就是合格的沙子,就需要放到备用桶内以待使用。
上述中桶1就是Eden区,桶2就是S0区,桶3就是S1区。
三、垃圾回收算法
三种,分别是复制算法,标记-清除算法,标记-整理算法。
(1)复制算法。
其会将内存区域分成同样大小的两块,一块用来使用,另外一块在GC的时候存放存活的对象,然后将使用的一块清除。如此循环往复。
适用于新生代。
优点:没有内存碎片,缺点:只能使用一般的内存。
(2)标记-清除算法。
使用所有内存区域,在GC的时候会将需要回收的内存区域先进行标记,然后同意回收。
适用于老年代。
缺点:产生大量内存碎片,会直接导致大对象无法分配内存。
(3)标记-整理算法。
使用所有内存区域,在GC的时候会先将需要回收的内存区域进行标记,然后将存活对象忘一边移动,最后将清理掉边界以外的所有内存。
适用于老年代。
四、GC日志查看
利用JVM参数-XX:+PrintGCDetails就可以在GC的时候打印出GC日志。
年轻代GC日志:
老年代GC日志:
五、垃圾收集器
主要有四类收集器以及七大收集器
四类:
(1)Serial:单线程收集器,阻塞工作线程,它一工作,全部都得停下。
(2)Paralle:Serial的多线程版本,也是阻塞工作线程
(3)CMS(ConcMarkSweep):并行垃圾收集器,可以和工作线程一起工作。
(4)G1:将堆分成大小一致的区域,然后并发的对其进行垃圾回收。
怎么查看默认的收集器呢?
用JVM参数-XX:+PrintCommandLineFlags,运行之后会输出如下参数。可以看到,jdk1.8默认是Parallel收集器。
七大收集器:
(1)Serial:串行垃圾收集器,单线程收集器。用于新生代。用JVM参数-XX:+UseSerialGC开启,开启后Young区用Serial(底层复制算法),Old区用Serial Old(Serial的老年代版本,底层是标记整理算法)。
(2)ParNew:用于新生代,并行收集器。就是Serial的多线程版本。用JVM参数-XX:+UseParNewGC,young:parnew,复制算法。Old:serialOld,标记整理算法。-XX:ParallecGCThreads限制线程回收数量,默认跟cpu数目一样。只是新生代用并行,老年代用串行。
(3)Parallel Scavenge:并行回收收集器。用JVM参数-XX:+UseParallelGC开启,young:parallel scavenge(底层是复制算法),old:parallel old(parallel的老年代版本,底层是标记整理),新生代老年代都用并行回收器。
这个收集器有两个优点:
可控的吞吐量:就是工作线程工作90%的时间,回收线程工作10%的时间,即是说有90%的吞吐量。
自适应调节策略:会动态调节参数以获取最短的停顿时间。
(4)Parallel Old:Parallel Scavenge的老年代版本,用的是标记整理算法。用JVM参数-XX:+UseParallelOldGC开启,新生代用Parallel Scavenge,老年代用Parallel Old
(5)CMS(ConcMarkSweep):并发标记清除。底层是标记清除算法,所以会产生内存碎片,同时也会耗cpu。以获取最短回收停顿时间为目标。-XX:+UseConcMarkSweep,新生代用ParNew,老年代用CMS。CMS必须在堆内存用完之前进行清除,否则会失败,这时会调用SerialOld后备收集器。
初始标记和重新标记都会停止工作线程,并发标记和并发清除会跟工作线程一起工作。
(6)SerialOld:老年代串行收集器(以后Hotspot虚拟机会直接移除掉)。
(7)G1:G1垃圾收集器,算法是标记整理,不会产生内存碎片。横跨新生代老年代。实现尽量高吞吐量,满足回收停顿时间更短。
G1可以精确控制垃圾收集的停顿时间,用JVM参数-XX:MaxGCPauseMillis=n,n为停顿时间,单位为毫秒。
区域化内存划片Region,会把整个堆划分成同样大小的区域块(1MB~32MB),最多2048个内存区域块,所以能支持的最大内存为32*2048=65535MB,约为64G。
上图是收集前和收集后的对比,有些对象很大,分割之后就是连续的区域,也即是上图的Humongous。
上述理论可能有点乏味,下图很清晰明了(某度找的)。
下面来一张整个垃圾回收机制的思维导图(太大,分成两部分)。