JVM垃圾收集器与内存分配策略

一、如何判断对象是否还在存活
  • 引用计数法:
    主流的Java虚拟机没有使用这种方法管理内存, 因为它很难解决循环依赖
  • 可达性分析:

    通过一系列的称为”GC Roots“的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链, 当一个对象到GC Roots没有与任何引用链相连时, 则证明该对象是不可用的。

作为GC Roots的对象包括以下几种: 虚拟机栈中引用的对象、 方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中JNI引用的对象。

二、引用:
  • 定义:

    如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称这块内存代表着一个引用。

  • 分类:
  • 强引用: 代码之中普遍存在的, 如Object obj = new Object(), 只要强引用还在, GC就永远不会回收该内容。

  • 软引用: 描述有用但非必须的对象。 对于软引用关联着的对象, 在系统将要抛出内存异常之前, 会将这些对象列进回收范围进行二次回收。 如果这次回收还没有足够的内存, 才会抛出异常。(SoftReference)

  • 弱引用: 弱引用也用来描述非必须的对象。被若引用关联的对象只能活到下次垃圾回收发生之前。 当垃圾收集器工作时, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。

  • 虚引用: 又称为幽灵引用或者幻影引用。 一个对象是否有虚引用的存在, 丝毫不会影响对象的生存时间, 也不能通过虚引用获得对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。

三、对象标记之后就会回收吗
  • 可达性分析之后, 没有在任何GC Roots引用链上的对象, 就会被第一次标记, 标记之后还有一次筛选的过程;
  • 筛选的条件是: 此对象是否有必要执行finalize方法, 有必要执行的条件是: 该对象覆盖了finalize方法, 并且没有虚拟机调用过, 也就是说任何一个对象的finalize方法都只会被系统执行一次。
  • 如果有必要执行finalize方法, 该对象则将会被放置一个成为F_Queue的队列之中, 并在稍后由一个有虚拟机自动建立的、低优先级的Finalizer线程去触发该方法。但不会等待finalize执行结束。
  • finalize方法是对象逃脱死亡命运的最后一次机会。稍后GC将对F_Queue中的对象进行第二次小规模的标记, 如果能与引用链上任何一个对象建立关系, 对象就不会被再次标记, 从而活下来。
  • 注: 如果没有必要执行finalize对象, 是不是就会立即被GC 回收呢
四、方法区的回收
  • 在堆中, 尤其是新生代中, 常规应用进行一次垃圾收集一般可以回收70%~95%的空间, 而永久代的垃圾收集效率远低于此。
  • 永久代中的垃圾收集主要回收两部分内容: 废弃常量和无用的类
  • 废弃常量: 当前系统中没有任何一个对象引用该常量。
  • 无用的类: 该类所有的实例都已被回收(Java堆中不存在该类的任何实例)、加载该类的ClassLoader被回收、该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。
五、垃圾收集算法
1、标记-清除算法:

首先是标记出所有需要回收的对象, 在标记完成后回收所有被标记的对象。

缺点:

  • 效率问题: 标记和清楚两个过程的效率都不高
  • 空间问题: 标记清除之后会造成大量不连续的内存碎片, 空间碎片太多导致需要分配较大对象时, 无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
2、复制算法:
  • 描述: 将可用内存按容量划分为两块, 每次只是用其中的一块, 当这一块内存用完了, 就将还存活的对象复制到另外一块内存, 然后再把已使用过的内存空间一次性的清理掉。
  • 优点: 不用考虑内存碎片
  • 缺点: 内存缩小为原来的一半
  • 应用: 目前的商业虚拟机都采用这种收集算法来回收新生代, 具体如下:

    将内存分为一块较大的Eden空间和两块较小的Survivior空间, 每次使用Eden和其中一个Survivior空间。

    当回收时, 将Eden区和Survivior中还存活着的对象一次性的复制到另外一块Survivior空间上, 最后清理Eden区和另一块Survivior区。

    Hotspot 虚拟机默认Eden和Survivior的大小比例是8:1

    当另外一块Survivior空间没有足够的空间存放上一次新生代存活的对象时, 这些对象将直接通过分配担保机制进入老年代。

3、标记-整理算法:

标记之后, 让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。

4、分代收集算法:

当前虚拟机的垃圾收集都采用”分代收集“算法。一般是把Java堆分为新生代和老年代;

新生代: 每次垃圾收集时都会有大批对象死去, 只有少量存活, 该区域采用复制算法。

老年代: 对象存活率较高, 而且没有额外空间对她进行分配担保, 就必须使用”标记-清理“或者”标记-整理“算法进行回收。

六、HotSpot 的算法
1、枚举根节点:

GC停顿:

