JVM的垃圾回收机制

jvm的内存分为线程独享的线程栈、程序计数器;线程共享的方法区、堆区,线程栈在编译时候就确定好了大小,方法执行完成后自动就可以回收所以是静态的。而方法区、堆区只有在运行时才知道内存分配的大小所以内存回收是动态的。本章主要讨论的是堆和方法区的垃圾回收

对象是否可被回收算法

引用计数算法

给对象添加一个引用计数器,当被引用时候引用计数+1,在垃圾回收时候只要应用计数为0就可以回收对象了。算法很简单,弊端是无法解决循环应用问题。
即:

1
2
a.instance=b;
b.instance=a;

这时候a和b是无法被回收的。

可达性分析算法-java采用的方式

我们通过对象GCRoot做可达性分析,如果对象的引用链没有GCRoot对象,说明对象是可以被回收的。jvm中适合做GCRoot对象的有

  • jvm方法栈中的局部变量表中的变量
  • jvm本地方法栈中的native对象
  • 方法区中的静态变量
  • 方法区中的常量

对象的引用

对象按照应用的按照从强到弱分别如下:

  • 强引用:我们new一个对象默认就是强引用。必须等到GcRoot不可达
  • 软引用:SoftReference对象,不管内存是否足够,下一次gc都会回收该对象
  • 弱引用:WeakReference对象,当内存不足时候时候gc会回收弱引用对象
  • 虚引用:PhantomReference,又称幽灵引用,不对对象的生命周期造成影响,get()方法返回的也是null,只在对象被回收时候系统会收到一个通知。使用场景堆外内存回收。系统调用unfase方法申请一块堆外内存,然后在堆中会持有一个虚引用的对象,当该对象被回收时候会清理堆外内存

finalize方法以及对象的自救

对象被回收需要通过标记、回收两部,第一步先标记可会回收的对象,然后检查对象是否实现了finalize()方法,如果对象实现了finalize方法并且在gc时候没有被系统调用过,则认为是可finalize的,会放在一个优先级比较低的F-Queue队列中,之后会有finalize线程执行finalize()方法,如果这时候把this赋值给别的标量则完成了对象的自救。

注意:

  • 只会执行一次;
  • finalize的执行由于优先级较低即为不靠谱不推荐使用。比如在finalize方法中执行一些清理操作我们可以放在finnal中执行

回收方法区

永久代的回收效率比较低(类信息、常量、静态变量),主要回收类信息、常量

回收常量:当该常量没有任何应用

回收类信息:

  1. 没有任何该class的实例
  2. 该类的ClassLoader被回收
  3. Class没有任何访问、即通过反射创建该Class的元素

gc的算法

mark-swap 标记清理

标记对象后将需要回收的对象进行销毁、会造成内存碎片,可能导致大对象没法分配

复制算法

年轻代会采用这种算法,jvm认为大量对象是朝生夕死,所以会将内存分为Eden和俩个Survivor空间,默认比例是8:1:1,平时只会用到一个Eden和1个Survivor。当gc时候会将所有对象赋值到没用的那个Survivor空间,然后清掉Eden和原来的Survivor空间。如果剩下的Survivor不足会进入老年代。

mark-compact 标记整理

标记之后让所有对象向一端移动,超过这个范围的对象会被回收。

HotSpot的算法实现

枚举根节点

从GcRoot找引用链,该操作对时间很敏感,因为这一步操作对数据一致性要求很高,所以在执行时候虚拟机必须要StopTheWorld,否则枚举根节点的结果就不准确了。方法区和堆内存往往都很大,如果都需要分析一遍性能会严重受影响。

HotSpot采用了OopMap对象,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap,把对象偏移量内存放的是什么数据保存起来,gc就很快知道了对象的信息。

安全点 safe point

虽然OopMap在gc时候能很方便的完成枚举根节点,但是影响OopMap数据的操作很多如果每一步操作都创建OopMap会很耗费内存。为了解决这个问题HotSpot设置了safePoint的概念,只有程序执行到safePoint,才会停止线程开始gc,并且在gc前创建OopMap。即每一个safePoint会生成一个OopMap对象。

安全点选定太少,GC等待时间就太长,选的太多,GC就过于频繁。选定原则是”具有让程序长时间执行的特征“,也就是在这个时刻现有的指令是可以复用的。一般选在方法调用、循环跳转、抛出异常的位置。

现在的问题是在Safe Point让线程们以怎样的机制中断,方案有两种:抢先式中断、主动式中断。

  • 抢先式中断:GC发生时,中断所有线程,如果发现有线程不再安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。
  • 主动式中断:设置一个标志,和安全点重合,再加上创建对象分配内存的地方。各个线程主动轮询这个标志,发现中断标志为真就挂起自己。HotSpot使用主动式中断。

安全区域 safe region

如果线程没有分配cpu时间,必须线程处于sleep或blocked状态,就无法响应JVM的中断请求,走到安全点去挂起。Safe Region解决了这一问题。

安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生GC都是安全的。当代码执行到安全区域时,首先标识自己已经进入了安全区域,那样如果在这段时间里JVM发起GC,就不用管标示自己在安全区域的那些线程了,在线程离开安全区域时,会检查系统是否正在执行GC,如果是,就等到GC完成后再离开安全区域。

java的垃圾回收器

HotSpot在jdk1.7之后的所有垃圾回收器,如图:

avator

Serial垃圾回收器

最早的最基本的垃圾回收器,jdk1.3时期新生代唯一的垃圾收集器。会停止jvm所有的工作线程。

是client模式下,新生代默认的垃圾收集器,因为没有线程交互开销所以在gc过程很高效,如果新生代内存不大,收集一次的耗时是可以接受的。

