前言
看到上图的问题,相信在面试的过程中或多或少的被问过类似的题,为了能让我们更好的熟悉JVM底层,那么接下来我们将开始了解JVM虚拟机中的垃圾回收(Garbage Collector,GC)部分。
接下来将讲述以下几个部分的内容:
- 对象什么时候被认定为是垃圾;
- 如何定位寻找垃圾;
- GC算法;
- 常见的垃圾收集器;
- 生产环境下的调优。
1. 什么对象会被认定为是垃圾
如下两幅图所示,当我们new一个对象obj1出来的时候,假设这个对象的成员变量指向了我们新new出来的另一个对象obj2,如图2所示。在运行过程中,当指向obj2的引用没有了,此时的obj2如果没有其它引用了,那它将会被认定为垃圾,在GC的过程中会被回收掉这一部分内存。
2.如何找到垃圾
在知道什么是垃圾对象之后,那么我们该如何找到这个垃圾并清理掉呢?主要的方法有如下两种。
2.1 引用计数算法(Reference Count)
引用计数法相对于比较简单,我们可以看如下3幅图所示,图4中,有三个引用指向一个对象(蓝色框为对象),此时引用数为3。当有一个引用消失后,引用数变为2,如图5所示。当引用数变为0时,如图6所示,该对象则被垃圾回收器判定为垃圾对象。
优点:处理较为简单方便。
缺点:无法解决循环引用的问题。如图7所示循环引用,此时三个对象的引用计数都为1,它们始终不会被判定为垃圾。为此衍生出了另一种算法。
2.2 根可达算法(Root Searching)
通过一系列的称为GC roots 的对象作为起始点,从这些起始点往下搜索,搜索走过的路径称为引用链,当一个对象和GC Roots没有任何引用链(即GC Roots到这个对象是不可达的),说明该对象是垃圾。
在Java中可作为GC Roots的对象有下面几种:
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
; -
本地方法栈中引用的对象
; -
运行时常量池中的引用对象
; -
方法区中类静态属性引用的对象
; -
方法区中常量引用的对象
。
3.垃圾回收算法
常见的垃圾收集算法有以下三种
- Mark-Sweep(标记清除);
- Copying(拷贝);
- Mark-Compact(标记整理或标记压缩)。
3.1 Mark-Sweep(标记清除)算法
首先对可回收的垃圾进行标记,然后再进行清除。
缺点:
- 空间碎片化:对象清除后会产生大量不连续的空间碎片,当需要分配给大对象较大的内存空间时会因为找不到足够的连续空间而不得不提前出发下一次垃圾收集;
- 时间效率:相对于拷贝算法效率低。
3.2 Copying(拷贝)算法
为了解决效率低下以及碎片化问题,拷贝算法出现了。该算法将分配到的内存一分为二,它将内存空间分为大小相等的两块区域,每次只使用其中一块,当进行垃圾收集时,将这块区域中还存活的对象复制到另一块,然后将这一块内存回收。这样就不会产生内存碎片的问题。
缺点:需要牺牲一半的内存空间。
3.3 Mark-Compact(标记整理)算法
在标记清除算法的基础上加入了整理这一步,即在标记清除后,会进行整理。
缺点:相对于复制算法,同样是效率偏低。
注:在新生代中,对象存活率低,适合使用复制算法,而老年代对象存活率较高,适合使用标记清除算法或标记-整理算法。
4.常见的垃圾收集器
由于随着内存越来越大,从而演变出不同的垃圾收集器。那如何知道自己项目当前使用的垃圾回收器是什么呢?可以使用以下指令查看:java -XX:+Print CommandLineFlags -version
4.1堆内存逻辑分区(适用于分代垃圾收集器)
JDK1.8默认的组合为Parallel Scavenge
和Parallel Old
。内存分配中新生代与老年代比例为1:2,即新生代内存占总堆内存的1/3,老年代占堆内存的2/3。同时,新生代部分又分为了eden区和两个survivor区,其比例经过统计分析得到,为8:1:1。这个分配比例可以由我们设置,默认情况下是上述比例。
新生代大量的死去,少量存活,这个时候采用复制算法;而老年代存活率高,回收较少,因此采用MC或MS。
如下图所示,s1~s2之间的复制年龄超过限制时,进入Old区。可以通过参数: -XX:MaxTenuingThreshold
配置。
注:
- MinorGC/YGC:年轻代空间耗尽时触发;
- MajorGC/FullGC:在老年代无法继续分配空间时触发,新生代老年代同时进行回收。
如下图所示,
- 当我们new一个对象的时候,先看栈是否分配得下(java中的小对象是可以分配在栈上的,好处:我们知道栈中每个方法都有对应的栈帧,这方法中用到的对象将分配到栈中,当方法结束时,弹出栈帧时,内存空间也被回收掉了,而不需要GC收集器的介入),若分配得下就将对象分配到栈内存空间中,此时若弹出栈帧,这个对象的生命周期则终止了;
- 若栈中分配不下,即栈无法分配足够的存储空间存放该对象,此时,JVM会看该对象是否足够大(可通过JVM参数进行设置),如果对象大于设定的值,则分配进老年代中,当进行FGC时,此对象才有可能被回收,结束整个生命周期;
- 若该对象不能分配到栈上,也不大于设定为大对象的值,则分配到
线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)
中,无论在TLAB是否分配得下,最终对象分配的内存空间都是Eden区中的内存空间(因为TLAB所使用的内存空间也是Eden区的内存空间)若要详细了解TLAB,可查阅此文章浅析java中的TLAB - 分配到Eden区后,经过GC回收后,若对象被清除,则该对象整个生命周期结束;若未能清除,则进入survivor区,再经过多次GC后,达到了进入老年代的年龄限制,若还存活,则进入老年代区。
4.2 Serial垃圾收集器
Serial是比较早期使用的垃圾收集器,工作于年轻代中,它是一个具有STW(stop the world)性质
,使用Copying(复制)算法
的单线程
垃圾收集器。
注:所有java的垃圾回收器都存在STW;
优点:单CPU时效率最高;
适用场景:虚拟机是Client模式的默认垃圾回收器。
4.3 Serial Old垃圾收集器
与Serial类似,只不过它工作于老年代,是一个具有STW(stop the world)性质
,使用Mark-Sweep
标记清除或Mark-Compact
标记整理算法的单线程
垃圾收集器。
以上两个垃圾收集器往往组合使用,但在随着内存逐渐增大后,由于使用的是单线程进行GC,会导致STW的时间过于长,给用户的表现就是卡顿,卡死的状态。为此,诞生了并行的垃圾收集器
4.4 Parallel Scavenge垃圾收集器
它是一个具有STW(stop the world)性质
,使用Copying(复制)算法
的多线程
垃圾收集器。工作于年轻代。
4.5Parallel Old垃圾收集器
它是一个具有STW(stop the world)性质
,使用Mark Compact算法
的多线程
垃圾收集器。工作于老年代。
以上两个垃圾收集器往往组合使用,是JDK1.8的默认组合。但并不是线程越多就能使STW的时间缩短,因为受限于CPU核数,若多于CPU核数,则会很多时间浪费在线程上下文切换上。
4.6 ParNew垃圾收集器
它是一个具有STW(stop the world)性质
,使用Copying算法
的多线程
垃圾收集器。工作于老年代。由于Parallel Scavenge与CMS配合不好,因此诞生了专门用于配合CMS垃圾收集器的ParNew垃圾收集器;
4.7 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一个并发
,以有效降低STW的时间为目标,使用 Mark Sweep
标记清除算法的垃圾收集器。GC线程与工作线程同时运行。
我们可以从图中看到,总共分为四步:
-
初始标记(CMS initial mark)
:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。 -
并发标记(CMS concurrent mark)
:进行GC Roots Tracing的过程,它在整个回收过程中耗时最长,不需要停顿。 -
重新标记(CMS remark)
:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 -
并发清除(CMS concurrent sweep)
:不需要停顿。
注:并发标记过程中会存在错标问题
(本来是垃圾,但是标记完后,系统又产生了对它的引用)与漏标问题
(本来不是垃圾,但在标记完后,不再有该对象的引用,即已经是垃圾,称之为浮动垃圾
)。漏标问题可以在下一次GC就可以解决。而错标问题,则是CMS-G1-ZGC等最新的垃圾收集器所关注并解决的问题,而这类垃圾收集器的区别就在于怎么解决错标问题。CMS和G1采用的都是三色标记法
,而ZGC采用的是颜色指针
。
CMS对于错标问题的做法是,第一次STW进行初始标记,在并发标记中产生的错标问题,则使用第二次STW对错标进行修正,即第3步中的重新标记。
4.8 三色标记算法
三色标记算法中的三色是逻辑上的概念。如下图所示,
- A自己已经访问过了,同时它的成员变量也已经都访问过了(即标记完成),则此时A为黑色;
- B自己已经标记过了,但是成员变量尚未标记,此时B为灰色;
- 而D是没有标记过得节点,此时为白色。
4.9 七种垃圾收集器的比较
本篇后续会补充完三色标记算法以及G1垃圾收集器的一些细节。下一篇Java虚拟机(三)中将主要介绍JVM调优部分的内容。