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

对象存活的确定

为了实现垃圾收集,首先要决定如何定义对象是否已经“死去”,即是否已经不被引用。

计数算法

最简单的计数方法,维护一个对象的计数器,当对其引用时就将计数器值加1,引用失效时减1,计数器值为0时即未被任何地方引用,对象死亡。
然而若存在循环引用时,当两个对象都不再被引用,但互相引用对方,此时计数器值不为0,即无法回收。

可达性分析算法

为了防止抱团取暖的情况发生,即去除循环引用的无用对象,采取可达性分析算法标记存活的对象。
以一系列称为“GC Roots"的对象作为起始点,按照引用链进行搜索,能够通过引用链到达“GC Roots"的对象则是存活的,而无法到达的对象将被放逐抛弃(可回收对象)。类似森林,每个“GC Roots"作为根节点生成树,无法追溯到根节点的对象无法从树根获取营养死亡。

可作为GC Roots的对象:

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

引用类型

JDK1.2后,Java对引用的概念进行扩充,分为强引用、软引用、弱引用、虚引用四种:

  • 强引用:只要存在则GC永远不会回收(类似 “Object obj = new Object()” )
  • 软引用:描述有用但非必要的对象,在将发生内存溢出前,会将其列入回收范围内进行二次回收。(jdk1.2后提供SoftReference类实现软引用)
  • 弱引用:描述非必需对象。只能生存到下一次回收前,无论内存是否足够都会被回收。(jdk1.2后提供WeakReference类实现弱引用)
  • 虚引用:不影响对象的生命周期,也无法通过虚引用获取对象实例。唯一目的是在对象被回收时收到一个系统通知。(jdk1.2后提供PhantomReference类实现虚引用)

按顺序总结:不回收 -> 空间不足时回收 -> 只要发生GC必定回收 -> 回收通知一声

对象自救

当对象被标记为死亡时,并非一定会被回收,仍有一次(且仅有一次)自救的机会。
GC会进行两次标记,第一次标记出没有引用链通向GC Roots的对象并进行一次筛选,筛选条件为是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或该方法已经被虚拟机调用过了,则视为没必要执行。
若筛选为有必要执行,则对象会被放入叫F-Query的队列中,稍后由虚拟机建立的一个低优先级的Finalizer线程执行。(不保证等待方法执行结束,因为当方法执行较慢或陷入死循环后,会导致队列永久等待甚至GC崩溃)
若对象要拯救自己,只需在finalize()方法中与引用链上任意一个对象建立关联即可(如将自己(this关键字)赋值给某个类变量或对象成员变量),拯救成功的对象会在第二次标记时被移出回收集合。
总结:确认枪毙名单(第一次标记) -> 判断有没有上诉资格(是否覆盖finalize()方法或方法是否已经被调用)-> 二次确认枪毙名单

回收方法区

方法区(永久代)对回收并不作要求,因为回收的效率远远不如新生代。

废弃常量

与堆中对象相似,当常量池中的接口、方法、字段的符号引用等没有被其他任何地方引用即可被清除出常量池。

无用的类

当满足下列三个条件,才可以说这个类是无用的:

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

垃圾收集算法

  • 标记-清除算法
    最基础的收集算法,分为“标记”和“清除”两个阶段。首先对需要回收的对象进行标记,标记结束后统一回收被标记的对象。
    缺点:标记与清除的效率都不高,清除后会产生大量的不连续的内存碎片
  • 复制算法
    将可用的内存均分为两份,每次只用一块,当这一块空间即将满了时,把存活的对象复制到另一块上,并清空这块内存,改用另一块。
    缺点:实际内存只有一半是可以用的
  • 标记-整理算法
    使存活对象向一端移动,移动结束后将端边界外的内存清空。

总结
标记-清除算法:将一块田里坏了的苗都原地拔掉,留下好的
复制算法:准备两块一样大的田,只种一块,把这块好的苗移到另一块按顺序种好,然后犁平,两块田交替使用。
标记-整理算法:把好的苗都按顺序种到一边,然后犁平剩下的田
( 新生代每次都有大批对象死去,少量存活,因此使用复制算法;老年代对象存活率高,使用标记-整理算法或标记-清除算法)

垃圾收集器

HotSpot虚拟机包含7种垃圾收集器,分布如下
新生代:Serial收集器、ParNew收集器、Parallel Scavenge收集器
老年代:CMS收集器、Serial Old收集器、Parallel Old收集器
(G1收集器在两代中均可用)

Serial收集器

特点:

  • 最基本的收集器,是一个单线程收集器
  • 简单而高效,很好运行于Client模式下
  • 只会使用一个cpu或一条收集线程完成垃圾收集
  • 在它工作时其他所有工作线程必须暂停(Stop the World)

