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