jlearning.cn

《深入理解Java虚拟机》读书笔记(三)——垃圾收集器与内存分配策略

概述

为什么要去了解垃圾收集:

  1. 当需要排查各种内存溢出、内存泄漏问题时。
  2. 当垃圾收集成为系统达到更高并发量的瓶颈时。

我们就需要对这些自动化的技术实施必要的监控和调节

判断哪些对象不可能再被任何途径使用

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,就加1,引用失效时,就减1.为0的对象就是不可能再被使用的。

很难解决对象之间的相互循环引用的问题。

1
2
objA.instance = objB;
objB.instance = objA;

这两个对象已经不可能再被访问,但是他们因为互相引用着对方,导致引用计数都不为0.

可达性分析算法

通过一个称谓“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达),证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈栈帧中的本地变量表)中引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。

引用

JDK1.2以前,引用的定义很传统:

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

JDK1.2之后,将引用的概念进行了扩充,分为:

  • 强引用(Strong Reference)
    • 代码中普遍存在,类似于Object obj = new Objext()
    • 永远不会回收
  • 软引用(Soft Reference)
    • 描述还有用但并非必需的对象
    • 在系统将要发生内存溢出之前,把这些对象列进回收范围进行二次回收。
    • 使用SoftReferencel类来实现。
  • 弱引用(Weak Reference)
    • 强度比软引用更弱一点。
    • 只能生存到下次垃圾收集发生之前,无论内存是否足够,都会回收掉只被弱引用关联的对象。
    • 使用WeakReference类来实现。
  • 虚引用(Phantom Reference)
    • 一个对象是否有虚引用的存在不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
    • 为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。
    • 使用PhantomReference类来实现

Java对象死亡过程

两次标记过程:

  1. 可达性分析,发现没有与GC Roots相连接的引用链——第一次标记并且进行一次筛选。
  2. 筛选条件是此对象是否有必要执行finalize()方法——没有覆盖或者已经被虚拟机调用过了,都视为“没有必要执行”。
  3. 如果有必要执行,把这个对象放置在F-Queue的队列之中,在售后由一个虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法,但并不承诺会等待它运行结束。
  4. 如果finalize()方法中重新与引用链上的任何一个对象建立关联,在第二次标记是将被移除出“即将回收”的集合。

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,方法不会再次执行。

finalize()方法,运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不建议使用。

回收方法区

主要回收两个部分内容:

  • 废弃的常量
    • 没有任何一个对象的值是这个常量,也没有其他地方引用了这个字面量。
  • 无用的类
    • 该类所有的实例都已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集方法

标记—清除算法

  • 首先标记出所有需要回收的对象,在标记完成后统一回收。
  • 不足:
    • 标记和清除两个过程效率都不高。
    • 标记清除之后会产生大量不连续的内存碎片。以后需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

  • 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 更清晰的讲解:如果jvm使用了coping算法,一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。 来自:http://blog.csdn.net/linsongbin1/article/details/51668859

coping算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用coping算法进行拷贝时效率比较高。不过jvm并不会根据新生代的特点,在应用coping算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。

jvm将Heap 内存划分为新生代与老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。

copying算法

  1. 当Eden去满的时候,会进行young gc,把还活着的对象拷贝到Survivor From区;、
  2. 当Survivor 0区也慢了,则把活着的对象移到Survivor To区,Survivor From被清空
  3. 当Eden区再次发生young gc的时候,直接把存活的对象复制到Survivor To区
  4. 当Survivor To区也满的时候,又会再次把存活的对象复制到Survivor From区,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),进入老年代。
  5. 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

标记—整理算法

  • 复制算法在对象存活率较高时效率会变低。考虑到所有对象都100%存活的情况,在老年代一般不能直接选用这种算法。
  • 让所有存活的对象都向一端移动。

分代收集算法

  • 在新生代中,因为每次垃圾收集都有大批对象死去,所有用复制算法。
  • 老年代对象存活率高,没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

HotSpot的算法实现

枚举根节点

  • 在全局性的引用(常量和类静态属性)和执行上下文(栈帧中的本地变量表)中。
  • 对执行时间敏感——GC进行时必须停顿所有Java执行线程。
  • HotSpot使用OopMap的数据结构,在类加载完成的时候,就把对象内什么偏移量上是什么类型的数据计算出来。
  • 在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

安全点

  • OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,将会需要大量的额外空间。
  • 在特定位置生成OopMap,这些位置成为安全点。
  • 安全点选定以程序“是否具有让程序长时间执行的特征”为标准进行选定的。
    • 方法调用
    • 循环跳转
    • 异常跳转
  • 如何在GC发生时,让所有线程(不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下载。
    • 抢先式中断
    • 主动式中断——简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域

  • 如果线程处于Sleep或者Blocked状态,无法响应JVM的中断请求。
  • 安全区域指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的。
  • 在线程执行到Safe Region中的代码时,首先标识。离开时,检查系统是否已经完成了根节点枚举,如果完成就继续执行,否则等待直到收到可以安全离开Sage Region的信号为止。

垃圾收集器

CMS收集器

  • 以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。
  • 基于标记—清楚算法实现
  • 分为四个步骤:
    1. 初始标记(Stop the World)标记一下GC Roots能直接关联到的对象
    2. 并发标记——GC Roots Tracing
    3. 重新标记(Stop the World)修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象标记记录
    4. 并发清除
  • 缺点:
    • 对CPU资源非常敏感
    • 无法处理浮动垃圾(Floating Garbage)
    • 标记—清楚算法会有大量空间碎片产生

G1收集器

特点:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿——能建立可预测的停顿时间模型,能然给使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾手机上的时间不得超过N毫秒。

步骤:

  1. 初试标记——标记一下GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。
  2. 并发标记——从GC Roots开始对堆中对象进行可达性分析,可与用户程序并发执行。
  3. 最终标记——修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。记录在Remembered Set Logs里面,合并到Remembered Set中。
  4. 筛选回收——对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。

内存分配与回收策略

  • 对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。
  • 少数情况下也可能会直接分配在老年代中。

对象优先在Eden分配

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,去你急将发起一次Minor GC。

大对象直接进入老年代

  • 大对象指需要大量连续内存空间的Java对象。
  • 最典型的大对象就是那种很长的字符串和数组。

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

动态对象年龄判定

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

空间分配担保

  • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续孔家您是否大于新生代所有对象总空间,如果成立Minor GC可以确保是安全的。
  • 如果不成立,会查看设置是否允许担保失败,如果允许那么会继续检查老年代最大可用的连续空间是否大于历次今生到老年代对象的平均大小,如果大于尝试进行一次Minor GC。
  • 如果小于,或者设置不允许冒险,改为进行一次Full GC。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。

Full GC 是清理整个堆空间—包括年轻代和永久代。