JVM学习笔记之垃圾回收
概述
- 什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
Java堆是垃圾收集器的工作重点(GC关注堆和方法区)。
垃圾回收相关算法
垃圾回收算法可以分为标记阶段的算法,清除阶段的算法。
标记阶段算法
标记阶段要做的就是区分内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记已经死亡的对象,在清除阶段才需要回收。
引用计数算法
- 为每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
- 如对于对象A,只要有对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性。
缺点:
- 需要单独的字段存储计数器,增加了空间开销
- 每次赋值都要更新计数器,增加时间开销
- 无法处理循环引用的情况。导致Java垃圾回收器没有使用这类算法
可达性分析算法(根搜索算法、追踪性垃圾收集)
有效解决循环引用问题,防止内存泄漏的发生,Java选择这种算法。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots,一组必须活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(reference chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。
在Java语言中,GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象。如各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象。如Java类的引用类型静态变量
- 方法区中常量引用的对象。如字符串常量池里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。如基本数据类型对于的class对象,一些常驻的异常对象,系统类加载器。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
清除阶段算法
标记后就要释放掉内存。
标记-清除(Mark-Sweep)算法
- 标记。垃圾收集器从引用根节点开始遍历,标记所有被引用的对象
- 清除。垃圾收集器对堆内存从头到尾线性遍历,如果发现某个对象没有被标记,则回收(其实是把对象地址保存在空闲地址列表中,如果有新对象需要加载时,直接替代)。
缺点:
- 效率不高。
- GC需要停止整个应用程序
- 内存不连续,会有碎片。需要维护一个空闲列表。
复制(Copying)算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记和清除过程,运行高效
- 没有内存碎片
缺点:
- 需要两倍的内存空间
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
标记-压缩(标记-整理、Mark-Compact)算法
先标记,再移动
优点:
- 和标记-清楚算法比较,没有内存区域分散的缺点
- 和赋值算法比较,没有内存减半的高额代价
缺点:
- 效率不如复制算法
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要STW
分代收集(Generational Collecting)算法
上述的垃圾回收算法没有最优的,都具有各自的优势和特点。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
年轻代生命周期短、存活率低,回收频繁,适合复制算法。
老年代区域大、存活率高,适合标记-清除或者标记-整理混合实现。
增量收集(Incremental Collecting)算法
为了解决上述算法存在的STW问题,增量收集的思想是每次只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点:
由于会频繁切换线程,造成系统吞吐量下降。
分区算法
将整个堆空间划分成连续的不同小区间。每个小区间都独立使用,独立回收。
对象的finalization机制
-
Java语言提供了对象终止(finalize)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的
finalize()
方法 - finalize方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
不要主动调用,交给垃圾回收器。由于finalize的存在,对象可能有三种状态:
- 可触及的。从根节点开始,可以到达这个对象
- 可复活的。对象的所有引用都被释放,但是对象有可能在finalize中复活
- 不可触及的。对象finalize被调用了,并且没有复活。(finalize只能被调用一次)
垃圾回收相关概念
System.gc()
通过System.gc()或者Runtime.getRuntime().gc()
的调用,会显示触发Full GC,同时对老年代和新生代回收。但不是一定调用的。
内存溢出与内存泄漏
内存溢出:没有空闲内存,并且垃圾收集器也无法提供更多内存。原因有二:Java虚拟机堆内存设置不够;代码中创建了大量大对象,并且长时间不能被垃圾收集器手机。
内存泄漏:对象不再被程序用到了,但是GC又不能回收他们的情况。举例:1、单例模式。单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏。2、一些提供close的资源未关闭导致内存泄漏。数据库连接,网络连接,IO连接等等。
STW
GC发生过程,需要停顿整个应用程序线程。
并发(Concurrent)、并行(Parallel)
并发:单核CPU不停切换,看起来并行。
并行:多核CPU。
安全点、安全区域
安全点:只有在特殊位置才能停下来GC。主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果标志为真,则将自己进行中断挂起。
安全区域:一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
引用
强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
强引用
传统的引用方式,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。(不回收。实际开发中99%的情况都是强引用)
软引用
用来描述一些还有用,但非必须的对象。
内存足够时,不会回收软引用的可达对象。当内存不够时,会回收软引用的可达对象。
在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够内存,才会抛出内存溢出异常。通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用。
弱引用
发现即回收。只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。可用来缓存。
被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用在创建时必须提供一个引用队列作为参数。
垃圾回收器
评估GC性能指标。吞吐量:运行用户代码的时间占总运行时间的比例。暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。内存占用:Java堆区所占的内存大小。
在最大吞吐量优先的情况下,降低停顿时间。
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
垃圾收集器组合关系
红线:JDK8之后弃用
绿线:JDK14之后弃用
CMS:JDK14之后删除
查看默认使用GC
-XX:+PrintCommandLineFlags
Serial
串行回收。采用复制算法、串行回收和STW执行内存回收
优势:简单高效(单线程的情况下),适合运行在client模式下的虚拟机
Serial Old
串行回收。采用串行回收、STW和标记-压缩算法回收老年代。
Parallel Scavenge
吞吐量优先。采用复制算法、并行回收、STW。与ParNew的区别是达到一个可控制的吞吐量。适合在后台运算而不需要太多交互的任务。例如,订单处理、工资支付、科学计算等。JDK8的默认垃圾收集器。
参数:
-XX:MaxGCPauseMillis
垃圾收集器最大停顿时间(毫秒)
-XX:GCTimeRatio
垃圾收集时间占总时间比例。1/(N+1)
-XX:+UseAdaptiveSizePolicy
设置自适应调节策略。虚拟机自动调整。(默认开启)
Parallel Old
采用了标记-压缩算法,也是基于并行回收、STW。JDK8的默认垃圾收集器
ParNew
并行回收。与Serial的区别就是多线程。多CPU、多核下效率比Serial高。
CMS
低延迟。用户线程和垃圾收集线程同时工作。标记-清除算法、STW。目的是尽可能缩短垃圾收集时用户线程的停顿时间。
工作原理:可分为四个阶段。初始标记阶段、并发标记阶段、重新标记阶段、并发清除阶段。
- 初始标记阶段。程序中的所有工作线程都会因为STW而暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以速度非常快。
- 并发标记阶段。从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程。
- 重新标记阶段。为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但也远比并发标记阶段时间短。
- 并发清除阶段。此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此当堆内存使用率达到某一阈值时,便开始进行回收。如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”,这时临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
优点:
- 并发收集
- 低延迟
缺点:
- 会产生内存碎片。由于采用的是标记-清除算法,所以会产生内存碎片。
- 对CPU资源非常敏感。在并发阶段,不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢。
- 无法处理浮动垃圾。在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收。
参数:
-XX:+UserConcMarkSweepGC
使用CMS,如果指定了老年代为CMS,则新生代为ParNew-XX:CMSlnitiatingOccupanyFraction
设置堆内存阈值-XX:+UseCMSCompactAtFullCollection
指定在执行完Full GC后对内存空间进行压缩整理,一次避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题是停顿时间变长了。-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理-XX:ParallelCMSThreads
设置CMS线程数量
G1
区域化分代式。在延迟可控的情况下获得尽可能高的吞吐量。
将堆内存分割为很多不相关的区域(region)。每次根据允许的收集时间,优先回收价值最大的region。由于这种方式的侧重点在于回收垃圾最大量的region,所以起名为垃圾优先(Garbage First)。是JDK9之后的默认垃圾收集器。适合多核CPU以及大容量内存环境。
G1回收器的优点:
- 并行与并发。并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,不会在整个回收阶段发生完全阻塞应用程序的情况。
- 分代收集。G1依然属于分代型垃圾收集器。将堆空间分为若干region,这些region中包含了逻辑上的年轻代和老年代。同时兼顾年轻代和老年代。
- 空间整合。Region之间是复制算法,整体上可看作标记-压缩算法。避免内存碎片。
- 可预测的停顿时间模型。每次根据运行的收集时间,优先回收价值最大的region,使得G1收集器在有限的时间内可以获取尽可能高的收集效率。
参数:
-XX:+UseG1GC
开启G1
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间
-XX:G1HeapRegionSize
设置每个region的大小,需要是2的幂,范围在1-32MB之间
回收过程:
年轻代GC、老年代并发标记过程、混合回收。
当年轻代伊甸园区用尽时开始年轻代回收。G1的年轻代收集阶段是一个并行的独占式收集器。从年轻代区间移动存活对象到Survivor区或老年代区。当堆内存使用达到一定值时,开始老年代并发标记过程。G1的老年代回收器不需要整个老年代被回收,只需要扫描/回收一小部分老年代的region就可以了。
Remembered Set。一个对象被不同区域引用的问题,一个region被其他region中对象引用,判断对象存活时,如果要扫描整个Java堆,效率不高。解决方法,使用记忆集来避免全局扫描,每个region都有一个对应的记忆集。每次reference类型数据写操作时,都会产生一个写屏障(write barrier)暂时中断操作。然后检查将要写入的引用指向的对象是否和该reference类型数据在不同的region。如果不同,通过cardTable把相关引用信息记录到引用对象所在region对应的记忆集中。当进行垃圾收集时,在GC根节点的枚举范围加入记忆集,就可以保证不进行全局扫描。
面试题
- 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下CMS和G1
- 如何判断对象是否死亡?
引用计数法;可达性分析算法
- 介绍下强引用、软引用、弱引用、虚引用
- 如何判断一个类是无用的类?
需要满足三个条件。该类的所有实例都已经被回收。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
HotSpot为什么要分为新生代和老年代?
因为对不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
-
JVM GC算法有哪些,目前的JDK版本采用什么回收算法
目前GC算法有引用计数算法、可达性分析算法、标记-清除算法、复制算法、标记-压缩算法。目前大部分公司在使用的JDK1.8默认使用Parallel和Parallel Old垃圾回收器,相应的回收算法为复制算法和标记-压缩算法
-
G1回收期讲下回收过程
-
GC是什么?为什么要有GC
-
GC的两种判定方法?CMS收集器与G1收集器的特点。
-
说一下GC算法,说一下分代回收
-
垃圾收集策略和算法
-
JVM GC原理,JVM怎么回收内存?
-
CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?
-
Java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的?
-
垃圾回收算法的实现原理?
-
如何选择合适的垃圾收集算法?
-
System.gc()和runtime.gc()会做什么事情?
-
Java GC机制?GC Roots有哪些?
-
CMS回收停顿了几次?为什么要停顿两次?