可达性分析要确保在一个一致性的快照中进行, 确保分析过程中引用关系不再变化。 GC进行时, 必须停顿所有的Java执行线程。 Stop the World

如何枚举根节点:

GC Roots主要在全局性的引用, 如常量、类静态属性, 与执行上下文中, 如果逐个检查, 必然消耗大量的时间。

使用OopMap: HotSpot使用一组OopMap来记录对象的引用, 加快GC Roots的枚举。

2、安全点:

背景: 如果每个指令都生成对应的OopMap, 那会需要大量的额外空间, 这样GC的成本将会变得非常高。

解决办法: 只在特定位置记录对象的引用情况, 这些特点的位置我们称之为安全点。

安全点的选定条件: 是否具有让程序长时间执行的指令 (原因)

如何保证GC时, 让所有线程都跑到最近的安全点上再停顿下来:

  • 主动式中断:

GC需要中断线程的时候, 不直接对线程操作, 而是简单的设置一个标志, 而是在执行到安全点时轮训该标志, 如果标志为真就自己中断挂起。

  • 抢先式中断:

GC发生时, 首先把所有的线程全部中断, 如果发现有线程中断的地方不在安全点上, 就恢复线程, 让他”跑“到安全点上。

现在几乎没有虚拟机采用这种方式来暂停线程以响应GC事件

3、安全区域:
  • 背景: 安全点机制保证了程序执行时, 在不太长时间就会遇到可进入GC的安全点, 如果程序没有执行呢, 比如处于sleep或者blocked状态,

    这时候线程无法响应jvm的中断请求, Jvm也显然不太可能等待线程被重新分配CPU时间。

  • 安全区域: 在一段代码之中, 引用关系不会发生变化, 在这个区域中任意地方开始GC都是安全的

  • 实现: 当线程执行到安全区域后, 首先会标示自己进入了安全区域, 那样, 当在这段时间内发生GC时, 就不用管这样的线程了, 当线程要离开

    该区域时, 要检查系统是否已经完成了根节点枚举, 如果没完成, 它就必须等待直到收到可以安全离开安全区域的信号。

七, 垃圾收集器

目前新生代垃圾收集器有Serial, ParNew, Parallel Scavenge; 老年代收集器有CMS, Serial Old, Parallel Old; G1这款垃圾收集器既能用于新生代又能用于老年代。

1, Serial收集器:

描述: 单线程收集器; 他进行垃圾收集时, 必须暂停所有的工作线程, 直到它收集结束; 新生代采取复制算法暂停所有用户线程, 老年代采取标记-整理算法暂停所有用户线程。

现状: 目前为止, 依然是虚拟机运行在Client模式下的默认新生代收集器。收集几十兆甚至一两百兆的新生代, 停顿时间完全可以控制在几十毫秒最多一百毫秒以内。

2, ParNew收集器:

描述: 其实就是Serial的多线程版本; 使用多线程进行垃圾收集; 新生代采取复制算法暂停所有用户线程, 老年代采用标记-整理算法暂停所有用户线程。

现状: 许多运行在Server模式下的虚拟机默认的新生代收集器; 一个与性能无关的原因是: 除了Serial收集器外, 目前只有它能与CMS收集器配合使用。

注:

  • ParNew收集器是使用-XX: +UseConcMarkSweepGC选项后的默认新生代收集器; 也可以使用-XX: UseParNewGC指定它
  • 可以使用-XX: +ParallelGCThreads参数限制垃圾收集的线程数

3, Parallel Scavenge收集器:

  • 描述: Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是cpu用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 参数:

最大垃圾收集停顿时间设置: -XX: MaxGCPauseMillis, 设置值是一个大于0的毫秒数, 收集器将尽可能地保证内存回收花费的时间不超过设定值。

GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的, 可能会把新生代调小一些, 以使在规定的时间内可以完成垃圾回收; 也可能为了减小停顿时间而增大GC频率。

-XX:+GCTimeRatio:设置一个大于0且小于100的整数值, 也就是垃圾收集时间占总时间的比率, 如果设置成x, GC时间的占比就是 1/(1+x)

-XX: +UseAdaptiveSizePolicy: 这是一个开关参数, 打开这个参数后, 不需要手工指定新生代大小, Eden和Survior区的比例, 晋升老年代对象年龄等细节参数。 虚拟机会根据当前系统的运行情况动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。

与ParNew收集器的区别: 可以设置吞吐量和最大停顿时间; 具有自适应调节策略。

4, Serial Old收集器:

描述: Serial Old是Serial收集器的老年代版本, 单线程收集器, 使用“标记-整理”算法。

5, Parallel Old收集器:

描述: Parallel Scavenge收集器的老年代版本; 吞吐量优先的收集器

