JVM学习笔记(3):垃圾回收机制详解
一、GC Roots的类型
JVM使用了可达性分析算法
,该算法会分析每个对象,看有谁在引用他,一层层判断有没有一个GC Roots
,在JVM规范中,局部变量是可以作为GC Roots
的,只要一个类的对象被局部变量引用了,那就说明有一个GC Roots
,就不能被回收了。当然,静态变量也可以看作是一种GC Roots
。总结下来就是,只要你的对象被方法的局部变量或是类的静态变量引用了,就不能回收它。
至此我们知道回收和引 用有关,Java中有四种引用类型,它们分别是:强引用
、软引用
、弱引用
和虚引用
,针对不同的引用类型又有不同的回收策略:
-
强引用
:一个变量引用一个对象。只要是强引用,垃圾回收时绝对不会去回收这个对象。 -
public class Kafka { private static ReplicaManager replicaManager = new ReplicaManager(); }
-
软引用
:通过SoftReference
软引用类型把ReplicaManager
类的对象直接包裹起来,这时候replicaManager
对ReplicaManager
类的对象就是软引用。正常垃圾回收不会回收软引用对象,只有当垃圾回收之后,发现内存还是不够存放新对象时,内存要溢出时,才会去回收软引用的对象。 -
public class Kafka { private static SoftReference<ReplicaManager> replicaManager = new SoftReference<>(new ReplicaManager()); }
-
弱引用
:通过WeakReference
软引用类型把ReplicaManager
类的对象直接包裹起来,这样静态变量持有的ReplicaManager
类对象就是弱引用的了。弱引用就跟没有一样,垃圾回收直接回收掉。 -
public class Kafka { private static WeakReference<ReplicaManager> replicaManager = new WeakReference<>(new ReplicaManager()); }
-
虚引用
:很少使用
总结:有GC Roots
引用的对象不能回收,没有引用的可以回收,强引用不能回收,对于软引用,如果回收后内存依旧不够放入新的对象,就回收软引用的对象了。
当然,如果我们不想没有GC Roots的对象立即被回收,我们可与使用重写Object的finalize()
方法来拯救一下,这东西很少用,知道一下即可。
有下面这么一段代码,ReplicaFetcher的对象会被回收掉嘛?
public class Kafka {
private static ReplicaManager replicaManager = new ReplicaManager();
}
public class ReplicaManager {
private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}
肯定是不会的,ReplicaFetcher
类的对象被ReplicaManager
类的replicaFetcher
强引用着,而ReplicaManager
类的对象被可作为GC Root的静态变量replicaManager
强引用着,所以ReplicaFetcher
对象可以向上找到GC Root,因此不会被回收。
二、针对新生代的垃圾回收算法——复制算法
背景:对于新生代,一种不太好的垃圾回收思路是直接对新生代里的垃圾对象进行标记,然后直接对垃圾进行回收,这样的缺陷是引入了许多内存碎片,内存碎片导致了内存的浪费,我们没有了完整的连续的内存空间是很难受的一件事。在这改进一下,我们可以将新生代内存空间划分为两部分,对不回收的对象进行标记,转移到另一块区域中顺便整理下,然后把另一块区域里的垃圾干掉,这就是所谓的复制算法。如下图
为什么标记不回收的呢?因为新生代里存放的对象大都是存活时间很短的,所以不回收的只是很少一部分,标记速度更快,如果标记回收的,就不合适了。
但是复制算法有缺点,因为我们永远只有一半内存可以使用,另一半放垃圾,这样转移,使得内存使用效率太低了。所以对复制算法进行了优化。因为新生代放的都是存活时期非常短的对象,极端情况比如99%的垃圾1%有用的。实际上新生代内存区被划分为三部分:1块Eden
区,2块Survivor
区。
初始对象都是优先被分配在Eden
区的,如果Eden
区快满了就触发Minor GC
,把Eden
中存活的对象全部转移到空的Survivor
区,接着清空Eden
中的垃圾,再次配分对象到Eden
。由于存活的对象较少,所以给Survivor区域分配的内存就较少,当然看实际场景来分配。
如果Survivor
一个区域中都放满了,并且Eden
区域中也占满了,但是垃圾回收后可能就只有10M对象活着,只要把那10MB对象转移到另一块Survivor
区域中即可,之后把第一块Survivor
区域和Eden
区域中垃圾全部回收,这样始终保证着有一块Survivor
区域是空的。
三、针对老年代的垃圾回收算法——标记整理算法
1、进入老年代的几种情况
(1)躲过15次GC(当然我们可以自己设置次数)
(2)大对象直接进入老年代
通过-XX:PretenureSizeThreshold
把值设置成字节数,创建的对象大于这个值就直接进入老年代,之所以这么做是避免新生代里的大对象屡次躲过GC还要在三个区域来回复制,耗费时间。
(3)动态对象年龄判断
比如当前放对象的Survivor区域里,一批对象的总大小,大于这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象就直接进入老年代
- 假设100MB的Survivor中有俩对象,年龄都是2岁,但是俩对象加起来超过了50MB,也就是超过了一半了,这个时候,Survivor中大于等于2岁的对象都要进入老年代里去。
- 避免动态年龄判断的方式:如果新生代内存有限,可以调整
-XX:SurvivorRatio=8
这个参数,默认是说Eden区比例为80%,也可以降低Eden区的比例,给两块Survivor区更多的内存空间,然后让每次Minor GC后的对象进入Survivor区中,还可以避免动态年龄判定规则直接把他们升入老年代。
(4)空间分配担保规则
Minor GC后发现对象太多,放不进Survivor区,就必须直接转移到老年代区的情况。在执行任何一次Micro GC
前,JVM都会检查老年代的可用空间是否大于新生代所有对象的总大小,记住,是新生代所有对象总大小,因为极端情况下新生代可能所有对象都活下来了。下面就有两种情况了。
- 第一种情况:老年代剩余内存大小大于新生代所有对象总大小,放心
Minor GC
吧!即使你的Survivor区域放不下也可以放老年代去; - 第二种情况:老年代剩余内存大小小于新生代所有对象总大小,此时就会看
-XX:HandlePromotionFailure
参数是否设置了,若设置了,则会看老年代可用内存大小是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果不是,只能Full GC
,即对老年代里的对象进行回收,才能让剩余存活对象进入老年代。如果是,则可以冒险尝试下Minor GC
,但这个尝试也是有三种可能:
可能一:Minor GC
后,剩余存活对象比Survivor
区还小,就直接放进Survivor
中
可能二:Minor GC
后,剩余存活对象大于Survivor
区,小于老年代可用区,即直接进入老年代
可能三:Minor GC
后,剩余存活对象大于老年代可用区,放不下了,这时候就会触发Full GC
,要是Full GC
后还是放不下,直接导致OOM内存溢出
。
简介:空间担保机制是看老年代可用空间是否大于新生代所有对象总大小的,如果成立了,那么Minor GC就是安全的,如果不成立,则看HandlerPromotionFailure
是否设置为true,true则允许担保失败,如果允许,则检查老年代可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试Minor GC,如果小于,则Full GC。
实际上-XX:HandlePromotionFailure
参数在JDK 1.6以后就被废弃了,所以现在一般都不会在生产环境里设置这个参数了。JDK 1.6之前是把空间担保机制和HandlerPromotionFailure
参数拆开了,JDK 1.6之后的空间担保机制只要满足"老年代可用连续空间 > 新生代对象总大小或历次晋升到老年代对象的平均大小"其中一个就可,不满足就Full GC。
2、老年代Full GC算法——标记整理算法
顾名思义,标记整理就是把老年代里活着的对象整理,紧凑在一起,避免垃圾回收后出现内存碎片。注意Full GC
的速度比Minor GC
慢10多倍,如果频繁出现Full GC
就影响了系统性能,出现卡顿,但是你多两次Minor GC
无关紧要,反正速度很快。
所以,所谓JVM优化,就是尽量能让对象在新生代里进行分配和回收,别让太多对象进入老年代,避免对老年代的频繁Full GC
,同时要给系统足够的内存大小来避免新生代频繁的Minor GC
。
未回收前
回收后
为什么老年代不用复制算法?
因为老年代存活的对象太多了,如果用复制算法,每次挪动90%的对象很不方便,所以采用标记回收,把有用的挪到一边,然后回收垃圾,是很好的一个方式。
3、针对第三节面试会问的问题
(1)、什么时候会尝试触发Minor GC?
答:当新生代的Eden区和其中一个Survivor区空间不足时。
(2)、什么时候会尝试触发Full GC?
答:
第一是老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
第二是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
第三是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足,就要Full GC;
第四是是“-XX:CMSInitiatingOccupancyFaction”参数的设置,当老年代中的对象到达这个比例时就会GC;
(老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC)
(3)、触发Minor GC之前会如何检查老年代大小,涉及哪几个步骤和条件?
答:
1、先判断新生代中所有对象的大小是否 小于 老年代的可用区域 true 则 触发Minor GC,false则继续进行下面2中的判断
2、如果设置了-XX:HandlePromotionFailure这个参数,那么进入第3步 如果没有设置-XX:HandlePromotionFailure参数,那么触发Full GC
(4)、什么时候在Minor GC之前就会提前触发一次Full GC?
答:当判断 新生代历次进入老年代对象的平均大小 大于 老年代的可用区域就会触发一次Full GC,让老年代腾出一些空间,腾出空间后再进行Minor GC。
(5)、Minor GC过后可能对应哪几种情况?
答:
情况1:Minor GC前先判断:存活的对象所占的内存空间 < Survivor区域内存空间的大小,那么存活的对象进入Survivor区。
情况2:Minor GC前先判断:Survivor区域内存空间的大小 < 存活的对象所占的内存空间 < 老年代的可用空间大小。那么存活的对象,直接进入老年代。
情况3:Minor GC前先判断: (存活的对象所占的内存空间 > Survivor区域内存空间的大小) && (存活的对象所占的内存空间 > 老年代的可用空间大小)。那么会触发Full GC,老年代腾出空间后,再进行Minor GC。如果腾出空间后还不能存放存活的对象,那么会导致OOM即堆内存空间不足、堆内存溢出。
四、垃圾回收容器简介
1、抛出问题:垃圾回收的同时能创建对象吗?
不能!垃圾回收的时候,尽可能要让垃圾收集器专心工作,此时JVM在后台直接进入Stop the World
状态,停止我们写的Java系统的所有线程,让我们代码不再运行。一旦回收完毕,就可以恢复线程运行了。这里要注意的就是避免频繁GC,无论是新生代还是老年代,我们都不希望系统隔一段时间卡死一下,这是JVM最需要优化的地方。当然我们可以使用适当的垃圾回收容器来缩短回收的时间。
2、垃圾回收容器
不同的内存区域使用不同的垃圾回收容器,简单介绍下垃圾回收容器
-
Serial和Serial Old
:分别用来回收新生代和老年代对象。工作原理就是单线程运行,回收的时候会停止系统中其他工作线程,现在几乎不用。 -
ParNew和CMS
:ParNew
是用在新生代的回收容器,CMS
是用在老年代的回收容器,他们都是多线程并发机制,性能较好,一般是线上系统标配组合。 -
G1
:统一收集新生代和老年代,采用了更优秀的算法和设计机制
多线程回收
五、捋清概念
有很多GC名词需要捋一下:Minor GC、Young GC、Full GC、Old GC、Major GC、Mixed GC。
1、Minor GC 与Young GC
年轻代 = 新生代,新生代的回收就叫Minor GC或是Young GC
2、Full GC与Old GC
Full GC就是整体的意思,指的是针对新生代、老年代、永久代的全体内存空间的垃圾回收,所以称之为Full GC,但是说实话,平时习惯就是把Full GC等价为Old GC,也就是仅仅针对老年代的垃圾回收。
3、Major GC
有些人把Major GC跟Old GC等价起来,认为他就是针对老年代的GC,也有人把Major GC和Full GC等价起来,认为他是针对JVM全体内存区域的GC。这个概念少提,如果提了,就要问清楚你到底针对整体还是针对老年代。
4、Mixed GC
G1中的一个垃圾回收机制,一旦老年代占据堆内存的45%了,就要触发Mixed GC,此时对年轻代和老年代都会进行回收。