JVM笔记——垃圾收集器与内存分配策略

1 判断对象是否已经死亡

在垃圾收集器对堆进行回收时,首先就要判断哪些存活,哪些死去。

1.1 引用计数算法

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

虽然引用计数算法实现简单,判定效率也高,但主流java虚拟机并没有使用它的,原因是它难以解决对象之间的循环引用问题。

1.2 可达性分析算法

主流的商用语言 都是用可达性算法来判定对象是否存活,基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(从GC Root到该对象不可达),证明此对象不可用

可达性算法判定对象是否可回收.jpg

可作为GC Roots的对象有:虚拟机栈(栈帧中本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

1.3 再谈引用

JDK1.2之前定义引用为:reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

在JDK1.2之后,Java对引用的概念进行了扩充,引用分为:强引用、软引用、弱引用、虚引用四种。四种引用的强度逐渐减弱。

  • 强引用是使用最普遍的引用,类似于"Object obj = new Object()"这类的引用,只要引用还存在,垃圾回收器永远不会回收掉被引用的对象,当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  • 软引用来描述一些还有用但并非必须的对象,在系统将要发生内存溢出之前,将会把这些引用指向的对象列入回收范围之中进行第二次回收。如果这次回收还没有足够内存,才会抛出内存溢出异常。可以用SoftReference类来实现软引用。
  • 弱引用也是用来描述非必须的对象,强度比软引用更弱,只能生存到下一次垃圾回收之前,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
  • 虚引用就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,也无法通过虚引用来取得对象实例。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。

一般程序中很少使用弱引用和虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。

1.4 生存还是死亡

即使在可达性算法中不可达的对象也不是非死不可,要判定一个对象死亡,至少要经过两次标记过程

如果对象在进行可达性分析后没有与GC Roots相连的引用链,它将会被第一次标记并进行一次筛选,条件是该对象是否有必要执行finalize()方法;当对象没有覆盖finalize()方法或方法已经被执行过,则视为没有必要执行。

当对象被判定为有必要执行finalize()方法,对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联(在finalize()方法中),否则就会被真的回收。

1.5 回收方法区(永久代)

永久代的垃圾回收主要回收废弃常量和无用的类。

1.5.1 废弃常量回收

回收废弃常量与回收java堆中的对象十分类似。以回收常量池中的字面量为例。字符串"abc"进入常量池,但当前系统内没有String对象叫"abc",这说明没有String对象引用常量"abc",也没有其他地方引用了这个字面量,若这时发生内存回收,必要的话这个常量"abc"就会被清出运行时常量池。其他类(接口)、方法、字段的符号引用也与此类似。

1.5.2 判定无用类

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

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

2 垃圾收集算法

垃圾收集算法有四种:标记-清除、复制、标记整理、分代收集

2.1 标记-清除算法

最基础的算法,有“标记”、“清除”两个阶段。首先标记出所有要被回收的对象,在标记完成后统一回收被标记的对象。

有两个不足:效率问题和空间问题。两阶段的效率都不高。清除后会产生大量的内存碎片,如果要分配大对象时没有足够的连续空间,会提前触发垃圾收集。

标记-清除.jpg

2.2 复制算法

为了解决效率问题,复制算法将可用内存划分为大小相等的两块,每次只使用其中一块;当这块用完之后,将存活的对象复制到另一块上面,然后将已使用过的内存空间一次性清理掉。

算法代价将内存缩小为原来的一半。
复制算法.jpg

2.3 标记-整理算法

根据老年代特点提出的算法,标记过程与“标记-清除”过程一致,后续操作并不是对对象直接进行清理,而是让所有存活对象都向一端移动,直接清理掉端边界以外的内存。
标记-整理.jpg

2.4 分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,将内存根据对象的生存周期划分为几块,一般将java堆分成新生代和老年代。可用根据各个年代的特点选择不同的收集算法,新生代中每次收集时只有少量对象存活,就选用复制算法;老年代中对象存活率高必须使用另外两种算法来回收。

3 垃圾收集器

HotSpot中的垃圾收集器.png

若两个垃圾收集器之间有连线说明它们可以搭配使用。

新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

3.1 Serial(串行)收集器

最基本、最悠久的收集器。一个单线程收集器,意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。使用复制算法

Serial Serial Old收集器运行示意图.png

适用于Client模式下的虚拟机

3.2 ParNew收集器

ParNew收集器是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
ParNew收集器.png

是Server模式下的首选的新生代收集器,除Serial外,只有他能与CMS(并发收集器)收集器配合工作

  • 并行:多条垃圾收集器线程并行工作,用户线程仍处于等待状态
  • 并发:用户线程与垃圾收集线程用时执行(不一定并行,可能交替执行),用户程序继续运行,垃圾收集程序运行于另一个CPU上。

3.3 Parallel Scavenge收集器

新生代收集器也是使用复制算法,并行多线程收集器。

CMS等收集器关注点尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器目标是达到一个可控制的吞吐量(运行代码时间/(运行代码时间+垃圾收集时间))。也被称为吞吐量优先收集器。

3.4 Serial Old收集器

Serial Old是Serial的老年代版本,同样是单线程,使用标记-整理算法。主要有两大用途:一种是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种是作为 CMS 收集器的后备方案。

3.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
parallel scavenge parallel old收集器.png

3.6 CMS收集器

CMS收集器是一种以获取最短停顿时间为目标的收集器,基于标记-清除算法实现。总体来说,回收过程与用户线程一起并发执行,适用于互联网站和B/S系统的服务器上。

收集过程分为4步

  • 初始标记:暂停其他线程,标记一下CG Roots能直接关联到的对象,速度很快。
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  • 重新标记:修正并发标记期间因用户程序继续运行而导致标记变动那一部分对象的标记记录,比初始标记时间长,但比并发清除时间短。
  • 并发清除:对标记的对象进行清除回收。耗时时间最长
    CMS收集器.png

它有三个明显缺点:

  1. 对CPU资源十分敏感
  2. 无法处理浮动垃圾
  3. 基于标记-清除算法,益产生内存碎片

3.7 G1收集器

一款面向服务端应用的垃圾收集器,与其他GC收集器相比具有如下特点:

  • 并行与并发:利用硬件多核的优势缩短停顿时间 ,部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 分代收集 :G1可以独立管理整个GC堆,可以采用不同方式处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象获取更好的收集效果。
  • 空间整合:整体看基于标记整理,局部看是基于复制 算法实现,不会产生内存碎片
  • 可预测的停顿:G1除了能降低停顿外还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1与其他收集器的区别:
其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。

G1为什么能建立可预测的停顿时间模型:
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。

G1收集器大致可分为如下步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
G1收集器.png

3 内存分配与回收策略

堆空间的基本结构.png

上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。

对象主要分配在新生代上的Eden区上,当Eden区没有足够空间时将发起一次Minor GC

  • 新生代GC(Minor GC) 发生在新生代的垃圾收集动作,java对象大多数朝生夕灭,所以Minor GC十分频繁,但回收速度也较快
  • 老年代GC(Major/Full GC)发生在老年代的GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

大对象(很长的字符串和数组)直接进入老年代,为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

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

虚拟机给每个对象定义了一个对象年龄(Age),若对象在Eden中出生,经过一次Minor GC仍然能存活并能被Survivor容纳,将被移动到Survivor空间中,并且Age置为1,Survivor区中对象每熬过一次Minor GC就将Age+1,年龄增长到一定程度时(默认15),就会晋升到老年代中。

动态对象年龄判定

如果在Survivor空间中相同的年龄所有对象大小综合大于Survivor空间的一半,年龄大于或等于该Age的对象将直接进入老年代

空间分配担保

在Minor GC之前,虚拟机会检查老年代中最大可用连续空间是否大于新生代所有对象总空间,条件成立则Minor GC为安全。如果不成立则会检查HandlePromotionFailure设置值是否允许担保失败,如果允许会检查老年代中最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则会进行Minor GC(有风险);如果小于,或者HandlePromotionFailure设置不允许冒险,要改为进行一次Full GC。

新生代使用复制算法为了内存利用率,只用其中一个Survivor空间作为轮换备份。所以在出现大量对象在Minor GC后存活,就需要老年代进行分配担保,把Survivor无法存放的对象直接放入老年代。

你可能感兴趣的:(JVM笔记——垃圾收集器与内存分配策略)