《深入理解Java虚拟机》三:垃圾收集器与内存分配策略

本章的主要问题是

哪些内存需要回收?
什么时候回收?
如何回收 ?

大纲

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

哪些内存需要回收?

  • Java堆:这部分内存的分配和回收都是动态的,只有在成员运行时才知道要创建哪些对象,因此,垃圾收集器主要所关注需要回收的就是这部分的内存;
  • Java运行时的其他数据区域,如程序计数器,虚拟机栈,本地方法栈个区域随线程而生 ,随线程而灭,不需要过多考虑回收的问题;
  • 方法区也有回收,只是相比Java堆”性价比“比较低,在Java虚拟机规范中,不要求实现垃圾回收。

判断对象是”存活”还是”死去”

判断一个对象是否存活,通常使用两种算法:

  • 引用计数算法
  • 可达性分析算法(主流商用程序语言(Java,C#)的主流实现)

引用计数算法

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

优点:实现简单 ,判定效率也很高
缺点:很难解决对象之间相互循环引用的问题,主流Java虚拟机没有使用到

可达性分析算法

通过一系列的称为”GCRoots”的对象作为起始3点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(ReferenceChain),当一个对象到GCRoots没有任何引用链相连(用图论的话来说,就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。

可作为GCRoots的对象包括:

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

不可达的对象不一定就是”非死不可“,真正死亡至少需要经历两次标记过程:
第一次:在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
第二次:有必要执行finalize()方法,那么这个对象将会放置在一个叫做FQueue的队列之中,稍后GC将对FQueue中的对象进行第二次小规模的标记。如果在finalize()中,对象重新与引用链上的任何一个对象建立关联,则可以自救逃脱”死亡“。

”有必要执行“,当对象覆盖finalize()方法且finalize()方法没有被虚拟机调用过。
如果没有被覆盖finalize()或者finalize()已经被调用过了,那么,对象就必定”死亡“。finalize()只会被调用一次,如果面临下一次回收,就没办法自救了。

引用

在JDK1.2之前,一个对象只有被引用和没有被引用,在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为下面4种,引用强度依次逐渐减弱。

  • 强引用(StrongReference)
    强引用就是指在程序代码之中普遍存在的,类似”Objectobj=newObject()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用(SoftReference)
    软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用(WeakReference)
    弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用(PhantomReference)
    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

回收方法区

主要回收内容

  • 废弃常量
    没有地方引用的常量
  • 无用的类
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

如何回收?— 垃圾收集算法

标记清除算法

标记出所有需要回收的对象 ,完成后统一回收

缺点

  • 效率问题 ,标记和清除两个过程的效率都不高 ;
  • 空间问题 ,清除之后会产生不连续的内存碎片,导致需要分配大对象时又要再次触发垃圾收集

复制算法

现在的商业虚拟机都采用这种算法收集新生代
如何解决标记清除算法中的空间问题:

  • 它将可用内存按容量划分为大小相等的两块 ,每次只使用其中的一块 。
  • 当这一块的内存用完了 ,就将还存活着的对象复制到另外一块上面 ,然后再把已使用过的内存空间一次清理掉 。
  • 由于大多数新生代对象都是”朝生夕死“,不需要1:1分配,而是把内存分为一块Eden空间和两块较小的Survivor空间。
    HotSpot虚拟机默认按Eden:Survivor=8:1分配。

标记-整理算法

为了解决对象存活率高复制频繁且没有额外空间进行担保,如老年代
标记过程与标记清除算法一样,清除不是直接全部复制,而是存活对象统一移到一端,去填空被回收出来的空间,再把端边界清除掉。

分代收集算法

当前商业虚拟机的垃圾收集都采用的算法。
把Java堆分为新生代和老年代,则可以根据他们的特点选择不同的算法

  • 新生代:复制算法
  • 老年代:标记清除算法或标记整理算法

HotSpot的算法实现

上面只是理论介绍了算法,实际实现的时候,需要考虑效率问题,如引用太多导致可达性分析的时候,检查太耗费时间。

实际上,没有检查全部引用,而是使用一组OopMap的数据结构,在特定的位置中记录那些引用,特点位置成为安全点(Safepoint),并且设置中断挂起的标志。只有程序执行到安全点才暂停执行GC。

如何在GC发生时让所有线程在最近的安全点停顿呢?

  • 抢先式中断:发生GC时,先中断所有线程,再检查是否中断在安全点,不在则回复让其跑到安全点停顿。现在几乎没有虚拟机使用
  • 主动式中断:设置标志,让线程去轮训该标志,标志位为空则自动中断挂起。

对于不执行的线程,则没办法响应中断请求,跑到安全点,这种情况就需要安全区域来解决。

安全区域:指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把安全区域看做是被扩展了的安全点。

垃圾收集器

下图为基于JDK 1.7 的HotSpot虚拟机,收集器之间存在连线,则说明可以搭配使用。新生代和老年代,采用不同的收集器。

  • 并行收集(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发收集(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Serial收集器

  • 最基本、发展历史最悠久的收集器,是一个单线程的收集器,进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
  • 简单高效。

ParNew收集器

  • 新生代
  • ParNew收集器其实就是Serial收集器的多线程版本,并行收集。
  • 除了Serial收集器外,新生代目前只有它能与老年代的CMS收集器配合工作。

Parallel Scavenge收集器

  • 新生代
  • 也是使用复制算法的收集器,又是并行的多线程收集器。
  • 关注点是达到一个可控制的吞吐量,而CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。
    • 高吞吐量适合在后台运算不需要太多交互的任务
    • 停顿时间越短,收集频率越高,吞吐量越小
  • 具备ParNew收集器没有的GC自适应的调节策略,通过-XX:+UseAdaptiveSizePolicy开关控制打开,就不需要用户自行设置各种参数。

Serial Old收集器

  • Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器
  • 使用“标记整理”算法
  • 用途:
    • 主要意义在给Client模式下的虚拟机使用
    • 在Server模式下,那么它主要还有两大用途:
      • 在JDK1.5以及之前的版本中与ParallelScavenge收集器搭配使用
      • 作为CMS收集器的后备预案,在并发收集发生ConcurrentModeFailure时使用

Parallel Old收集器

  • Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记整理”算法
  • 搭配Parallel Scavenge,“吞吐量优先“收集器组合

CMS收集器

  • CMS(ConcurrentMarkSweep)收集器是一种以获取最短回收停顿时间为目标的收集器,并发收集,低停顿
  • 基于“标记—清除”算法实现的,过程如下
    1. 初始标记(CMSinitialmark)
    2. 并发标记(CMSconcurrentmark)
    3. 重新标记(CMSremark)
    4. 并发清除(CMSconcurrentsweep)
  • 缺点
    1. 对CPU资源非常敏感
    2. 无法处理浮动垃圾
    3. 收集结束时会有大量空间碎片产生

G1收集器

  • 当今收集器技术发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器
  • 新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合
  • 基于“标记—整理”算法实现,过程如下:
    1. 初始标记(InitialMarking)
    2. 并发标记(ConcurrentMarking)
    3. 最终标记(FinalMarking)
    4. 筛选回收(LiveDataCountingandEvacuation)
  • 特点
    1. 并行与并发
    2. 分待收集
    3. 空间整合
    4. 可预测的停顿

分析GC日志

当发生GC的时候,通过分析GC的日志可以知道其执行结果

内存分配和回收策略

Java中的自动内存管理,就是解决两个问题:

  1. 给对象分配内存
    大的方向分析,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动本地线程分配缓存,将按线程优先在TLAB上分配。
  2. 回收分配给对象的内存

内存分配规则

在使用Serial/Serial Old(ParNew/Serial Old)收集器下内存分配和回收的策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间分配担保

公众号:亦袁非猿

欢迎关注,交流学习