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

  • 主要思考的问题:
    • 标记-那些内存(那些死,那些活着)需要回收?
    • 什么时候回收?
    • 如何回收?
  • 内存部分分类
    • 线程独有:程序计数器,虚拟机栈,本地方法栈
    • 线程共享:堆,方法区

标记概要

标记算法

  1. 引用计数法(不能用): 每当一个地方引用它时,计数器+1,引用失效时,计数器-1,任何时刻计数器为0的对象就是不可能在被使用

java没有用最主要的原因很难解决对象之间互相循环引用的问题;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ReferenceCountingGC {
    public Object instance=null;
    public static void main(String[] args) {
        ReferenceCountingGC objA=  new ReferenceCountingGC();
        ReferenceCountingGC objB=  new ReferenceCountingGC();
        objA.instance=objB;
        objB.instance=objA;
        objA=null;
        objB=null;
    }
}
  1. 可达性分析算法:当一个对象到GC Roots没有任何引用链相连的话,则证明该对象是不可用的
  • Java 语言中,可作为GC Roots的对象包括下面几种;
    • 虚拟机栈中(栈帧中的本地变量表)的引用对象
    • 方法区中类静态属性引用对象
    • 方法区中类常量引用对象
    • 本地方法栈JNI引用的对象

再谈引用

  • 目标:描述“鸡肋”引用,即内存足够时保留,不足时抛弃

  • 引用分为四种 强,软,弱,虚四种 强度依次减弱

    • 强引用:类似Object obj=new Object() 这类引用,只要强引用还在,垃圾收集器就永远不会回收被引用的对象;
    • 软引用:用来描述一些还有用但并未必须的对象。内存溢出异常之前,会把这些对象列入回收范围之内进行二次回收。如果回收后还没有足够的内存这回OOM;
    • 弱引用:用来描述非必须的对象。若引用关联的对象只能活到下一次垃圾回收之前;
    • 虚引用:唯一目的对象被回收时收到一个系统通知

不可达对象的最后历程

总结:finalize()方法不执行或者只能执行一次

  • 不可达对象,也并非”非死不可” 这时候是在缓刑阶段。要真正宣告死亡,至少要经理两次标记过程。
  • 如果对象进行可达性分析后发现没有GC Roots相关联的引用链,会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。
  • 对象没有覆盖finalize方法(逃脱命运的最后机会),或者finalize()方法被虚拟机掉用过(只能执行一次),虚拟机将这两种情况都视为”没有必要执行”
  • 如果被判定有必要执行,那么对象会放置叫一个F-Queue的队列之中,并且稍后虚拟机自动建立Finalize线程去执行它既finalize方法
  • 但并不承诺会等待他运行结束,怕死循环或者运行缓慢。finalize方法是逃脱命运的最后机会,如果没有逃脱就真的被回收了

回收方法区(又叫HotSpot中的永久代)

永久代回收“性价比”低

永久代回收:废弃常量和无用的类

  • 什么是无用的类?

    • 无所有实例
    • 无加载该类的ClassLoader
    • 无该类的引用包括反射
  • 啥时候需要?

    • 大量使用反射,动态代理
    • CGLib等ByteCode框架
    • 动态生成JSP以及OSGi
    • 总之就是需要频繁自定义ClassLoader的场景

垃圾收集算法

标记-清除算法(基础算法,剩下的都是基于它的不足而进行改进的)

标记:标记所有需要回收的对象

清除:统一回收所有被标记的对象

不足1:效率问题,标记和清除效率都不高;

不足2:空间问题,产生大量的不连续的内存碎片

复制算法

内存容量划分两个大小相等的两块,每次使用其中的一块。这块用完了复制存活的对象到另一块,在把这块清理掉

不足:代价太高 把内存缩小为原来的一半;

现代的商用虚拟街都采用这种算法来回收新生代;

因为新生代都是朝生暮死,所以不需要1:1来划分,而将内存分为一块较大的Eden 和两个较小的Survivor,默认大小比;8:1, 每次新生代中可用的内存空间是整个新生代容量的90=(Eden+Survivor),“浪费” 10 因为没办法保证回收只有不多于10的存活,Survivor空间不够需要老年代进行担保;

标记-整理(Mark-Compact)算法(老年代常用)

标记和以前一样,后续步骤不是直接回收,而是存活对象向一端移动,然后清理边界以外的内存

分代收集

根据对象存活周期将内存划分不同的几块。一般堆分为 新生代 和老年代。这样根据年代的特点采用最适当的收集算法

  • 新生代:少量存活 选择复制算法

  • 老年代:存活率高,没有额外空间担保,必须使用 标记清理 或者标记整理;

