JVM:自动内存管理-垃圾收集器与内存分配策略

Java与C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

二、对象已死/h2>

1、引用计数法

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

引用计数器虽然占用了一些额外的内存空间来进行计数,原理简单,判定效率很高;

为什么主流Java虚拟机没有使用引用计数器来管理内存呢/p>

引用计数法看似简单的算法有很多例外情况要考虑,必须配合大量额外处理才能保证正确的工作,比如单纯的引用计数很难解决对象之间互相循环引用的问题

引用计数器的缺陷

运行代码收查看日志信息发现,这两个对象均被回收虚拟机并没有因为这两个相互引用就放弃回收他们—->Java虚拟机并不是通过计数算法来判断对象是否存活的;

2、可达性分析算法

该算法的核心思想:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连(图论话来说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分 代假说之上:

  • 1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  • 3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

  1. 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  1. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

2、标记-清除算法

最基础的收集算法

算法分为“标记”和“清除”两个阶段:

  • 首先标记出所有需要回收的对象
  • 在标记完成后,统一回收掉所有被标记的对象
  • 也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
  • 标记过程就是对象是否属于垃圾的判定过程

缺点

  • 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

Appel式回收

  • 把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
  • Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。

内存的分配担保也一样,如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。

4、标记-整理算法

  • 针对老年代对象的存亡特征 —–>“标记-整 理”算法,
  • 其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理
  • 而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

2、安全点

实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。

  • 有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停
  • 安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的

“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用

如何在垃圾收集发生时让所有线程(这里其实不包括 执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。

这里有两种方案可供选择:抢先式中断主动式中断

  • 抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚 拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

3、安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

4、记忆集与卡表

为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”

伪共享是处 理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

在JDK 7之后,HotSpot虚拟机增加了一个新的参数,用来决定是否开启 卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

6、并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析, 这意味着必须全程冻结用户线程的运行。

为什么必须在一个能保障一致性的快照上才能进 行对象图的遍历/p>

为了能解释清楚这个问题,我们引入三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

两种解决方案:

  • 增量更新(Incremental Update)

    • 增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

2、ParNew收集器

  • ParNew收集器实质上是Serial收集器的多线程并行版本
  • 能与CMS 收集器配合工作。

CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次 实现了让垃圾收集线程与用户线程(基本上)同时工作。

  • 使用算法:复制算法
  • 适用范围:新生代
  • 应用:运行在Server模式下的虚拟机中首选的新生代收集器

高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算 任务,主要适合在后台运算而不需要太多交互的分析任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量

  • 控制最大垃圾收集停顿时间 的参数

    • 允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。
    • 垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的
  • 直接设置吞吐量大小的参数。

    • 参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的 比率,相当于吞吐量的倒数。

4、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年1月11日
下一篇 2021年1月11日

相关推荐