Garbage Collection 垃圾收集(垃圾回收)
回收java无用的对象
不回收会导致内存泄露
1960年 List 使用了GC
Java中,GC的对象是堆空间和永久区
GC的基本原理:
将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况
使用者
引用计数法实现
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用,该对象才会被回收。
引用计数法的问题
引用计数法引起的内存泄露
复制 (Coping)
标记-清除 (Mark-Sweep)
标记-压缩(Mark-Compact)
分代收集算法(Generational Collection)
java采用。通过一系列名为“GC root”的对象作为起点,从这些点开始向下搜索,搜索走过的路劲叫做引用链。当一个对象到GC root没有任何引用链时,则证明此对象是不可用的。这种算法也叫也叫引用链法/可达性算法。
核心流程
第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活
Gc ROOT
核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。新生代的内存空间通常都是所有代里最大的,适用复制算法。
很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片
复制算法优点
没有标记和清除的过程,效率高,没有内存碎片,可以利用 Bump-the-pointer(指针碰撞)技术实现快速内存分配,因为已用和未用的内存各自一边,内存分布规整有序,当新对象分配时就可以通过修改指针偏移量将其分配在第一个空闲的内存位置上,从而快速分配内存,否则只能使用空闲列表(Free List)方式分配内存
复制算法缺点
开辟专门的空间存放存活对象,占用更多的内存。
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。
因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。
在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。
在清除阶段,清除掉所以的未被标记的对象。
标志清除算法优点
标志清除算法缺点
核心流程
一样是从从根集合开始扫描,对存活动对象进行标记,然后重新扫描整个内存空间,并往一个方向移动存活对象,虽然移动对象的消耗时间,但不产生内存碎片,可以通过 Bump-the-pointer(指针碰撞)快速分配内存。
应用场景
标记压缩算法的应用场景是老年代,说老年代执行GC效率低,可用对象重排整理是主要原因。
年轻代:
年老代:
持久代:
SUN的jvm内存池被划分为以下几个部分:
综上 jvm的内存回收过程是这样的
对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。
当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。
若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作。
Serial Coping(串行复制),Parallel Scavenge(并行复制),ParNew(并发复制)这三种回收器都是基于复制算法,复制 young eden 和 young from 中还存活的对象到 young to,或者根据设定对象的大小和 GC 次数直接晋升到 old,清空 young eden 和 young from 中的垃圾,下一次 GC 发生交换 young from 和 young to,只可使用于新生代,是在 young eden 内存空间不足以分配给对象时触发 Minor GC(新生代垃圾回收)。
Serial Old (串行标记-清理-压缩)
Parallel Old(并行标记-压缩)
CMS Concurrent Mark-Sweep(并发标记清除)
Serial Coping(串行复制)
适合客户端工作,不适合在服务器运行,针对单 CPU,小新生代,不太在乎暂停时间的应用,可通过- XX:+UseSerialGC手动指定新生代使用 Serial Coping(串行复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。
ParNew(并发复制)
是 Serial Coping(串行复制)的多线程版本,在多 CPU 核情况下可以提高收集能力,但如果是单 CPU 条件下,还要来回切换任务,不一定比 Serial Coping(串行复制)收集能力强,通过- XX:+UseParNewGC手动指定新生代使用 ParNew(并发复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。
Parallel Scavenge(并行复制)
跟 ParNew(并发复制)相比更注重于吞吐量而不是低延迟,如果吞吐量优先,必然会降低 GC 的频次,也就造成 GC 回收垃圾量更多、时间更长。如果低延迟优先,为了降低每次的暂停时间,就得高频的回收,这频繁的回收又会导致吞吐量的下降,所以吐吞量和低延迟是对矛盾体,适合多 CPU、高 IO 密集操作、高计算消耗的应用,通过XX:+UseParallelGC手动指定新生代使用 Parallel Scavenge(并行复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。
Serial Old (串行标记 - 清理 - 压缩)
单线程串行回收,停顿时间长,可以使用
查看暂停时间,适合客户端使用,不会产生内存碎片
Parallel Old(并行标记 - 压缩)
根据 GC 线程数划分若干区域(Region),并行做标记,重新扫描,定位到需要压缩的 Region,统计 Region 里所有存活对象的下次要移动的目的地地址,然后并行的往一端压缩,不产生内存碎片,整理后的空闲区域是连续的,通过- XX:+UseParallelOldGC手动指定新生代使用 Parallel Scavenge(并行复制)收集器,老年代使用 Parallel Old(并行标记 - 压缩)收集器执行内存回收。
CMS Concurrent Mark-Sweep(并发标记清除)
第一阶段是初始标记,需要 Stop-the-world,这阶段标记出那些与根对象集合所连接的不可达的对象,标记完就会被暂停的应用线程;
第二阶段是并发标记,这阶段是应用线程和回收线程交替执行,把第一步标记为不可达的对象标记为垃圾对象,由于是交替进行,一开始被标记为垃圾的对象,后面应用线程可能更改对象的引用关系导致标记错误;
所以第三阶段重新标记,需要 Stop-the-world,修正上个阶段由于对象引用或者新对象创建导致的标记错误,这阶段只有回收线程执行,确保修正的正确性。
经过三个阶段的标记,第四个阶段会并发的清除无有的对象释放内存,这阶段是应用线程和回收线程交替执行,如果用户应用线程产生了新的垃圾(浮动垃圾),只能留到下次 GC 进行回收,极端情况如果产生的新的垃圾,而老年代的预留空间又不够,就会产生 Concurrent Mode Failure,这个时候只能通过后备的 Serial Old (串行标记 - 清理 - 压缩)来进行垃圾回收。
又因为 CMS 并没有用到压缩算法,回收后会产生内存碎片,为新对象分配内存无法使用 Bump-the-pointer(指针碰撞)技术实现快速内存分配,只能使用空闲列表(Free List :JVM 会维护一张可用内存地址的列表,当需要分配空间,就从列表搜索一段和对象大小一样的连续内存块用于存放要生成的对象实例)方式分配内存。
但也可以通过- XX:CMSFullGCsBeforeCompaction,用于指定经过多少次 Full GC 后对内存碎片整理压缩,由于内存碎片不是并发执行,会带来更长的停顿时间,通过- XX:+UseConcMarkSweepGC设定新生代使用 ParNew(并发复制)收集器,老年代使用 CMS Concurrent Mark-Sweep(并发标记清除)收集器执行内存回收,当出现浮动垃圾导致 Concurrent Mode Failure 或者新对象分配内存失败时,通过备用组合新生代使用 ParNew(并发复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收,适用于要求暂停时间短,追求快速响应的应用,如互联网应用。
JVM回收需要注意的点:
在执行 Minor GC 的时候,JVM 会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小,如果大于,则直接执行 Minor GC;
如果小于了,JVM 会检查是否开启了空间分配担保机制;如果开启了,则 JVM 会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小;
如果大于则会执行 Minor GC,如果小于则执行改为执行 Full GC,如果没有开启则直接改为执行 Full GC。
当老年代(Major GC)和永久代发生 GC 时,除了 CMS 外都会触发 Full GC,Full GC 就是先按新生代 GC 方式进行 Minor GC,再按照老年代的配置进行 Major GC,包含对老年代和永久代进行 GC,若 JVM 估计 Minor GC 会产生晋升失败,则会采用 Major GC 的配置进行 Full GC。
如果 Minor GC 执行失败则会执行 Full GC。
吞吐量:应用运行时间/总时间,暂停时间:每次 GC 造成的暂停
分区分代增量式式收集器:G1(Garbage-First)收集器
传统的分代收集也提供了并发收集,但最致命的是分代收集把整个堆空间划分成固定间隔的内存块,每次收集都很容易触发 Full GC 从而扫描整个堆空间,这会拖慢应用,而且要对整个堆空间都做内存碎片整理会很麻烦。
而增量式的收集方式是一种新的收集思想,增量收集把堆空间划分成一系列的小内存块(内存块大小可配置),使用时只使用部分内存块,等这部分内存块空间不足时,再把存活对象移动到未被使用过的内存块,避免整个堆用完了再 Full GC,可以一边使用内存一边收集垃圾。
G1 收集器将整个 Java 堆区分成约 2048 大小相同的 Region 块(分新生 Region 块、幸存 Region 块、老年 Region 块),Region 块大小在 1MB 到 32MB 之间,每个对象会被分配到 Region 块里,既可以被块内对象引用也可以被块外对象引用,在判断对象是否存活时,为了避免全堆扫描或者遗漏,是通过 Remembered Set 来检查 Reference 引用的对象是否存在不同的 Region 块中的。G1 在收集垃圾时,会对各个 Region 块的回收价值和成本做排序,根据用户配置的期望停顿时间来进行回收。
G1 收集器与 CMS 收集器执行过程类似。初始标记阶段,Stop-the-World,标记 GC Roots 可直接访问到的对象,为下一个阶段并发标记时,和应用线程交替执行时,有正确可有的 Region 来分配新建对象,并发标记阶段识别上个阶段标记对象的下层对象的活跃状态,找出存活的对象,也就是标记 GC Roots 可达对象;
最终标记阶段,Stop-the-World,修正上次线程交替执行产生的变动;
清除复制阶段,Stop-the-World,这阶段并不是最终标记执行完了就一定执行,毕竟是要 Stop-the-World,为了达到准实时(可配置在 M 毫秒内最多只占用 N 毫秒的时间进行垃圾回收)会根据用户配置的 GC 时间去决定是否做清除。
还有,因为清除复制阶段使用的是复制算法,每次清理都必须保证”to space” 空间是足够的(将存活的对象复制到未使用的 Region 块),所以只有已用空间达到了(1-h)*堆大小(h 是 G1 定义的一个堆空间的百分比阈值,是个固定值)才执行清除,把存活的对象往一个方向移动到”to space” 并整理内存,不会产生内存碎片。
接着把”Eden space” “from space” 的垃圾对象清理,根据维护的优先列表,优先回收价值最大的 Region,通过五个阶段完成垃圾收集,可以通过设定 - XX:UseG1GC 在整个 Java 堆使用 G1 进行垃圾回收,G1 适合高吞吐、低延时、大堆空间的应用。
什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC(monor gc)
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。
这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。full GC会影响系统短暂性停歇。
有如下原因可能导致Full GC:
1、System.gc()方法的调用
系统建议执行Full GC,但是不必然执行
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
2、老年代代空间不足
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
3、永生区空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
4 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
6、堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。