文章目录
1. 垃圾回收
1.1 如何判断对象可以回收
- 引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器加一;当引用失效时,计数器减一;
任何时刻计数器为零的对象就是不可能再被使用的
。
缺陷:对象之间相互循环引用问题
两个对象之间存在相互应用,除此之外,再无其它引用。实际上,两个对象已经不可能再被其他地方访问,但是因为互相引用的对方,引用计数值不为零,无法回收它们。
- 可达性分析算法
- Java虚拟机中的垃圾回收期采用
可达性分析
探索所有存活的对象- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的应用链找到该对象,未找到,表示可以回收
GC Root对象包括:
虚拟机栈中引用的对象
,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量等- 在
方法区中类静态属性
引用的对象,譬如Java类的引用类型静态变量- 在
方法区中常量
引用的对象,譬如字符串常量池(StringTable)中的引用- 在
本地方法栈
中JNI(Native方法)引用的对象虚拟机内部的引用
,如基本数据类型对应的Class对象,系统类加载器等所有被同步锁(synchronized关键字)持有的对象
引用类型补充:
- 一般来说,一个对象只有"被引用"或者"未被引用"两种状态,但希望能描述一种对象:当内存空间足够时,能保留在内存中,如果内存空间在进行过一次垃圾回收之后仍然非常紧张,那么就可以被抛弃
- Java虚拟机中对引用的概念做了进一步扩充,将引用分为强引用、软引用、弱引用、虚引用
强引用:
最传统的"引用"定义,只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收软引用:
仅有软引用引用该对象是,在垃圾回收后,内存仍不足时会再次发起垃圾回收(第一次不会被回收,内存不足第二次会回收),回收软引用对象。可以配合引用队列来释放软引用自身。弱引用:
仅有弱引用引用该对象时,在垃圾回收过程中,无论内存是否充足,都会回收弱引用对象。可以配合引用队列释放弱引用自身。虚引用:
一个对象是或否有虚引用的存在,完全不会对器生存时间构成影响,也无法通过虚引用来取得该对象的实例。为对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时,收到一个系统通知(如释放直接内存的场景)。
必须配合引用队列使用,被引用对象回收时,将虚引用入队m,由Reference Handler线程调用虚引用相关方法释放直接内存。
1.2 垃圾收集算法
分代回收理论
分代假说:
- 弱分代假说:绝大数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
基于两个分代假说,可以将Java堆划分成新生代和老年代两个区域,新生代中每次垃圾回收都或有大批对象死去,回收后存活的少量对象会逐步晋升到老年代中(老年代会以较低的频率进行收集)。
简单划分区域会出现“跨代引用”问题:新生代中的对象完全可能被老年代中的对象引用,此时为了找出新生代中的存活对象,不得不在固定的GC Roots对象之外,再额外遍历整个老年代中的对象以确保可达性结果的正确性。
- 这时提出了第三条经验法则,
跨代引用假说:跨代引用相对于同代引用来说仅占极少数
依据这条假说,只需在新生代上建立一个全局的数据结构(记忆集),这个结构
将老年代分成若干小块,标识出老年代中的哪一块内存会存在跨代引用,此后发生Minor GC时,无需扫描整个老年代,只需扫描包含跨代引用的小块内存。
- 标记清除算法
标记存活的对象,统一回收所有未标记的对象,也可以反过来标记。
缺点:内存空间碎片化问题
,标记、清除之后会产生大量不连续的内存碎片,碎片太多可能导致分配较大对象时无法找到足够的内存连续内存而不得不进行第二次垃圾收集动作。
- 标记-复制算法
- 将可用内存划分成大小相等的两块,每次只使用其中一块。当这一块的内存用完时,就将还存活着的对象复制到另外一块中,然后再将已经使用过的内存一次清理掉。
- 特点:空间浪费;内存复制时间开销;不会产生内存锁片;
- 标记-整理算法
- 标记过程与"标记-清除算法"相同,但后续步骤不是直接对可回收对象进行清理,而是清理后让存活的对象向内存一端移动。
- 移动存活对象并更新引用地址是一种即为负重的操作,而且对象移动操作必须暂停用户引用程序才能进行(
Stop The World
)。- 不移动存活对象,内存碎片问题只能依赖
分区空闲分配链表
解决内存分配问题,但内存分配和访问频率相较于垃圾收集频率高得多,这部分耗时增加,影响引用程序的吞吐量
垃圾分代回收
针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,
设计新生代的内存布局
。将新生代分为一块较大的Eden空间(伊甸园
)和两块较小的Survivor空间(幸存者区
)。
- 对象首先分配在伊甸园区域
新生代空间不足时,触发Minor GC,伊甸园和From区存活的对象使用copy复制到To区,存活的对象年龄加一,然后直接清理掉伊甸园和From区空间
。- Minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发Minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长
2. 垃圾回收器
垃圾回收器指标
吞吐量:
用户线程运行时间 / (用户线程时间 + 垃圾回收执行时间),吞吐量越高,则能够高效率利用处理器资源,尽快完成程序的运算服务。停顿时间:
表示垃圾收集过程中,导致用户线程停顿的时间,如果停顿时间越长,那么用户线程卡顿的时间越长,用户体验越差。适合与用户交互要求较高的场景通常情况下,
吞吐量和低延时(停顿时间)这两个指标是对立的
,无法同时兼顾两者,低延时是以牺牲吞吐量和新生代空间位代价换取的:新生代空间减小,虽然停顿时间缩短,但意味着垃圾收集频率会提高,这样就会导致吞吐量下降。
2.1 Serial收集器(串行)
Serial/Serial Old收集器是单线程工作的收集器,并且
在垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW现象)
- Serial是针对新生代的收集器,采取
复制算法
- Serial Old收集器是针对老年代区域的收集器,采用的是
标记-整理算法
- HotSpot虚拟机
客户端
模式下默认采取的收集器,是所收集器额外内存消耗最小的
2.2 Parallel Scavenge / Parallel Old收集器(吞吐量优先
)
- Parallel Scavenge收集器是
针对新生代的并行垃圾收集器,采用复制算法,也会同样造成STW
。与其他收集器不同的是,Parallel Scavenge收集器目标是达到一个可控制的吞吐量
,也被称作"吞吐量优先收集器"- Paramllel Old收集器是
针对老年代,也支持多线程的并行执行,基于标记-整理算法实现
- 参数配置
- XX:GCTimeRadio:
表示用户期望的虚拟机消耗在GC上的时间不超过程序运行时间的1 / (1 + N)
。例如:如果 GCTimeRatio 参数的值配置的 19,那么 GC 运行的时间占总时间的 5%,JVM 通过这个参数来达到控制系统吞吐量的目的。- XX:MaxGCPauseMills:
收集器将尽力保证内存花费的时间不超过用户设定值
。通常情况下,我们无法精准地把控每次垃圾回收需要停顿的时间,所以该参数需要慎用,一不小心,配置的不合理,可能适得其反- XX:UseAdpativeSizePolicy:它表示的是让 JVM根据系统的运行情况来
动态调整新生代老年代的大小
,我们只需要设置好最基本的内存参数以及 MaxGCPauseMillis或者 GCTimeRatio(不能同时设置
),不需要设置-XX:Xmn(新生代的内存大小)、-XX:SurvivorRatio(Surivivior区域的比例)等参数了,JVM 会根据系统运行时监控到相关信息,来动态进行调整
2.3 CMS收集器(响应时间优先)
CMS收集器是
一种以获取最短回收停顿时间为目标,针对老年代,基于标记-清除算法的收集器
,CMS 的工作原理大致分为四个步骤:初始标记、并发标记、重新标记、并发清除
- 初始标记:指的是仅仅只标记出和 GC Roots 直接关联的对象,这个过程需要暂停所有的用户线程,因此会产生 STW。由于这一步仅仅标记和 GC Roots 直接关联的对象,因此这一步耗费的时间会很短,造成的停顿时间会很短。
- 并发标记:从和GC Roots直接关联的对象出发,开始遍历对象引用链,这个过程是
GC线程和用户线程并发执行的,因此不会造成STW
。这一步因为需要遍历所有对象的引用链,耗费时间较长,但由于不会造成STW,即使耗时较长,也不会有太大影响。- 重新标记:
为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分的标记记录
。这一过程需要暂停所有的用户线程,因此会造成STW现象。这一步的耗时会比初始标记截断长一些,但是远小于并发标记阶段。- 并发清除:清理删除标记阶段判断已经死亡的对象,
由于不要移动存活对象,所以这个阶段可以与用户线程并发执行。
整体上看,CMS只有初始标记和重新标记阶段会造成STW现象,但这两步耗时较短。耗时较长的并发标记和清除阶段,由于是与用户线程并发执行,因此CMS收集器是低延时的。
CMS优缺点
CMS收集器的优点就是低延时,适合需要与用户交互或需要保证服务响应质量的程序,但其还存在一下缺点:
- 对处理器资源非常敏感:面向并发设计的程序都对处理器资源比较敏感。虽然在并发阶段,CMS收集器不会导致用户程序暂停,但会因为
占用了一部分线程导致应用程序变慢,降低总吞吐量
。可以通过参数-XX:ConcGCThreads=threads
控制GC线程的数量,系统默认数值是(处理器核心数量 + 3) / 4- 产生浮动垃圾:CMS进行回收时,用户线程同时在并发执行,在此过程中,用户线程可能也会产生新的垃圾,而这些新的垃圾出现在标记过程之后,CMS无法在当次收集中处理它们,这些垃圾称为
浮动垃圾
。老年代会预留一部分内存以存储这些浮动垃圾,因此CMS收集器不会等到老年代内存完全用完时才进行垃圾回收,而是当老年代内存使用率达到设定阈值之后,机会触发CMS执行垃圾收集。可以通过-XX:CMSInitiatingOccupancyFraction=percent
设置具体阈值
此外,当预留存的内存空间无法存储产生的浮动垃圾(无法满足分配新对象)时,会出现一次并发失败(Concurrent Mode Failure)
。 JVM会采用后备方案:冻结用户线程,采用Serial Old收集器进行老年代区域的垃圾收集。在一定程度上,会降低系统的性能。- 垃圾碎片:CMS 使用的是
标记-清除算法
,所以会产生内存碎片,内存碎片可能会影响程序性能。如果要为大对象分配内存时,发现内存中没有一块能容得下该对象的内存,那这个时候 JVM 就不得不触发一次Full GC,导致程序发生STW。
2.4 Garbage First收集器
Regin分区:虽然G1仍然保留新生代和老年代的概念,但
G1不再坚持固定大小以及固定数量的分代区域划分,而是将连续的Java堆划分为多个大小相等的独立区域(Region)
,每个Region区都能够根据需要,扮演Eden空间、Survior空间或者老年代空间。
此外,G1新增了H区的概念:如果对象的大小超过了Region区容量的50%,那么就会采用多个连续的Region进行存储。每个Region区的大小都可以通过参数-XX: G1HeapRegionSize
设置。
停顿时间
- G1收集器的另外一个特点是可以设置一个
期望的停顿时间
,然后在期望的停顿时间内,对一部分Region进行垃圾回收。- 实际上,在系统运行过程中,G1 会收集每个 Region 的回收耗时、垃圾占比等各个可测量的信息,然后计算回收每个 Region 带来的收益大小(可回收的内存+回收耗时),通过维护一个优先级列表,然后在设置的最大停顿时间内,回收那些能带来最大收益的 Region。
可以通过参数
-XX: MaxGCPauseMillis
设置默认的期望停顿时间,不能随意调低期望停顿时间。如果设置太小,那么每次收集时只能回收极少的Region区,随着时间推移,回收速度更不上新对象产生的速度,内存空闲越来越少,最终触发Full GC,从而导致系统停顿时间更长。
运行阶段
初始标记:
仅标记GC Roots能直接关联到的对象,这个阶段需要用户线程停顿(STW)
,但耗时很短,而且是借助Minor GC运行时同步完成,所以G1收集器在这个阶段实际并没有额外的停顿。并发标记:
从GC Root开始对堆中对象进行可达性分析,递归地扫描整个对象图。这阶段耗时较长,但可与用户程序并发执行。当对象扫描完成之后,还要重新处理STAB记录下的在并发时有引用变动的对象。最终标记:
对用户线程做另外一个短暂的暂停(STW),用于处理并发标记阶段结束后仍遗留下来的最后少量的STAB记录(并发标记阶段,对象引用会发生改变)
筛选回收:
负责更新 Region 的统计数据,根据每个 Region 的回收价值和成本进行排序,然后根据用户期望停顿的时间内来指定回收计划,可以选择多个 Region 构成回收集,然后采用复制算法,将 Region 中存活的对象复制到空闲的 Region 中,从而回收 Region。这个阶段涉及到存活对象的移动,必须暂停用户线程,由多条GC线程并行完成。G1收集器除了并发标记之外,其余阶段也是要完全暂停用户线程。即
G1收集器并不是纯粹的追求低延迟,而是在延迟可控的情况下或者尽可能高的吞吐量
。
优缺点
与同样具有低延时的CMS收集器相比,G1具有如下优点:
- 指定期望停顿时间;对内存进行Region分区,按照收益动态进行垃圾收集;
- G1收集器从局部角度,采用的复制算法,从整体上看采用的是标记-整理算法。这两种回收算法都不会产生内存碎片。
但G1收集器也存在如下缺点:
G1占用内存相对较大
。虽然G1和CMS收集器都采用的是卡表实现记忆集以解决跨代引用问题
,G1中每个Region(无论是老年代还是新生代角色)都必须拥有一份卡表,而CMS整个堆内存只需维护一份卡表即可(只需处理老年代到新生代的应用)- G1收集器对系统的造成的负载较高。
G1收集器运行细节
G1 垃圾回收器既能回收新生代,又能回收老年代,那么究竟在什么情况下会触发新生代 GC,什么情况下触发老年代 GC 呢?
- 什么时候出发Minor GC(新生代区域)?
- 在G1中,Eden、Survivor、老年代的大小是动态变化的。虽然进行了Region分区,但是新生代仍然可以划分为Eden和Survivor区域。随着系统的运行,Eden区中的对象越来越多,当Eden区达到最大大小时(默认是堆系统的60%),触发新生代垃圾收集。这一过程采用复制算法,不用考虑并发的场景,全程都是STW,会根据设置的停顿时间,尽可能的最大效率回收新生代区域。
- 什么时候出发Mixed GC?
- 在 G1 中,
不存在单独回收老年代的行为,而是当要发生老年代的回收时,同时也会对新生代以及大对象进行回收,因此这个阶段称之为混合回收(Mixed GC)
- 老年代占用堆空间比例达到阈值时,就会触发混合回收,这一阈值可以通过参数
-XX:InitiatingHeapOccupancyPercent
进行设置(默认是45%)- 当触发Mixed GC 时,会依次执行初始标记(
在 Minor GC 时完成
)、并发标记、最终标记、筛选回收这四个过程。最终会根据设置的最大停顿时间,来计算对哪些 Region 区域进行回收带来的收益最大。- 什么时候出发Full GC?
- 在进行混合回收时,使用的是复制算法,如果当发现
空闲的Region大小无法放得下存活对象的内存大小,那么这个时候使用复制算法就会失败,因此此时系统就不得不暂停应用程序,进行一次 Full GC
。进行Full GC时采用的是单线程进行标记、清理和整理内存,这个过程是非常漫长的,因此应该尽量避免Full GC的触发。- 参数设置
- 在筛选回收阶段,可以分多次回收Region,可通过参数
-XX: G1MixedGCCountTarget
设置分批回收次数(默认是8次)。当需要回收的Region个数太多时,一下次回收可能造成的停顿时间较长。分次进行回收,执行完成一次就执行用户线程,接着再交替执行GC线程。- G1 垃圾回收器的回收思路是:不需要对整个堆进行回收,只需要保证垃圾回收的速度大于内存分配的速度即可。因此在每次进行 Mixed GC 时,虽然我们设置了停顿时间,但是当回收得到的空闲 Region 数量达到了整个堆内存的 5%,那么就会停止回收。可以由参数
G1HeapWaterPercent
控制,默认值为 5%。
GC是收集器调优核心关键是设置最优的期望停顿时间(--XX:MaxGCPauseMillis)
部分内容参考自作者:天堂同志 链接:https://juejin.cn/post/6844904200690728974