HotSpot的算法实现

枚举根节点

可达性分析必须在一个能确保一致性的快照中进行,这导致GC进行时必须停顿所有Java执行线程(Sun称为“Stop-The-World”)。CMS收集器中,枚举根节点时也是必须要停顿的。

一致性:整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可能出现分析过程中对象引用关系还在不断变化的情况,该点不满足就无法保证分析结果准确性

由于目前的主流Java虚拟机使用的都是精准式GC,在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内对应偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

准确式GC:就是让JVM知道内存中某位置数据的类型什么。比如当前内存位置中的数据究竟是一个整型变量还是一个引用类型。这样JVM可以很快确定所有引用类型的位置,从而更有针对性的进行GC roots枚举。

安全点

GC会产生停顿(Sun也叫它 “Stop The World”),OoMap 存放着GC Roots,不是每条指令都生成一个。 不是任何时都能停下来进行 GC ,只有在 “特定的位置” 才可以GC 这个位置也叫安全点(Safepoint) 安全点的选定基本上是以程序”是否具有让程序长时间执行的特征”为标准选定的

关于安全点另一个需要考虑的就是如何在GC发生的时让所有线程都”跑”到最近的安全点上在停下来;有两种方案

  • 抢先式中断(Preemptive Suspension)(现在几乎都这种方案):不需要线程的执行代码主动配合,GC发生时候先把线程全部中断,如果有线程不在安全点,就回复线程让它跑到安全点。

  • 主动式中断(Voluntary Suspension):当GC需要中断线程的时候,不对线程造作,仅仅简单地设置一个标志位,各个线程执行的时候主动去轮询这个标志位,发现中断标志位真就挂起,轮询标志的地方安全点重合。 而对于不执行的线程,任何时间都是安全的也称为安全区;

安全区域

全点的机制似乎已经完美的解决了 “什么时候以及何时开始 GC” 的问题,但是实际情况并非如此;安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行;此时就引入了安全区的概念。

  • 安全区(Saferegion):安全区域是指在一段区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的;线程运行时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

垃圾收集器

Serial收集器

Serial:单线程收集器,在进行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束。

  1. 需要STW(Stop The World),停顿时间长。
  2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。

ParNew收集器

ParNew:是Serial的多线程版本,除了使用多线程进行垃圾收集外,其他行为与Serial完全一样

Tips:1.Server模式下虚拟机的首选新生收集器,与CMS进行搭配使用

Parallel Scavenge收集器

Parallel Scavenge:目标是达到一个可控制的吞吐量。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,并且虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略。

Serial Old收集器

Serial Old:老年代的单线程收集器,使用标记 - 整理算法,

Parallel Old收集器

Parallel Old:老年代的多线程收集器,使用标记 - 整理算法,吞吐量优先,适合于Parallel Scavenge搭配使用

CMS收集器

CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步: - 初始标记,标记GCRoots能直接关联到的对象,时间很短。 - 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。 - 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。 - 并发清除,回收内存空间,时间很长。

三个缺点: - 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降。

  • 无法处理浮动垃圾

因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。

  • 会产生大量的内存碎片,不利于大对象的分配

G1收集器

G1收集器因为没有商用的就不写了;

内存分配与回收策略

对象优先在Eden分配

  • 三个参数

    • Xms20M:初始堆
    • Xmx20M:最大堆
    • Xmn10M:新生代堆
    • -XX:SurvivorRation=8: eden:from:to = 8:1:1
  • GC类型

    • Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快。
    • Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
    • Full GC:指发生在老年代和新生代的GC,速度很慢,需要Stop The World。

大对象直接进入老年代

大对象就是大量连续内存空间的Java对象,典型的就是很长的字符串及数组。并且内存超过虚拟机设置大对象的值

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

jvm给每个对象定义一个对象年龄计数器。如果eden出生并经过第一次Minor GC后仍然存活并且能被Survivor容纳的话,将被移动到Survivor空间并将对象年龄设为1.对象在Survivor区每”熬过”一次Minor GC则年龄+1,当年龄达到一定程度(默认15岁),下一次将会被晋升老年代。

动态对象年龄判定

为了更好的适应内存状况。如果在Survivor空间中相同年龄的所有对象大小的综合大于Survivor的一半,那么大于等于这个年龄的将被一起带入老年代

空间担保分配

检查老年代最大可用空间是否大于新生代对象总大小,大于就尝试一次Minor GC,尽管有风险,小于的话并且设置不冒险就要Full GC