JVM垃圾回收
Java的JVM内存管理是关键,理解垃圾回收机制 #生活技巧# #工作学习技巧# #编程语言学习路径#
一个对象的这一辈子:
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了(当Eden区满时,触发Minor GC),我就被迫去了Survivor区的“From”区。自从去了Survivor区,我就开始漂了(Minor GC),有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次Major GC加一岁),然后被回收。
设置堆中各区域比例参数:
-Xmx1024m :堆内存的最大Size1024m
-Xms1024m:堆内存的初始化Size1024m
jvm 启动时,初始化内存init = 已提交内存commit = xms
随着程序的运行 , 已使用内存 use 起来越大 < 已提交内存commit 被迫增长
随着程序的运行 , 已提交内存commit 也随之增长 = xmx ,会抛出OMM
随着垃圾回收,已使用内存 use 变小< 已提交内存commit 变小幅度小 > xms
总结:xmx = xms commit就没这么多事了,节省申请和回收内存的消耗
-Xmn512m:新生代的内存大小512m
-XX:SurvivorRatio=8:新生代中Eden和Survivor的比例为8 :2
–XX:NewRatio=2:新生代与老年代的比例,此时新生代占整个堆空间的1/3,老年代占2/3
Minor GC:清理年轻代 ,当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了
Major GC 是清理老年代。
Full GC 是清理整个堆空间—包括年轻代和老年代。
对象从新生代到老年区的流程:
1.-XX:MaxTenuringThreshold=15:对象在Survivor中的最大年龄
2.-XX:PretenureSizeThreshold=10m:对象Size超过10m后,将直接在老年代分配
3.-XX:+HandlePromotionFailure:关闭新生代分配担保(已被移除在1.6)
空间分配担保
新生代使用复制算法,当Minor GC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC。在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败,如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC。
可达性分析算法《JAVA对象引用》
GC清理方式选择标记清除算法(图1):
过程大致分为两步:标记,清除。首先标记出所有需要回收的对象,在标记结束后,统一回收被标记的对象。
优点:简单
缺点:标记和清除步骤效率都不高,清除后没有进行内存整理,出现大量的内存碎片,如果内存太碎,后面程序再进行申请大内存的时候,不得不又得进行一次垃圾回收。
标记整理算法(图2):
过程大致分为步:标记,整理。首先标记出所有需要回收的对象,在标记结束后,将被标记的对象统一往某一端移动,然后统一回某一边的垃圾对象。
优点:不会产生内存碎片
缺点:效率不高。
复制算法(图3):
为解决效率问题,复制算法会将内存分为两部分,每次使用其中一块,当其中一块使用完毕的时候,便将幸存下的对象复制到另外一块区域,原先的那块内存则被清空
优点:效率较高,不会产生内存碎片
缺点:对内存要求较高
GC Root
jvm的GC ROOTS有哪几个地方呢?
虚拟机栈(局部变量表中引用的对象) 本地方法栈(本地方法引用的对象) 方法区中静态属性引用的对象 方法区中静态常量池中引用的对象 OopMap与安全点JVM回收的时候怎么快速的枚举所有的GC ROOT 然后根据它们可达性分析?
线程在运行程序的时候会把自己的所用的引用信息映射到一个名叫OopMap数据结构中,每个方法的OopMap的个数不会太多,不会太少,太多会导致线程在执行方法时,频繁的生成OopMap,增加程序运行时符合,不能太少,太少会导致系统调用gc之后,等待gc的时间太长。
方法会在以下几种情况下生成OopMap:
1.退出循环的代码行
2.可能抛出异常的代码行
3.方法返回的代码行
4.等等....但情况也不多了
当线程到达上述情况的时候,GC是安全的(引用不会再变化),因此称这此情况为安全点。
另外wait,sleep的进程怎么办,虚拟规定了几个安全区域Safe Region,当程序运行到安全域的时候,Jvm进入gc的时候,就不管这些进入Safe Region的线程了,等线程从Safe Region出来的时候,首先检查Jvm是否正在gc,gc是否完成,没完成,则等待直到收到gc完成的信号为止。
因为堆是线程一共享的区域,必须所有线程都到达安全点或者安全哉才可以进行GC。
抢先式中断:gc发生时,立刻中断所有线程,并检查线程是否中断在安全点上,如果未中断在安全点上,则恢复线程,让其走至安全点。
主动式中断:gc发生时,不操作任何的线程,而是设置一个标志位,等线程查询该标志位时,发现需要gc,则中断线程。
线程查询标志位的时机:生成新的对象,并分配内存的时候,代码经过安全点的时候
新生代垃圾收集器
图1.Serial收集器工作在新生代,采用复制算法
这是一个单线程的收集器,所有的标记和清理操作都在同一个线程,工作时会Stop所有的工作线程(STW),直到工作结束
图2.ParNew收集器工作在新生代,采用复制算法
并行收集器,多线程进行工作,工作时仍会STW
-XX:UserConcMarkSweepGC默认新生代收集器为ParNew收集器,
-XX:UserPerNewGC强制新生代收集器为PerNew收集器
-XX:ParallelGCThreads来限制垃圾回收的线程数,因为并不是线程越多垃圾回收效果就越好,默认开启收集线程数和cpu的数量相同
图3.Parallel Scavenge收集器工作在新生代,采用复制算法
并行收集器,多线程进行工作,工作时仍会STW
-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间,但该参数并不是设置越短就越好,因为停顿时间的减少是使用降低吞吐量和新生代空间获得得,这也代表着,时间越短,垃圾回收的会越频繁
-XX:GCTimeRatio直接控制吞吐量大小:但吞吐量也不能设置太大,因为吞吐量越大,也预示着gc必须要有足够长的时间找到更多的垃圾回收。
-XX+UseAdaptiveSizePolicy会让虚拟机根据当前系统的运行情况动态的调整,以达到最合适的停顿时间或者最大的吞吐量,这种调节方式叫做GC自适应的调节策略
与 ParNew的不同是,他更加注重吞吐量。吞吐量的含义:用户代码执行时间/(用户代码执行时间+GC时间)
老年代垃圾收集器
图1.Serial Old收集器工作在老年代,使用标记-整理算法,单线程收集器,使用时会停止所有的工作线程,直至工作完毕.可搭配:
1.新生代收集器有Serial 收集器
2.Parallel Scavenge收集器
3.在cms收集器发生Concurrent Mode Failure 时,作为后备收集器使用
图2.Parallel Old工作在老年代,使用标记-整理算法,多线程并行收集器,使用时仍会STW
它将gc的吞吐量控制到一个标准(用户可以自己设定),在注重吞吐量和CPU资源敏感的场景使用
另一种,CMS 全称为 Concurrent Mark Sweep。它是现在非常主流的一款老年代的垃圾回收器,因为它能够实现和用户线程并行进行,使用标记-清除算法.
主要分为这几步:
1.初始标记:
Stop the world标记一下 GC roots 关联到的对象。
2.并发标记:主要使用一个三色标记法。
这个算法就是把 GC 中的对象划分成三种情况:
白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
灰色:正在搜索的对象
黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
如图:假设有 A -> B -> C, A 是 GC Roots 关联的对象,
1.把GC Roots 标记,也就是 A 标记成灰色(证明现在正在搜索 A 相关的)
2.搜索A的引用,也就是B,那么搜索了B,把B变成了灰色,那么A就搜索完成了(此时注意,现在是不管C的,因为C不是A的引用,现在只管A的引用是什么)此时把A相关的搜索完了,那么A就变成了黑色,证明 A 已经 ok 了.(Ps:浮动垃圾就是说,此时 GCROOT 改变引用 A 又不用了,那么 A 是没办法回收的,因为 A 已经标记了)
3.循环递归1-2步骤
此时准备要搜索 B 了。刚好,此时,用户线程要执行了,用户线程把原来 A -> B -> C 的引用改成了 A -> C,同时 B 不再引用C。然后又到 GC 线程执行了。GC 线程发现 B 没有引用的对象了(因为用户线程已经把 B -> C 去掉了),那么 B 就相当于搜索完成了,变成黑色了。最后,C 怎么办,C还是白色的呢,白色的是不会搜索,当做垃圾处理的。此时的解决办法就是有一个叫做写入屏障的东西。就是说,如果A已经被标记了(已经是黑色的了),那么用户线程改动 A->C的时候,会把 C 变成灰色,这样,以后就可以搜索 C了。
3.并发预处理:由于第二阶段时间比较长,在其期间发生引用变化的对象比较对多(相对第二阶段还是比较少的),为了下一阶段(重新标记STW的时间近一步减少),提前处理大部份引用变化。
4. 中断的并发预清理: 该阶段主要工作仍然是并发标记对象是否存活,只是这个过程可被中断。
此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值(可以通过参数修改)。如果此阶段执行时等到了Minor GC,Reamark阶段需要扫描的对象就少了.除此之外CMS为了避免这个阶段没有等到Minor GC而陷入无限等待,提供了参数CMSMaxAbortablePrecleanTime ,默认为5s,含义是如果可中断的预清理执行超过5s,不管发没发生Minor GC,都会中止此阶段,进入Remark。如果超过了设置的5s被中断,期间没有等到Minor GC ,Remark时新生代中仍然有很多对象,CMS提供CMSScavengeBeforeRemark参数,用来保证Remark前强制进行一次Minor GC。为什么搞一阶段策略尽量让重新标记阶段前要用Minor GC
Init-mark初始标记,并发标记处理的前提是Minor GC完后的GC Root不变的前提下
如图,如果在初始标记,并发标记期间 新生带引用了老年代的情况出现。图中对象A因为引用存在新生代中,它在Remark阶段就不会被修正标记为可达,GC时会被错误回收。因它的存在,Remark阶段必须扫描整个堆来判断对象是否存活,包括图中灰色的不可达对象。
扫描全堆的原因:新生代GC和老年代的GC是各自分开独立进行的,只有Minor GC时才会使用根搜索算法,标记新生代对象是否可达,也就是说虽然一些对象已经不可达,但在Minor GC发生前不会被标记为不可达,CMS也无法辨认哪些对象存活,只能全堆扫描(新生代+老年代)
上述分析只涉及老年代GC,其实新生代GC存在同样的问题,即老年代可能持有新生代对象引用,所以Minor GC时也必须扫描老年代。
JVM是如何避免Minor GC时扫描全堆的?
经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。如下图所示:
卡表的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。
5.重新标记:本次标记STW,本次得到的结果将是最后垃圾回收的结果,由于并发预处理这个阶段用户线程和GC 线程并发,假如这个阶段用户线程产生了新的对象,这个对象是白色的,总不能被 GC 掉吧。这个阶段就是为了让这些对象重新标记。
6.并发清理:
第一阶段:将之前得到的结果采用的是标记-清除算法进行清除。 第二阶段:重置CMS内部状态,为下次回收做准备。CSM参数:
-XX:+UseConcMarkSweepGC启用CMS
-XX:ConcGCThreads可以设置并发的线程,线程不是越多越好
-XX:CMSInitiatingOccupancyFraction 来调整老年代触发Full gc的内存的比例阈值, 比例不能太大,容易导致 Concurrent Mode Failure,比例不能太小,导致老年代频繁GC。因为cms收集器的并发标记阶段,用户线程仍在进行,所以会产生浮动垃圾,这部分垃圾是在本次gc中无法回收。如果浮动垃圾的大小过大,本次gc回收期间,再次到达触发Full gc的条件,收集器将报出Concurrent Mode Failure,然后再使用Serial Old收集器重新回收,这样就导致了STW的时间更长。
-XX:+UseCMSCompactAtFullCollection(默认开启) 进行Full GC时开启内存碎片整理。 使用标记-清除算法可能造成大量的空间碎片。空间碎片过多,就会给大对象分配带来麻烦。往往老年代还有很大剩余空间,但无法找到足够大的连续空间来分配当前对象,不得不触发一次Full GC。
-XX:CMSFullGCBeforeCompaction 用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,每次进入Full GC时都进行碎片整理)。
-XX:+CMSParallelRemarkEnabled为了减少第二次暂停的时间,开启并行remark
-XX:+CMSScavengeBeforeRemark 强制remark之前开始一次minor gc,减少remark的暂停时间
G1垃圾收集器
G1收集器采用了不同的方法,它将整个Java堆分成2000个左右相同大小的堆区域(年轻代+老年代),这些区域集像以前的收集器一样被分配到不同的角色,如Eden, Survivor和Old, 但是没有固定的区域数量被明确固定在某个角色上,这将为内存使用上提供了更大的灵活性。还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1的YoungGC
存活的对象会被准转移(拷贝或移动)到1个或者多个survivor区域。如果到达了年龄阈值,一些对象会被晋级到老年代的区域。
我们如何找到所有的老年代根对象呢(老年代有些对象引用着新生代),扫描整个老年代?
G1引进了RSet的概念,在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。但G1中使用point-in来解决(G1中分区过多)。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合(记录着区域内哪些对象被引用)
G1 Mix GC
不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。它的GC步骤分2步:
1.全局并发标记(global concurrent marking)
Phase
Description
(1) 初始标记(STW)
这是一个STW的事件,使用G1,初始标记阶段会在YoungGC附带。标记survivor区域对象(根区域),它们可能引用到老年代对象
(2) 根区域扫描
扫描survivor区域对象引用了老年代对象。这一阶段和应用程序并发,必须在YoungGC之前完成
(3) 并发标记
在整个堆上查找活动对象,并发标记和应用程序并发进行。这一阶段可以被YoungGC打断
(4) 重新标记(STW)
完成堆上所有存活对象的标记,使用一种叫snapshot-at-the-beginning (SATB) 的算法,该算法比CMS重新标记的算法高效很多
(5) 清理(STW和并发)
(1)执行存活对象和完全空闲区域的统计(STW) (2)清空Remembered Sets(STW) (3)重置空闲区域并将它们返回到空闲列表(并发)
(*) 拷贝(STW)
这个阶段STW操作,转移和复制存活的对象到未使用的区域。这可以在年轻代执行,日志显示为[GC pause (young)]。或者同时在年轻代和老年代的区域执行,日志显示为[GC Pause (mixed)]
其大致和CMS的GC相似,但他的三色标记法和CMS是不同的。
CMS采用的是 把C改用搜索状态(在插入A->C的时候记录对象)
G1则是把A改用搜过状态 (在删除A->B的时候记录对象)
2.拷贝存活对象(evacuation)
只是起到个整理的作用
G1作为新生带的垃圾收集器主要有以下几个特点:
1.并行和并发缩短STW的时间
2.空间整合:使用标记-整理算法和复制算法结合,不会产生内存碎片
3.可以同时应用到新生代和老年代
4.可预测的停顿
Option and Default Value
Description
-XX:+UseG1GC
G1收集器开关
-XX:MaxGCPauseMillis=n
设置最大的目标暂停时间,这是一个软性目标,JVM蒋尽量去实现
-XX:InitiatingHeapOccupancyPercent=n
启动并发GC的Java堆占用阈值,用于触发GC周期,不仅仅是触发一个代(例如G1)。值为0表示“始终启动GC循环”,默认值是45
-XX:ParallelGCThreads=n
设置在垃圾回收器的并行阶段使用的线程数,默认值根据JVM平台而不同
-XX:ConcGCThreads=n
并发垃圾收集器将使用的线程数,默认值根据JVM平台而不同
-XX:G1ReservePercent=n
设置作为空闲空间的预留内存百分比,以降低晋级失败的可能性,默认值是10%
-XX:G1HeapRegionSize=n
G1把Java堆细分成均匀大小的区域,这个参数设置region的大小。此参数的默认值根据堆的大小设置,最小值是1Mb最大值是32Mb
垃圾收集的常用组合young
Tenured
JVM options
Serial(Copy)
Serial Old(ConcurrentMarkSweep)
-XX:+UseSerialGC
Parallel New(ParNew)Serial Old(ConcurrentMarkSweep)-XX:+UseParNewGC
Parallel Scavenge(PS Scavenge)
Serial Old(ConcurrentMarkSweep)
-XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel Scavenge(PS Scavenge)
Parallel Old(PS MarkSweep)
-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New(ParNew)
CMS(ConcurrentMarkSweep)
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Serial(Copy)CMS(ConcurrentMarkSweep)-XX:-UseParNewGC -XX:+UseConcMarkSweepGCG1(G1 Young Generation)
G1(G1 Old Generation)
-XX:+UseG1GC
硬件条件较低的Jvm适合Serial 收集器和Serial Old收集器,如果注重交互的话,可以使用serial 收集器和CMS收集器
硬件配置较高的Jvm,如果应用程序是对吞吐量要求较高的后台计算的程序,则可以使用Parallel Scavenge收集器和Parallel Old收集器
硬件配置较高的Jvm,如果应用程序是对用户体验要求较高的强交互的程序,则可以使用PerNew收集器和CMS收集器搭配
想尝试新鲜事物的可以使用G1收集器,据说G1在降低停顿时间上还是有些效果的
我们公司选用的是PerNew收集器和CMS收集器搭配
实验证明使用 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15
证明:对象都是先出生在eden,当Eden区满时,触发Minor GC 会移动到from
如图代码,我们会发现 我们创建的对象都在 eden区,8192*83%=6799K ,比我们的对象 1024*4+512=4608K 要大2191K(现在常量池也在堆中,也是新生成在eden)
如图代码,我们一直创建对象,让Eden区满,则会触发由于 分申请内存失败引起的GC(Allocation Failure),看经过GC后,年轻代从,6662K->934K了 ,看 from 区从原来的 0 增长到934K 其中 512K是对象b1,多出来的则是活下的常量。然后b4的创建又在 eden占用了,2048K + 164K =2212K
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15
证明:年龄到了或者同龄人到达量,也会进入老年代
如图参数中加上 -XX:MaxTenuringThreshold=1,也就是经过一次GC的b1在第二次GC的时候。直接进入老年代concurrent mark-sweep generation
如图参数中加上 -XX:MaxTenuringThreshold=15或者直接去掉,也就是 也就是经过一次GC b1,b2。在进行第二次GC的时候,如果 b1+b2+其它同年变量大于 512K时 直接进入 老年代concurrent mark-sweep generation
证明:空间担保
图1当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
右图 1 都满足,老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量,直接进行担保
右图 2 不满足,老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量,直接提保失败fullGC
网址:JVM垃圾回收 https://www.yuejiaxmz.com/news/view/182762
相关内容
JVM垃圾回收器G1垃圾回收器参数
垃圾分类丨大件垃圾≠可回收物
建筑垃圾回收再利用
生活垃圾分类“你问我答”(可回收垃圾篇)
可回收物+有害垃圾智能回收设备
餐厨垃圾回收回去变成什么
垃圾分类|你知道回收垃圾,可再造哪些价值?
家庭垃圾分类及回收利用
垃圾分类回收有哪几大类?