0%

JVM之第三章:垃圾收集器和内存分配策略

第三章:垃圾收集器与内存分配策略

概述

程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而灭,栈帧随着方法的进入和退出有条不紊的执行出栈入栈操作,每一个栈帧中分配多少内存基本上是类结构确定下来就已知的。因此这几个区域不用过多考虑回收的问题。而Java堆和方法区就不一样,实现类的需要的内存一样,只有在运行期才能知道创建哪些对象,这部分的内存分配和回收都是动态的。

对象已死吗

对堆进行回收前,要确定哪些对象是存活的,哪些是已经死去的

引用计数算法

给对象中田间一个引用计数器,当多一个地方引用,计数器加1,引用失效时,计数器减1,任何计数器为0的对象就是不可能在被使用的。(但是主流的Java虚拟机并没有使用计数器来管理内存,原因在于他很难解决对象相互循环引用的问题

可达性分析

Java是通过可达性分析来判断对象是否存活的。基本思路是通过被称为‘GC Roots’的对象作为起始点,从这些结点开始向下搜索,当一个对象到GC Roots没有任何的引用链(GC Roots到这个对象不可达),证明此对象是不可用的。

可作为GC Roots的对象包含下面几种:

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

我们希望能描述这样一类对象:当内存空间足够时,则保留在内存中,当内存空间紧张时,则可以抛弃这些兑现。

引用在JDK1.2之后进行概念的扩充

  • 强引用:类似Object obj = new Object()这种是前引用。
  • 软引用
  • 弱引用
  • 虚引用
生存还是死亡

当可达性分析不可达时,这时处于缓刑阶段,还要经历两次标记过程:当对象发现没有与GC Roots相连接,就是第一次标记并筛选,帅选是看这个对象是否有必要执行finalize()方法(对象没有覆盖finalize()或者finalize()已经被调用过)。

如果判定需要执行,那么i这个对象会被放到一个F-Queue队列中,稍后会被Finalizer线程执行。如果对象在finalize()中成功与因引用链上的任意一个对象建立关联,则被移除即将回收的集合。否则的话就要被回收了(finalize()只会执行一次)

回收方法区

方法区的回收效率很低

永久代的垃圾回收主要回收两部分内容:废弃常量和无用的类。

垃圾收集算法

标记清除算法

标记清除算法是最基础的算法,分为”标记“和”清除“两个阶段,首先标记处所有需要回收的对象,在标记完成后统一回收。标记过程就是前一节所述的过程。

问题有两个:

  • 效率问题
  • 空间问题,标记清除后会产生大量不连续的内存碎片。后续当需要分配较大对象时,无法找到连续内存而不得不触发另一次垃圾回收工作。
复制算法

先将内存按容量分为两部分,只是用其中一块,第一块用完了,将所有活着的对象全部移到第二块中。这种方法有点事简单高效,缺点是内存缩小为原来的一半。

现在的虚拟机都是采用这种方法来回收新生代的。

虚拟机将内存分为8:1:1的三块内存,一块打的Eden空间和两块小的Survivor空间,每次回收时将Eden和一块Survivor还存活的对象复制到另一块Survivor中。

标记整理算法

复制算法在对象存活率比较高时效率低,并且还需要分配担保,所以在老年代不选用这个算法。

先使用标记清除的方法,后续在将所有存活的对象全部向一端移动,在将剩下的区域清除掉。

分代收集算法

根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代。

新生代使用复制算法,老年代使用标记清除或者标记整理算法.

HotSpot的算法实现

枚举根节点

在HotSpot中,是使用一组成为OopMap的数据结构来得知哪些地方存放着对象引用,在类加载完成的时候,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来。

安全点

在OopMap的协助下,HotSpot可以很快的完成GC Roots枚举,但是导致引用关系变化的指令非常多,如果每一条指令都生成OopMap,则会需要大量的空间。

所以HotSpot在特定的位置记录这些信息,被称为安全点。

垃圾收集器

收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。

Serial收集器

一个单线程的收集器,标记-整理算法

ParNew收集器

Serial的多线程版本,标记整理算法

Parallel Scavenge收集器

它是一个新生代收集器,使用复制算法,又是并行的多线程收集器。

它的关注点是达到一个可控制的吞吐量,即CPU用于运行用户代码的时间与CPU总消耗时间的比值。

Serial Old 收集器
Parallel Old收集器
CMS(concurrent mark sweep)收集器

基于标记-清除算法。分为四个步骤

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

并发标记和并发清除耗时最长,但是可以与用户线程一起工作。

优点:并发收集,低停顿。

缺点:

  • CPU资源敏感
  • 无法处理浮动垃圾,在CMS并发清除阶段用户线程还运行着,这时产生的垃圾出现在标记之后,就只能等待下一次GC清除了。
  • 标记-清除算法会产生大量空间碎片。
G1收集器

G1具备如下特点:

  • 并发与并行
  • 分代收集
  • 空间整合:整体看是标记-整理算法,局部是复制算法。
  • 可预测的停顿:简历可预测的停顿时间模型。G1之所以能建立可预测的停顿时间模型,是因为它优化的避免在整个Java堆中进行全区域的垃圾收集。追踪每个Region里面的垃圾价值,在后台维护一个优先列表。

内存分配策略

对象优先在Eden分配

大多数情况,对象在新生代Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

大对象直接进入老年代

典型的大对象是长的字符串和数组。

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

当对象在Eden出生并经过第一次Minor GC后仍然存活,并且被Survivor容纳,在Survivor中每熬过一次GC,则年龄增加1岁,默认增加到15岁,就会晋升老年代。

动态对象年龄判定

如果Survivor空间中相同年龄的所有对象总和大于空间的一半,则大于该年龄的对象直接进入老年代。

空间分配担保
~~