6, CMS收集器: Concurrent Mark Sweep

描述: 以获取最短回收停顿时间为目标; 基于“标记-清除”算法 步骤:

  • 初始标记: 标记GC Roots能直接关联到的对象, 速度很快; 该阶段需要stop the world
  • 并发标记: 进行GC Roots trace, 并发进行
  • 重新标记: 修正并发期间因为用户程序继续运行而导致变动的那一部分对象的标记记录。
  • 并发清除:

缺点:

  • CMS收集器对CPU资源非常敏感, 它虽然不会导致用户线程停顿, 但是由于占用了一部分cpu时间而导致应用变慢, 总吞吐量会降低。

  • CMS收集器无法处理浮动垃圾, 可能出现Concurrent Mode Failure失败, 导致另一次full gc的产生。并发清理过程中, 用户线程生成的垃圾不能被本次回收所清理, 只能等到下次GC时清理, 这部分垃圾称为“浮动垃圾”; 每次垃圾清理时, 都要为用户线程的运行留出内存空间, 所以不能等到没有空间时才回收。如果CMS运行期间预留的内存空间无法满足程序需要, 就会出出现一次“Concurrent Mode Failure”失败, 这时虚拟机将启动后背预案: 临时使用Serial Old收集器重新进行老年代的垃圾收集; 使用-XX: CMSInitiatingOccupancyFraction设置阈值

  • CMS是基于“标记-清理”算法实现的收集器, 意味着收集结束时会产生大量的空间碎片。 空间碎片过多时, 会因为无法找到足够连续的内存而无法为大对象分配内存, 造成FULL GC

八、 理解GC日志

1.停顿类型:

[GC : minor GC, [Full GC: full GC

2.GC的位置:

[DefNew: Default New Generation Serial收集器新生代 [ParNew: Parallel 新生代 [PSYoungGen: Parallel Scanvenge收集器的新生代 [Tenured: 老年代 [Perm: 永久代

3.回收前后内存空间变化:

35592K -> 1814K(36288K): 回收前内存空间大小 -> 回收后内存空间大小(总的内存空间大小

九、内存分配与回收策略:

大的方向说, 对象主要分配在堆的新生代的Eden区上, 如果启动了本地线程分配缓冲, 将按线程优先在TLAB上分配。

  1. 对象优先在Eden上分配:
    当Eden上没有足够的空间分配时, 虚拟机会发起一次Minor GC, 将Eden上和一个survivior上存活的对象复制到另外一个Survivor空间上, 如果另外一个Survivior空间上没有足够的空间, 将会将存活的对象直接移动到老年代, 如果老年代也没有足够的空间, 虚拟机将会发起一次Full GC, 如果Full GC之后还是放不下, 则会报OOM异常。

  2. 大对象会直接进入老年代:
    虚拟机提供一个参数, -XX:PretenureSizeThreshould, 大于这个值的对象直接在老年代分配, 避免Eden区域Survivior区的来回复制。

  3. 长期存活的对象直接进入老年代:
    虚拟机每个对象定义了一个对象年龄计算器, 每经过一次Minor GC, 对象年龄加一, 当对象年龄达到一定数时(默认15), 将会晋升到老年代。 阈值设置参数: -XX: MaxTenurngThreshould

  4. 动态对象年龄判断:
    如果Survivior空间中相同年龄的对象的大小总和大于survivior空间的一半时, 大于等于该年龄的对象就可以直接进入老年代。

  5. 空间分配担保:
    准备Minor GC时, 虚拟机首先会检查老年代的剩余空间是否大于新生代所有对象的总空间, 如果大于, 则可以进行Minor GC; 否则, 会去查看是否允许担保失败(HandlePromotionFailure), 如果不允许, 虚拟机会直接发起一次Full GC; 如果允许, 虚拟机会去检查老年代剩余空间是否大于历次晋升到老年代对象的平均大小, 如果不大于, 则会发起一次Full GC; 如果大于, 则会发起Minor GC, 如果这时发现, 老年代没有足够空间来容纳新生代晋升来的对象的总大小, 这时仍要触发一次Full GC, 这个圈子绕的就有点大了。

十、Full GC 和Minor GC:

Minor GC: 发生在新生代, 速度比较快

Full GC: 发生在老年代, 一般都伴随这一次Minor GC

Full GC一般都要比Minor GC慢十倍以上, 因为新生代采用复制算法, 速度比较快; 而老年代一般采用标记-清理/整理算法;

本文转自 JVM垃圾收集器与内存分配策略 - 王颜培的个人空间 - 开源中国社区
常量池、perm(持久代)、方法区、栈
JVM内存管理:深入垃圾收集器与内存分配策略