过程:
用户线程到达SafePoint(暂停) -> GC线程(单线程)(新生代采用复制算法) -> 用户线程运行到达SafePoint(暂停) -> GC线程(单线程)(老年代采用标记-整理算法) ->用户线程继续运行

ParNew收集器

多线程的Serial收集器,除使用多线程进行垃圾收集,其余与Serial收集器一样。

过程:
用户线程到达SafePoint(暂停) -> GC线程(多线程)(新生代采用复制算法) -> 用户线程运行到达SafePoint(暂停) -> GC线程(单线程)(老年代采用标记-整理算法) ->用户线程继续运行

Parallel Scavenge收集器

使用复制算法的并行多线程收集器,与ParNew相同。
不同点:其他收集器更关注尽可能缩短垃圾收集时用户进程的停顿时间,而Parallel Scavenge收集器更注重达到一个可控制的吞吐量,即cpu用于运行用户代码的占比,因此也被称为“吞吐量优先”收集器。

特点:

  • 高效利用cpu时间,适用于后台运算和不需要太多交互的任务
  • 提供两个参数精准控制吞吐量,分别控制最大垃圾收集停顿时间、直接设置吞吐量大小。

Serial Old收集器

是Serial收集器的老年代版本
特点:

  • 单线程收集器
  • 使用“标记-整理“算法
  • 主要在Client模式的虚拟机上使用

Parallel Old收集器

Parallel Scavenge收集器的老年代版本
特点:

  • 多线程收集器
  • 使用“标记-整理“算法
  • 在注重吞吐量以及cpu资源敏感的场合使用

CMS收集器

特点:

  • 使用”标记-清理“算法
  • 以获取最短回收停顿时间为目标
  • 集中应用在互联网站或B/S系统的服务端上

过程:
用户线程到达SafePoint(暂停)-> 初始标记(仅标记GC Roots,速度很快)-> 并发标记(与用户线程并行,进行对象的追踪标记)-> 重新标记(与初始标记相同,为了标记并发期间变化的对象)->并发清理(与用户线程并行,进行对象的清理)

G1收集器

特点:

  • 并行与并发:通过多核缩短”Stop the World"停顿的时间
  • 分代收集:只需一个收集器就能独立管理整个GC堆,能采用不同的方式处理新对象跟旧对象。
  • 空间整合:采用”标记-整合“算法,不存在内存空间碎片,有利于程序长时间运行。
  • G1将堆分为很多个大小相等的独立区域(Region),虽然还保留新生代老年代的概念,但已经不是物理隔离,都是一部分c(不需要连续)的集合
  • 可预测的停顿:可明确指定在长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。通过跟踪各个Region里垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了有限时间内的最大收集效率。

过程:
用户线程到达SafePoint(暂停)-> 初始标记(仅标记GC Roots,速度很快)-> 并发标记(与用户线程并行,进行对象的追踪标记)->最终标记(多线程,修复并发标记阶段产生的标记变动)->筛选回收(多线程)

内存分配与回收策略

java的堆分区为:

  • 新生代:Eden区(1个),Survivor区(2个,form和to区)
  • 年老代

永久代(方法区)不属于堆
(Java 8 移除了永久代,创建了Metaspace(元空间)来替代,现大多数的类元数据分配在本地化内存中,元空间被并入堆中,堆的空间变大)

当新对象产生时

  • 大多数情况下,对象首先分配在Eden区。
  • 当Eden区没有足够空间时,虚拟机会发起一次Minor GC,此时Eden区和两个Survivor区中的from区里的和存活的对象都会被复制到to区(若to区的空间不足,则会通过分配担保机制提前进入老年代。)
  • 清空Eden区和from区,此时的to区变为from区,from区变为to区

注:大对象(需要大量连续内存空间的,如长字符串以及数组)直接进入老年代。

对象的年龄

对象进入Survivor区后年龄设为1,每在Survivor区熬过一次Minor GC,年龄就增加1岁,当达到一定程度(默认15)转入老年代。
并不是一定要达到要求值才能转入老年代,如果Survivor区中相同年龄所有对象大小总和大于Survivor空间的一半,则年龄大于等于该数值的对象都可以直接进入老年代。

空间分配担保

发生Minor GC前,虚拟机会先检查老年代的最大连续可用空间是否大于新生代所有对象总空间,若成立则可确保是安全的。若是不成立,则需检查设置是否允许担保失败,如果允许,则会检查老年代的最大连续可用空间是否大于历次晋升对象的平均大小,若大于,则会尝试本次Minor GC(尽管存在风险),若小于或不允许冒险,则改为进行Full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值