Java垃圾回收
2020-09-29 15:38:42 0 举报
AI智能生成
JVM性能调优-垃圾回收器和回收策略-史上最全
作者其他创作
大纲/内容
垃圾收集需要完成的三件事
4W1H
Why
为什么需要垃圾回收
Java程序在虚拟机运行,会占用内存资源,比如创建对象,加载类型数据等,而且内存资源有限
当创建对象不再被引用时,这些对象称之为垃圾对象,如果不被回收,只会越堆越多,最终会撑爆内存,导致内存溢出
Where
垃圾回收发生在哪里
确定性的回收
程序计数器
虚拟机栈
本地方法栈
非确定性的回收
堆
垃圾对象的回收
方法区
废弃的常量
常量池中废弃的字面量、字段、方法的符号引用等
无用的类
该类所有的实例都已经被回收,Java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Which
哪些内存需要回收
一般一个对象不再被引用,就代表该对象可以被回收了
When
对象在什么时候被回收
JVM启动时提供一个垃圾回收线程来跟踪每一块分配出去的内存空间,并定期清理需要被回收的对象
Java程序无法强制执行垃圾回收,但我们可以通过调用System.gc方法来"建议"执行垃圾回收,但是否可执行,什么时候执行,是不可预期的
How
如何回收
引用计算算法
可达性分析算法
判断哪些内存需要回收
引用计数算法
用计数器来判断对象是否被引用
每当对象被引用,计数器就加1
每当引用失效,计数器就减1
当对象的引用计算器的值为0时,说明该对象不再被 引用,可以被回收
优点:引用计数算法实现简单,判断率高
缺点:无法解决对象之间互相循环引用的问题。若两个对象互相引用,但没有任何其它对象引用它们,而它们的引用计数器都不为0,就无法被回收
可达性分析算法(GC Roots)
Java虚拟机栈中的引用的对象,如方法参数,局部变量,临时变量等
方法区中的类静态属性引用的对象
方法区中的常量引用的对象,如字符串常量池的引用
本地方法栈中JNI的引用对象
Java虚拟机内部的引用,如基本数据类型的Class对象,系统 类加载器等
Java中的引用类型
强引用
只要引用还在,垃圾回收器就不会回收被引用的对象
强引用是最普遍的引用方式,如在方法中定义:Object obj = new Object()
软引用
有用但在非必须的对象,可以使用SoftReference类来实现软引用
对于软引用关联着的对象,在系统将要发生内存溢出异常之前(一般发生老年代的GC时),会把这些对象列进回收范围之中。如果回收之后 内存还是不足,才会报内存溢出异常
这一点很好地用来解决OOM的问题,并且这个特性很适合用来实现内存缓存,当内存快满时,就回收掉这些软引用的对象,然后需要的时候再重新查询。
示例代码
弱引用
非必须的对象,可以使用WeakReference类来实现弱引用。
它只能生存到下一次垃圾回收发生之前(一般发生年轻代的GC时),当垃圾回收机制开始时,无论是否会内存溢出,都将回收掉弱引用关联的对象
PS:我们使用SoftReference来创建软引用对象,使用WeakReference类来创建弱引用对象,垃圾回收时,是回收它们关联的对象,而不是Reference本身。同时,如果Reference关联的对象被其它GC Roots引用着,也是不能被回收的。
垃圾回收时,只有T002这个Account对象能被回收,回收后reference2.get()返回值为null,account,reference1,reference2所指向的对象都不能被回收
虚引用
最没存在感的一种引用关系,可以使用PhantomReference类来实现虚引用
引在或不存在几乎没影响,也不能通过虚引用来获取一个对象实例,存在的唯一目的是被垃圾回收器回收后可以收到一条系统通知
分代收集理论
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域
一般至少将堆分为新生代和老年代,再根据不同代的特点采取最适合的回收算法
新生代中,每次垃圾回收都有大量对象死去,因为程序创建的绝大部分对象的生命周期很短,朝生夕灭
新生代回收后存活的对象会逐步晋升到老年代中存放。老年代每次收集只有少量对象需要被回收,因为老年代大部分对象一般都是全局变量引用,生命周期比较长
划分出不同区域后,垃圾回收器就可以每次只回收其中一个或某些部分的区域,因而有了Young GC,Old GC,Full GC的回收类型划分
新生代收集(Minor GC/Young GC):指目标只是新征伐的垃圾收集
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集,包括新生代、老年代、方法区的回收,一般Full GC等价于Old GC
可以针对不同区域安排与里面存储对象存亡特征相匹配的垃圾回收算法,如"标记-清除","标记-复制","标记-整理"等针对性的垃圾回收算法
垃圾收集算法
标记清除法
Mark-Sweep
分为"标记"和"清除"两个阶段,首先从GC Roots进行扫描,对存的对象进行标记,标记完成后,再统一回收所有款被标记的对象
优点:
1、不需要进行对象移动,只需要回收未标记的垃圾对象,在存活对象比较多的情况下极为高效
缺点:
1、执行效率不稳定,如果堆中对象很多,而且大部分都是要回收的对象,就必须 进行大量的标记和清除支付,导致标记、清除两个过程的效率随着对象数量增长而降低
2、清除后会生产大量不连续的内存碎片,大量的碎片有可能因无法被分配到大对象而更容易引用另一次垃圾回收
标记-复制算法
Copying
半区复制算法
主要是为了解决标记-清除算法在存在大量可回收对象时执行效率低下和内存碎片的问题
优点:
1、每次都是针对整个斗殴进行内存回收,清理速度快,没有内存碎片产生
2、每次回收后,对象有序排列到另一个空闲区域,分配内存时也就不用考虑有空间碎片的复杂情况
缺点:
1、如果内存中多数对象都是存活的,这种算法将产生大量的内存空间复制的开销
2、将可用内存缩小为原来的一半,内存使用率低
复制算法优化
1、大多数对象都是朝生夕来,新生代中98%的对象几乎熬不过第一轮回收,因此不需要按照1:1的比例来划分新生代的内存空间
2、因此新生代复制算法一般把新生代分为一块较大的Eden区和两块较小的Survivor区,每次分配内存只使用Eden和其中一块Survivor。发生垃圾回收时,将Een和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,如此往复。当对象经过垃圾回收的次数超过一定阀值还未被回收时,就会进入老年代,有些大对象也可以直接进入老年代
优点:
1、HotSpot虚拟机默认Eden和Servivor区的大小比例是8:1:1,新生代与老年代的比例大概是1:2。内存空间利用率高,只会有10%的空闲空间
缺点:
1、有可能一次的Young GC后存活的对象超过一个Survivor区的大小,这时候会依赖其他内存区域进行分配担保,让这部分存活下来的对象直接进入老年代区
标记整理算法
Mark-Compact
采用标记-清除算法一样的方式进行对象的标记,但清除时不同,它不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存(适用于老年代)
优点:
1、没有内存碎片产生,适合老年代垃圾回收
缺点:
1、会有对象的移动,老年代存活对象多,移动对象还需要更新指针,因此成本会更高
分区收集算法
将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收
优点:
1、可以控制 一次性回收多少个小区间
2、根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿
HotSpot算法细节
根节点枚举
安全点
线程到最近的安全点上主动中断挂起
安全区域
sleep,无法响应虚拟机终端请求
安全区域中任意地方开始垃圾收集都是安全的
记忆集
记录从非收集区域指向收集区域的指针集合的抽你数据结构
卡表
记忆集的具体实现
写屏障
维护卡表状态
垃圾收集器
Serial收集器
单线程、复制算法、STW
服务端程序几乎不使用Serial回收器,因为单线程,标记,清理阶段花费时间长,会导致系统较长时间的停顿
一般应用于客户端程序,因为客户端程序一般分配的内存小,回收停顿时间可授受,而且Serial是所有回收器里额外消耗内存最小,也没线程切换开销,非常简单高效
Serial Old收集器
单线程、标记整理、STW
是 Serial 的老年代版本
可应用于服务端程序,主要有两种用途
1、与Parallel Scavenge回收器搭配使用
2、作为CMS回收器发生失败时的备用案,在并发收集发生Concurrent Mode Failure时使用
ParNew收集器
多线程、复制算法、STW
是Serial回收器的多线程并行版本,除与同时使用多条线程进行垃圾回收外,其他行为与Serial回收完全一致
是除Serial回收器外,目前只ParNew回收器能与CMS回收器配合工作,ParNew是激活CMS后的默认新生代回收器
Parallel Scavenge收集器
新生代回收器,多线程复制算法、高效、STW
主要关注可控制的吞吐量,其它回收器的关注点是尽可能地缩短垃圾回收时的停顿时间。吞吐量就是处理器用于运行程序代码的时间与处理器总消耗时间的比值,总消耗时间等于运行程序代码的时间加上垃圾回收的时间
提供两个参数用一精确控制吞吐量
-XX:MaxGCPauseMills:控制最大垃圾回收停顿时间,参数值是一个大于0的毫秒数,回收器将尽力保证垃圾回收花费的时间不超过这个值
-XX:GCTimeRatio:直接设置吞吐量大小,参数值是一个大于0小于100的整数,就是垃圾回收时间占总时间的比率。默认为99,即允许最大1%的垃圾收集时间
另一个参数,-XX:+UseAdaptiveSizePolicy:设置这个参数之后,就不需要人工指定新生代的大小、Eden与Survivor区的比例等细节参数。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
Parallel Old收集器
多线程、标记整理、STW
是Parallel Scavenge的老年代版本
CMS收集器
是一种以获取最短回收停顿时间为目标的回收器。CMS用于老年代的垃圾回收,采用标记-清除算法实现
回收过程
初始标记:需要STW,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记:从GC Roots的直接关联对象开始遍历整个对象引用链的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾回收线程一起并发运行
重新标记:需要STW,这个阶段是为了修正并发标记期间,因程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
并发清除:清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活动对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS的问题
并发回收导致CPU资源紧张
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序 变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数+3)/4,当CPU术数不足4个时,CMS对用户程序 的影响就可能变得很大。
无法清理浮动垃圾
在并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对你是出现在标记过程结束后,CMS无法在当次收集中处理它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”
并发失败
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX: CMSInitiatingOccupancyFraction 参数来设置
要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。所以参数 -XX: CMSInitiatingOccupancyFraction 设置得太高将会很容易导致大量的并发失败产生,性能反而降低;太低又可能频繁触发CMS回收,所以在生产环境中应根据实际应用情况来权衡设置
-XX: CMSInitiatingOccupancyFraction 参数值默认为-1,计算出来的阀值是92%,也可以自己指定值,同时还需要设置 -XX:+UseCMSInitiatingOccupancyOnly,让JVM使用设定的回收阈值,如果不设置,JVM仅在第一次使用设定值,后续则自动调整。
CMS并不是时时刻刻都在执行GC的,可以通过 -XX:CMSWaitDuration 参数设置CMS GC线程的间隔时间,默认值为2000毫秒
内存碎片问题
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
C1收集器
G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
回收器的特点
可预期的回收停顿时间
G1 可以指定垃圾回收的停顿时间,通过 -XX: MaxGCPauseMillis 参数指定,默认为 200 毫秒。这个值不宜设置过低,否则会导致每次回收只占堆内存很小的一部分,回收器的回收速度逐渐赶不上对象分配速度,导致垃圾慢慢堆积,最终占满堆内存导致 Full GC 反而降低性能。
G1之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元,即每次回收到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾回收。G1会去跟踪各个Region的垃圾回收价值,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的回收停顿时间,优先处理回收价值收益最大的那些Region。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1回收器在有限的时间内得到尽可能高的回收效率。
占用更高的内存
由于Region数量比传统回收器的分代数量明显要多得多,因此G1回收器要比其他的传统垃圾回收器有着更高的内存占用负担。G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持回收器工作
内存布局
G1不再是固定大小以及固定数量的分代区域划分,而是把堆划分为多个大小相等的Region,每个Region的大小默认情况下是堆内存大小除以2048,因为JVM最多可以有2048个Region,而且每个Region的大小必须是2的N次冥。每个Region的大小也可以通过参数 -XX:G1HeapRegionSize 设定,取值范围为1MB~32MB,且应为2的N次幂
G1也有新生代和老年代的概念,不过是逻辑上的区分,每一个 Region 都可以根据需要,作为新生代的Eden空间、Survivor空间,或者老年代空间。新生代默认占堆内存的5%,但最多不超过60%,这个最大值可以通过 -XX:G1MaxNewSizePercent 参数设置。
大对象Region
Region中还有一类特殊的 Humongous 区域,专门用来存储大对象,而不是直接进入老年代的Region。G1认为一个对象只要大小超过了一个Region容量的一半就判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的 Humongous Region 之中,G1的大多数行为都把 Humongous Region 作为老年代的一部分来看待。
回收过程
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象
最终标记:对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象
混合回收:更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。
G1混合回收
-XX:InitiatingHeapOccupancyPercent,它的默认值是45%,就是如果老年代占堆内存45%的Region的时候,此时就会触发一次年轻代+老年代的混合回收
-XX:G1HeapWastePercent,默认值是 5%。就是在混合回收时,Region回收后,就会不断的有新的Region空出来,一旦空闲出来的Region数量超过堆内存的5%,就会立即停止混合回收,即本次混合回收就结束了
-XX:G1MixedGCLiveThresholdPercent,默认值是85%。意思是回收Region的时候,必须存活对象低于Region大小的85%时才可以进行回收,一个Region存活对象超过85%,就不必回收它了,因为要复制大部分存活对象到别的Region,这个成本是比较高的。
回收失败
并发回收失败
在并发标记阶段,用户线程还在并发运行,程序继续运行就会持续有新对象产生,也需要预留足够的空间提供给用户线程使用。G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,跟CMS会发生并发失败一样,G1也要被迫暂停程序,导致 Full GC 而产生长时间 Stop The World。
混合回收失败
混合回收阶段,年轻代和老年代都是基于复制算法进行回收,复制的过程中如果没有空闲的Region了,就会触发失败。一旦失败,就会停止程序,然后采用单线程标记、清理和内存碎片整理,然后空闲出来一批Region。这个过程是很慢的,因此要尽量调优避免混合回收失败的发生。
GC性能衡量指标
吞吐量
吞吐量是指应用程序所花费的时间和系统总运行时间的比值。系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
停顿时间
指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低
垃圾回收频率
通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可
内存设置和查看GC日志
设置JVM内存
JVM内存分配有如下一些参数
-Xms:堆内存大小
-Xmx:堆内存最大大小
-Xmn:新生代大小,扣除新生代剩下的就是老年代大小
-Xss:线程栈大小
-XX:NewSize:初始新生代大小
-XX:MaxNewSize:最大新生代大小
-XX:InitialHeapSize:初始堆大小
-XX:MaxHeapSize:最大堆大小
-XX:MetaspaceSize:元空间(永久代)大小,jdk1.8 之前用 -XX:PermSize 设置
-XX:MaxMetaspaceSize:元空间(永久代)最大大小,jdk8 之前用 -XX:MaxPermSize 设置
-XX:SurvivorRatio:新生代 Eden 区和 Survivor 区的比例,默认为 8,即 8:1:1
一般 -Xms 和 -Xmx 设置一样的大小,-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 设置一样的大小。-Xms 等价于 -XX:InitialHeapSize,-Xmx等价于-XX:MaxHeapSize;-Xmn等价于-XX:MaxNewSize。
命令行启动时可以按照如下格式设置
java -jar -Xms1G -Xmx1G -Xmn512M -Xss1M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M app.jar
查看GC日志
设置GC参数
可以在启动时加上如下参数来查看GC日志:
-XX:+PrintGC:打印GC日志
-XX:+PrintGCDetails:打印详细的GC日志
-XX:+PrintGCTimeStamps:打印每次GC发生的时间
-Xloggc:./gc.log:设置GC日志文件的路径
例如,我在IDEA中添加了如下JVM启动参数:
-Xms1G
-Xmx1G
-Xmn512M
-Xss1M
-XX:MetaspaceSize=128M
-XX:MaxMetaspaceSize=128M
-XX:SurvivorRatio=8
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log
0 条评论
下一页