西游篇——JVM-垃圾回收器GC
上回说到孙小生帮工厂老板整顿了工厂之后,再次踏上了旅途。这一次,他遇到了这个西游世界的核心,垃圾厂。他是这个世界所有废品的回收中心,有了这个垃圾厂,才保证了这个世界的清洁、有序。接下来让我们一起随着孙小生,去探索这个庞大的运转机器
1. 为什么要有GC
在开发编程中,程序员需要对申请的资源进行手动地释放,比如在C中malloc申请的内存,在不使用的时候,需要显示地调用free函数释放使用的资源,否则申请的空间不释放,一旦多次申请后,导致OOM;但是人为地控制相对来说是比较麻烦的,而且不易掌控,所以在Java中有GC,即开发人员不需要关心资源的释放,全部交由GC处理。
2. 什么样的资源被定义为垃圾
1. 引用计数法
- 定义:都知道在Java中要操作对象,首先要获取该对象的引用,所以可以通过判断该对象是否可以被回收的依据就是引用。当为一个对象添加引用时,引用计数就+1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示该对象没有被引用,那么就可以被GC回收。
- 缺点:循环引用问题:即两个对象互相引用,导致引用计数一直为1,无法回收,一旦这样的对象多了,导致OOM。
2. 可达性
为了解决循环引用,所以有了可达性分析。
- 定义:
3. 引用类型
既然依据对象引用来确定垃圾,那么就要知道Java中都有那些引用类型
1. 强引用
2. 软引用
- 定义:如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
- 存储对象:
- 大对象的缓存
- 常用对象的缓存
3. 弱引用
- 定义:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
- ThreadLocal、WeakHasMap
4. 虚引用
3. 垃圾回收算法
知道了什么是垃圾之后,那么就需要相应的垃圾器去回收了这些被确定为垃圾的对象。
1. 标记清除算法 Mark-Sweep
- 定义:该算法分为两个阶段:
- 存在问题:
- 内存整理:该算法只是直接清除垃圾对象,释放占用内存。并没有对内存进行整理,这就导致,如果之前存放的对象小于后面存放的大对象,那么大对象就无法使用这一小块内存,导致内存碎片产生
2. 复制算法 copying
- 定义:
- 解决的问题:
- 这样不会导致内存碎片的产生
- 产生的问题:
- 浪费内存,在同一时间,16G的内存,只有8G能用;其次存活对象的来回复制,影响运行效率
- 小结:因此复制算法适用于那种生命周期短的对象,“朝生夕死”
3. 标记整理算法 Mark-Compact
4. 分代收集算法
- 定义:分代垃圾算法根据对象的不同类型将内存划分为不同的区域。JVM中将堆划分为老年代和新生代,新生代存放那些生命周期短的对象,因此采用复制算法;老年代存放那些生命周期长的、大对象,采用标记整理算法。
- 当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。 - 分代带来的隐患:对象之间的跨代引用:举一个这样的例子:
- 解决:跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
1. 新生代进一步划分
新生代进一步划分为Eden区,Survivor区,Survivor区进一步划分为Survivor From区和Survivor To区,这两个区是可以相互替换的,不是一成不变
1. Eden区:
对象优先在Eden区、Survivor分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(YGC)
2. Servivor区
在MinorGC后,会将Eden区以及Survivor From区存活的对象赋值放入Survivor To区,然后清除Eden、Survivor From区。下次则将Eden、Survivor From(这里Survivor To变为Survivor From区,两者可以替换)存活对象复制到Survivor To区。
在网上看到这样一段话:https://blog.csdn.net/weixin_43896829/article/details/104600153
3. 一个对象的一辈子:
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
4. 对应参数的控制
- 设置年轻代的大小:
-XX:NewSize;-XX:MaxNewSize
:JVM堆区新生代内存最大为多少; -XX:PretenureSizeThreshold
:可以设置多大的对象可以直接进入老年代内存区域。尽量减少FGC-XX:HandlePromotionFailure
:打开空间担保分配
5. 空间担保分配
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。
4. 垃圾回收器
前面知道了原理,接下来,看一下具体的实现,JVM针对新生代,老年代,分别提供了多种不同的垃圾回收器。来一一了解下他们的优、缺点
1. 新生代
先了解一波新生代的
1. Serial:单线程、复制算法
- 定义:该垃圾器基于复制算法实现,单线程工作,即当Serial正在进行GC时,必须暂停其他所有工作线程,就是STW,直到垃圾垃圾回收结束。
- 特点:由于采用的是复制算法:所以实现比较简单,运行高效,对于单核cpu来说,没有线程交互开销;
- 场景:所以Serial一般作为JVM下Client模式下的新生代的默认垃圾收集器
- 相关参数:
2. ParNew:多线程、复制算法
- 定义:同样基于复制算法实现,不过是多线程的,在进行GC时同样需要STW,不同的是Serial单线程,ParNew是多线程;
- 特点:因为是多线程,所以GC效率要高于Serial,STW时间优于Serial,但是单核cpu下效率不比Serial收集器快。除了Serial外,目前只有它能和CMS(后面介绍)配合工作。
- 场景:用于JVM下Server模式下的新生代默认的垃圾收集器
- 相关参数:
JVM的Client模式和Server模式:
- Client模式启动速度较快,Client模式启动的JVM采用的是轻量级的虚拟机
- Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化
3. Parallel Scavenge:多线程,复制算法
- 定义:同样基于复制算法实现,在系统吞吐量上有很大的优化,可以高效地利用cpu尽快完成垃圾回收任务
- 特点:通过自适应调节策略提高系统吞吐量,但是STW时间较长,用户体验不好
- 参数:
-XX:MaxGCPauseMillis
:控制最大垃圾收集停顿(STW)时间-XX:GCTimeRatio
:控制吞吐量大小,垃圾收集时间占总时间的比值,相当于吞吐量的倒数。例如设置吞吐量19,那最大GC时间就占总时间5%(1/(1+19)),默认值99。-XX:+UseParallelGC
:开启Parallel Scavenge收集器。开启此参数使用parallel scavenge & parallel old搜集器组合(server模式默认值)。-XX:+UseAdaptiveSizePolicy
:自适应调节策略,当打开这个参数之后,JVM会根据运行情况收集性能监控信息,
将动态调整这些参数(如-Xmn,-XX:SurivivorRatio,-XX:PretenureSizeThreshold等)
2. 老年代
1. Serial Old:单线程,标记整理算法
- 定义:基于标记整理算法实现,单线程工作,老年代GC时,必须暂停其他工作线程,是JVM运行在Client模式下的老年代的默认垃圾收集器
- 特点:同年轻代的Serial一样
- 参数:
-XX:+UseSerialOldGC
:开启Serial Old 收集器
2. Parallel Old:多线程,标记整理算法
- 定义:基于标记整理算法实现,多线程工作,同样老年代GC,要STW
- 特点:优缺点和Parallel Scavenge收集器一样,只是之前老年代收集只有Serial Old收集,所以Parallel Scavenge收集和Serial Old收集组合一起发挥不了吞吐量的优势,所以出现了该收集器。注重吞吐量以及cpu资源敏感的场合,优先考虑Parallel Scavenge+Parallel Old收集器。
- 参数:
3. CMS:多线程、标记清除算法
- 定义:基于多线程的标记清除算法实现,以便在最短的GC时间提高系统的稳定性
- 工作流程:与其他GC收集器不同的是,CMS的工作流程比较复杂,分为4个阶段:
- 特点:可并行并发处理,注重停顿时间,用户体验更快,但是产生浮动垃圾、内存碎片、吞吐量会下降
- 参数:
-XX:+UseConcmarkSweepGC
:开启CMS收集器,开启此参数使用ParNew & CMS(serial old为替补)组合搜集器。-XX:CMSInitiatingOccupancyFraction
:由于CMS收集存在浮动垃圾,CMS不能等到老年代用尽才进行回收,而是使用率达到设定值就触发垃圾回收。不能设置太高,否则会出现“Concurrent Mode Failure”错误而临时启用Serial Old收集器导致停顿时间加长XX:+UseCMSInitiatingOccupancyOnly
:开启固定老年代使用率的回收阈值,如果不指定,JVM仅在第一次使用设定值,后续则自动调整。XX:+UseCMSCompactAtFullCollection
:开启对老年代空间进行压缩整理(默认开启)。由于CMS收集会产生内存碎片,所以需要对老年代空间进行压缩整理。-XX:CMSFullGCsBeforeCompaction
:设置执行多少次不压缩的Full GC后,紧接着就进行一次压缩整理(默认为0,每次都进行压缩整理)-XX:+CMSScavengeBeforeRemark
:执行CMS 重新标记(remark)之前进行一次Young GC,这样能有效降低remark时间。
1. CMS中用到算法、以及需要注意那些问题:
1. Remark阶段:三色标记算法(标记存活对象)
-
概念:
-
漏标的问题:
4. G1:多线程,标记整理算法
-
特点:年轻代和老年代共用,可并行并发处理、分代收集,空间整合、有点回收垃圾多的区域、可预测低停顿;在G1中采用了另一种完全不同的方式组织堆内存,如下图:
堆被划分为多个大小相等的内存块(Region),每个Region都是逻辑连续的一段内存,每个Region在运行时都充当了一种角色。H表示存放一个超大对象,就是当新建对象超过Region大小一半时,就将这个对象定义为H
-
相关参数:
-XX:+UseG1GC
:开启G1收集器。-XX:MaxGCPauseMillis
:设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.-XX:InitiatingHeapOccupancyPercent
:启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45-XX:NewRatio
:新生代与老年代(new/old generation)的大小比例(Ratio). 默认值为 2.-XX:SurvivorRatio
:eden/survivor 空间大小的比例(Ratio). 默认值为 8-XX:MaxTenuringThreshold
:提升年老代的最大临界值(tenuring threshold). 默认值为 15.-XX:ParallelGCThreads
:设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.-XX:ConcGCThreads
:并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.-XX:G1ReservePercent
:设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.-XX:G1HeapRegionSize
:使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.
1. GC模式过程
1. Young GC
当eden region被耗尽时,会触发YGC,然后会将存活的对象放入survivor中,如果对象年龄够了的话,就放入old region中,然后清除eden,将空闲的region放入空闲表中。
2. Mixed GC
这个过程比较复杂,它类似于CMS过程,但是这里需要注意的是:并不是进行old gc,只是除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
- 初始标记:同样要STW,标记GC Roots可达对象
- 并发标记:工作线程与GC标记同时进行,标记出GC Root可达对象衍生出去的存活对象
- 最终标记:因为并发过程中,对象状态会改变,所以需要重新标记,所以此期间需要STW
- 并发回收:清除垃圾,释放region
3. Full GC
如果对象分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc。
所以可以降低Mixed GC触发的阈值,让Mixed GC提早发生。
5. 垃圾收集器的常用参数:
选项 | 描述 | 使用示例 |
---|---|---|
-xms | 设置Java堆的初始大小。当可用的Java堆区内存小于40%时,JVM就会将内存调整到选项-Xmx所允许的最大值 | -xms100M |
-Xmx | 设置Java堆的最大值。当可用的Java堆区内存大于70%时,JVM就会将内存调整到选项-xms所指定的初始值。与-xms设置为一样的话可以避免堆自动扩展带来的性能开销。 | -Xmx300M |
-Xss | 设置栈容量大小。每个线程都拥有一个栈,生命周期与线程相同,每个方法的调用都会创建一个栈帧。在堆容量确定的情况下,栈容量越大意味着能建立的线程越少。 | -Xss2M |
-Xmn | 设置新生代的大小。-Xmn的内存大小为Eden+2个Surivivor空间的值,官方建议配置为整个堆的3/8。 | -Xmn100M |
-XX:NewSize | 设置新生代的初始大小。和-Xmn等价,推荐使用-Xmn,相当于一次性设定了NewSize与MaxNewSize的内存大小。 | -XX:NewSize=100M |
-XX:MaxNewSize | 设置新生代的最大值 | -XX:MaxNewSize=100M |
-XX:NewRatio | 新生代(Eden+2个Surivivor空间)与老年代(不包括永久代)的比值 | -XX:NewRatio=4,表示新生代与老年代所占的比值为1:4 |
-XX:SurivivorRatio | Eden与一个Surivivor的比值大小。默认为8:1,即Eden占8/10。 | -XX:SurvivorRatio=8 |
-XX:PermSize | 设置非堆内存(方法区,永久代)的初始大小。方法区主要存放Class相关信息,如类名、访问修饰符、常量池、字段描述等。 | -XX:PermSize=10M |
-XX:MaxPermSize | 设置非堆内存(方法区,永久代)的最大值 | -XX:MaxPermSize=50M |
-XX:MaxDirectMemorySize | 设置本机直接内存大小。一般通过Unsalf类来操作直接内存。 | -XX:MaxDirectMemorySize=100M |
-XX:PretenureSizeThreshold | 设置对象超过指定字节大小时直接分配到老年代 | -XX:PretenureSizeThreshold=3145728 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保失败。 进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 | -XX:+HandlePromotionFailure |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 | -XX:ParallelGCThreads=4 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄。每次Minor GC之后,存活年龄就加1,当超过这个值时进入老年代。默认为15 | -XX:MaxTenuringThreshold=15 |
-XX:+HeapDumpOnOutOfMemoryError | 内存溢出时Dump出当前的内存堆转储快照以便事后进行分析 | -XX:+HeapDumpOnOutOfMemoryError |
-XX:+PrintGCDetails | 发生垃圾收集时打印详细回收日志, 并且在退出的时候输出当前内存区域分配情况。 | -XX:+PrintGCDetails |
XX:+PrintGCDateStamps | 输出GC时的时间戳 | XX:+PrintGCDateStamps |
XX:+PrintHeapAtGC | 在进行GC的前后打印出堆的信息 | XX:+PrintHeapAtGC |
-XX:+PrintGCApplicationStoppedTime | 输出GC造成应用暂停的时间 | -XX:+PrintGCApplicationStoppedTime |
-Xloggc | 日志文件的输出路径 | -Xloggc:./logs/gc.log |
-XX:+disableExplicitGC | 是否关闭手动System.gc | -XX:+disableExplic |
垃圾收集器关系图解:
到这里算是把目前现有的JVM的垃圾收集器介绍完了,但是还有更为细节的知识,预知后事如何,请听下回分解……
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。