深入理解JVM二:垃圾收集器与内存分配策略

一、概述

垃圾收集器(Garbage Collection, GC), 大部分人把这项技术当做Java语言的伴生产物,实际上,GC的历史更加久远。
在Java语言中,线程隔离的三个区域内的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为会随着线程的结束而回收。Java堆和方法区则不一样,GC关注的也是这部分内存。

二、判断对象是否存活

2.1 引用计数法

给对象添加一个引用计数器,每当一个地方引用它,计数器数值加1;当引用失效时,计数器值就减1;计数器数值为0的对象就是不可能被使用的。实现简单,效率高(python使用)。主要缺点:很难解决对象之间互相循环引用的问题。

2.2 可达性分析算法

在主流的商用语言(java,C#)通过可达性分析来判定对象是否存活。通过 GCRoos对象作为起点,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。
在Java语言中,可作为GC Roots的对象包括下面几种:

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

2.3 引用的分类

这四种引用强度依次减弱

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

2.4 生存还是死亡

至少经历两次标记过程,才会被回收
对象的finalize()方法只会被系统调用一次,下一次回收则不会执行

2.5 回收方法区

方法区(HotSpot虚拟机中的永久代)中的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与Java堆中的对象非常类似。
而满足下面3个条件才能算是“无用的类”:

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

虚拟机可以对满足上述3个条件的无用类进行回收,并不必然。

三、垃圾收集算法

介绍几种算法的思想及发展过程

3.1 标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:

  • 效率问题,标记和清除的两个过程的效率都不太高
  • 空间问题,标记清除后会产生大量不连续的内存碎片,会导致当需要分配较大对象时,无法找到连续的内存而触发另一次垃圾收集动作。

3.2 复制算法

将内存分成大小相等的两块,每次只使用其中的一块,但这一块用完了,就将还存活的对象复制到另一块,然后将已使用的空间一次清掉。这样就解决了内存碎片的问题,但内存缩小了一半。
现在商用虚拟机,一般分 Eden和两个survivor(8:2)

3.3 标记-整理算法

过程与标记-清除一样,但后续不是直接对回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理边界以外的内存。

3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

四、HotSpot的算法实现

这部分介绍如何发起内存回收

4.1 枚举根节点

可达性分析,会导致GC进行时会停顿所有Java线程。准确式GC,并不需要一个不漏检查所有引用位置,虚拟机有办法直接得知哪些地方存放着引用(OopMap)

4.2 安全点(Safepoint)

程序到达该点后,才会暂停开始GC

  • 抢先式中断
    现在几乎没有这种方式
  • 主动式中断
    设置标志,当需要暂停线程时,标志置位,线程执行到test(test指令是HotSpot生成的轮询指令)指令时就会触发中断

4.3 安全区域

安全点似乎已经完美地解决了如何进入GC的问题,但实际情况却不一定。在程序不执行的时候,系统没有为该线程分配CPU,典型的例子就是线程处于Sleep状态,这时候无法主动走到安全点去中断。
安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,可以看做是拓展了的安全点。
在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区;当JVM开始GC的时候,就不用管这些线程了。当线程要离开安全区时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),如果没有完成,则需要等待完成之后,线程才可以安全离开安全区。

五、垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器该如何实现并没有任何规定,因此不同的虚拟机的垃圾收集器可能会有很大差别。垃圾收集器没有最好的,只有最合适的。

5.1 Serial收集器

新生代 采取复制算法
最基本,发展历史最悠久的收集器。顾名思义,是一个单线程收集器,在进行垃圾收集时,会暂停所有工作线程(Stop The World)。HotSpot虚拟机开发团队为消除因垃圾回收导致的停顿而努力,开发出了很多垃圾收集器,时间的确在缩短,但没办法完全消除。
简单而高效,对于运行在Client模式下的虚拟机是一个很好的选择。

5.2 ParNew 收集器

新生代 复制算法
Serial收集器的多线程版本,它是除Serial之外,唯一能和CMS收集器配合工作的

5.3 Parallel Scanvenge收集器

新生代 复制算法,并行多线程。
该收集器的特点是以吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))为目标

5.4 Serial Old 收集器

老年代 标记-整理算法
主要意义给Client模式下的虚拟机使用

  • 与Parallel Scanvenge搭配使用
  • 作为CMS收集器的后备预案

5.5 Parallel Old收集器

老年代 标记-整理算法,使用多线程
可以替代Serial Old 与Parallel Scanvenge搭配使用

5.6 CMS (Concurrent Mark Sweep)收集器

老年代 标记-清除
是一种以获取最短回收停顿时间为目标的收集器,应用在互联网站或者B/S系统的服务端上。
分为四个步骤:

  • 初始标记
    标记一下GC Roots能直接关联到的对象,速度快,需要停顿
  • 并发标记
    GC Roots Tracing,时间长,无需停顿
  • 重新标记
    修正并发标记期间用户程序产生的变动,需要停顿,比初始表记时间稍长
  • 并发清除
    无需停顿

是一款优秀的收集器,称之为并发低停顿收集器,但是有三个明显的缺点

  • CMS收集器对CPU资源非常敏感,会占据一定的CPU资源
  • 无法处理浮动垃圾(并行程序运行时产生的垃圾),会出现Concurrent Mode Failure而导致另一次Full GC的产生,也可以临时启动 Serial Old收集器来重新处理老年代垃圾的收集。
  • 基于标记清除的算法会导致空间碎片的产生

5.7 G1(Garbage-First)收集器

是当今收集器计数发展的最前沿成果之一。面向服务端,具有以下特点

  • 并行与并发
    充分利用多CPU的、多核环境的硬件优势,来缩短停顿时间
  • 分代收集
    可以不需要其他收集器配合而独立管理整个GC堆
  • 空间整合
    整体来看是基于 标记-整理的算法,局部来看是基于 复制算法,因此不会产生内存空间碎片
  • 可预测的停顿
    建立可预测停顿时间模型,让使用者明确在一个长度为M毫秒时间片段内,GC时间不超过N毫秒

虽然保留分代的概念,但是新生代和老年代不再是物理隔离的了。分为多个大小相等的独立区域(Region),用Remembered Set来存储新生代和老年代的相互引用,大致可分一下步骤

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

六、内存分配与回收策略

自动内存管理归结为解决两个问题:给对象分配内存以及回收分配给对象的内存。
注:新生代GC(Minor GC) 频繁迅速
老年代GC(Major GC /Full GC):出现Major GC,一般伴随着Minor GC,慢10倍以上

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
    虚拟机为每一个对象定义了一个年龄计数器,经历一次Minor GC仍存活年龄加1,当到一定程度(默认15)就会晋升老年代。
  • 动态对象年龄判定
    如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代
  • 空间分配担保
    当出现大量对象在Minor GC仍然存活的时候就需要老年代分配担保。
    只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值