ParNew收集器

Serial的多线程版本。

是server模式下首选的新生代垃圾收集器。原因是jdk1.5之后的CMS收集器除了Serial以外只有它能和ParNew配合使用。

开启CMS后,-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

在单线程或者俩个线程时候由于有线程切换的问题表现不见得比Serial好,他适合线程数或者核数多的场景。默认开启的线程数等于cpu的数量,当然我们也可以通过参数-XX:ParallelGCThreads来控制

Parallel Scavenge收集器

更关注系统的吞吐量【运行代码时间/(运行代码时间+垃圾回收时间))】适用于内部计算、和外部没有交互的服务,主要参数:

  • -XX:MaxGCPauseMillis:设置停顿时间,保证垃圾回收时间尽量不超过这个阈值,采用所少新生代空间,会导致垃圾回收更频繁
  • -XX:GCTimeRatio:垃圾收集时间占总时间的比例。
  • -XX:+UseAdaptiveSizePolicy:值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。

Serial Old收集器

Serial的老年版垃圾收集,采用“标记-整理”算法,目前主要用于配合Parallel Scavenge和作为CMS的替代方案。

Parallel Old收集器

Parallel Scavenge老年版,使用多线程和“标记-整理”算法。适合吞吐量优先的场景。

CMS收集器

大名鼎鼎的ConcurrentMarkSweep。以获取最短停顿时间为目标的垃圾回收期,试用场景希望停顿时间较短的场景。

分为四个阶段:

  1. 初始标记(CMS Initial Mark):会stop the world。标记GcRoots能直接关联的对象。只做标记所以很快;
  2. 并发标记(CMS Concurrrent Mark):不会stop the world和用户线程一起处理。去做GcRoot Tracing;
  3. 重新标记(CMS Remark):会stop the world。处理初始标记到并发标记过程中新创建的对象,比初始标记长但是这时候创建的对象很少,所以停顿时间可接受;
  4. 并发清除(CMS Concurrrent Sweep):不会stop the world和用户线程一起处理。去做对象的清理操作;

如图:

avator

默认启动线程是(CPU数量+3)/4,所以在CPU核数变高时候CMS占用的线程资源会越来越少。

缺点:

  • 对cpu敏感会占用一部分用户线程,从而cpu负载很高时候导致程序变慢。
  • CMS无法处理浮动垃圾(即:因为在回收时候会产生的新对象),有可能导致另一次GC。因为在做垃圾回收时候用户线程在工作所以要预留一部分空间给用户线程。默认是92%(since 1.6)
  • 内存碎片问题:由于标记清除会带来内存碎片问题导致明明很大的空间但是无法申请内存,这时候就要进行一次FullGC。
    • CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
    • 虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full,然后在来一次压缩的默认是0,每次进行FullGc都压缩。

G1收集器

面向服务端收集器,特点:

  • 并行与并发:充分利用多核cpu,缩短stoptheworld时间。其他收集器需要停顿的它依然可以工作。
  • 分代收集:虽然依然分代,但是g1可以不借助其他收集器独自工作。
  • 空间整合:不会产生碎片。收集后提供规整内存。
  • 可预测的停顿:用户可指定消耗在gc上的时间不超过n毫秒

    G1不在区分新生代和老年代而是将堆内存分为一些大小相等的Region,Region分为新生代和老年代并且且同一年代的Region不连续的。
    G1可预测的停顿时间的实现原理是。是内部维护了一个垃圾回收的优先级列表,针对Region内可回收的对象的大小、多少、收益能排序,优先回收收益较大的Region。

    G1是如何解决不同Region之间的相互引用的,即新生代的Region引用了老年代的Region,它是通过每个Region持有的RememberSet。当一个Refrence发生写操作时候会生成一个WriteBarrier,暂时中断写操作,如果发现他们俩个处于不同的Region,则通过CardTable将应用信息记录到被引用对象Region的RememberSet,回收时候只需要RemberSet就可以知道准确的引用关系避免了全堆扫描。

    G1的垃圾回收步骤

  1. 初始标记:标记下GcRoots能直接关联的对象,并且修改TAMS(next top at mark start)值,能让下一阶段并发标记时候在正确的region中创建值。需要停顿,但是很短。
  2. 并发标记:同cms,可以和工作线程一起运行,进行可达性分析查找存活对象,耗时较长。不需要停顿,可并行执行。
  3. 最终标记:修正初始标记到并发标记。从初始标记到最终标记这段时间内创建的对象都会记录到Remember Set Log中,合并到Remember Set中这段时间需要停顿线程,但是可并行执行。
  4. 筛选回收:根据用户设置的阈值指定回收计划。这一步理论上是不需要停顿的,但是但是停顿用户线程会增加回收效率。

如图:
avator

综述:
我们一般ParNew+CMS或者是G1的方案。

内存分配

对象默认分配到eden区,如果开启TLAB会有限直接分配在线程栈中。

大对象会直接进入老年代,对象大小超过一个PretenureSizeThreshold会直接进入老年代,

长期存活的对象会晋升入老年代,每次垃圾回收对象的age会+1,如果到了MaxTenuringThreshold(默认值是15)会直接进入老年代。对象的动态年龄规划:当survivor区相同年龄的对象超过了survivor大小的一半,那么大于等于这个年龄的对象都可以直接进入到老年代。

共同担保机制:当MinorGC发生时候,如果老年代的连续空闲内存空间超过新生代使用的对象的空间即可正常发生MinorGC,否则查看HanleProotionFailure值,如果开启,则会尝试MinorGC,失败后触发FullGC。如果关闭直接进行FullGC