第3章 垃圾收集器与内存分配策略

概述

  1. 每个栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来是就已知的。
  2. 程序计数器、虚拟机栈、本地方法栈这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而java堆和方法区则不一样。

对象已死吗

Java堆回收
  1. 主流的java虚拟机里面没有选用引用计数器算法来管理内存,其中最主要的原因是它很难解决对象之间互相循环引用的问题。
  2. 通过可达性分析算法来判定对象是否存活着,算法思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
  3. 可作为GC Roots的对象包括下面几种:
    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中JNI引用的对象
  4. java将引用分为强引用、软引用、弱引用、虚引用,引用强度逐渐减弱。
  5. 可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
  6. finalize()方法中对象逃脱死亡命运的最后一次机会。
  7. 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再次被执行。尽量避免使用它。
方法区回收
  1. Java虚拟机规范中确实说过可以不要求虚拟机在方法区(永久代)实现垃圾收集。
  2. 永久代的垃圾回收主要回收两部分内容:废弃产量和无用的类。
  3. 无用的类:
    1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何引用。
    2. 加载该类的ClassLoader已经被回收。
    3. 该类对用的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
  1. 什么是新生代、永久代、老年代
HotSpot的算法实现

以下是介绍如何发起内存回收,还没回收:

  1. 枚举根节点;
  2. 安全点中断方式:抢先式中断、主动式中断,几乎没有虚拟机采用抢先式中断;
  3. 安全区域:解决了“不执行的线程”无法到达安全点的问题。安全区域是指在一段代码片段之中,引用关系不会发生变化。
垃圾收集器(真正回收)

真正回收垃圾

  1. Serial收集器(新生代收集器,client模式下是很好的选择,单CPU下效果好)

  2. ParNew收集器:Serial多线程版本(Server模式下首选新生代收集器)

  3. Parallel Scavenge收集器(新生代):吞吐量优先的收集器

  4. Serial Old收集器(老年代)

  5. CMS收集器(老年代):以获取最短回收停顿时间为目标的收集器。步骤:

    1. 初始标志
    2. 并发标志
    3. 重新标志
    4. 并发清除
  6. G1收集器(新生代、老年代),优点:

    1. 并行与并发

    2. 分代收集(新生代、老年代)

    3. 空间整合(整体:标记-整理实现,局部复制算法实现)

    4. 可预测的停顿

      步骤:

    5. 初始标记

    6. 并发标记

    7. 最终标记

    8. 筛选回收

理解GC日志

阅读GC日志是处理Java虚拟机内存问题的基础技能。

内存分配与回收策略

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

几条最普遍的内存分配规则

  1. 对象优先在Eden分配,-Xmn10M表示10M分配给了新生代;-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1.
  2. Minor GC和Full GC有什么不一样吗?

大对象直接进入老年代

最典型的大对象就是那种很长的字符串以及数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。

长期存活的对象将进入老年代

  1. 对象晋升老年代的年龄阈值,可以通过参数:-XX:MaxTenuringThreshold设置。
  2. 1 对象优先在Eden中分配,当Eden中没有足够的空间分配时会促发一次Minor GC。每次Minor GC结束后,Eden区会清空,因为它会把Eden中还依然存活的对象放到Survivor中,当Survivor中放不下时,则由分派担保进入老年代中。2 大对象直接进入老年代中。-XX:+PretenuerSizeThreshold 控制”大对象的“的大小。即当创建的对象大于这个临界值时,则该对象直接进入老年代。3 长期存活的对象将进入老年代。虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。如果对象在Eden出生并在第一次发生Minor GC时仍然存活,并且能够被Survivor中所容纳的话,则该对象会被移动到Survivor中,并且设Age=1;以后每经历一次Minor GC,该对象还存活的话会被移动到另一个Survivor区中,并且Age=Age+1。4 动态对象年龄判定:如上所示,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保

每一次回收晋升到老年代对象容量的平均大小值最为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间。老年代空间不足会出现担保失败。

总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。