垃圾收集机制和内存分配机制

前言

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

自动内存管理是: 给对象分配内存和回收废弃对象占用的内存。

垃圾回收的范围: java堆和方法区。

一、对象判活算法

1) 引用计数算法(主流虚拟机并未采纳): 给对象添加一个引用计数器,每当有一个地方引用到它时,计数器加1;当引用失效时,计数器值就减1;任何时刻当一个对象的引用计数器为0时,它就变为废弃对象。可以被回收。

优点: 实现简单,判断效率高;

缺点: 很难解决对象之间的相互循环引用的问题。

2)可达性分析法: 算法思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到"GC Roots"没有任何的引用链时,则说明此对象是不可用的。

可作为"GC Roots"的对象包括以下:

1、虚拟机栈中引用的对象;

2、方法区中类静态属性引用的对象;

3、方法区中常量引用的对象;

4、本地方法栈中JNI(即一般native方法)引用的对象。

3)确定生存还是死亡

即使是在可达性分析法中不可达的对象,也不是被立即回收的。因为要真正被当作废弃对象进行回收,至少要经过两次“标记”过程。

当对象在被第一次发现与一系列“Gc Roots”节点之间没有引用链时,那么它将会被标记一次,并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。虚拟机将视为“没有必要执行”。

如果该对象被判定有必要执行finalize()方法,那该对象将会被防止在一个F-Queue队列中,并在稍后由一个低优先级的线程中执行。finalize()方法是对象逃过被垃圾回收的最后一次机会。稍后会对F-Queue进行第二次标记。如果期间还没有与任何对象建立关系,那在第二次标记后基本上真的要被回收了。

二、垃圾收集算法

1)标记-清除算法

标记清除算法实现思路包含两个阶段,第一个阶段,根据可达性分析算法标记所有不可达的「垃圾」,第二阶段,直接释放这些对象所占用的内存空间。

优点:是垃圾回收的基础思想算法,后续的回收算法都是基于这种算法进行改进。

缺点:

  • 效率问题:标记和清除的效率都很低,需要遍历两遍堆。
  • 空间问题:标记清除后,会产生大量的内存碎片。一旦由于需要分配大对象,常常不得不再一次触发垃圾回收。

2)复制算法

将内存划分为两份大小相等的块,每次只使用其中的一块,当系统发起 GC 收集动作时,将当前块中依然存活的对象全部复制到另一块中,并整块的释放当前块所占内存空间。

现代商用虚拟机:都是采用这种收集算法来回收新生代区域的对象。一般是将内存分为一块较大的Eden和两块较小的Survivor空间。默认比例是8:1:1。

这样的话,内存使用率就由50%提升至90%,但是即使是这样,也不能保证每次回收的都只有不多于10%的对象存活。这个时候,就要由其他内存(主要是老年代)来进行内存分配担保。

3)标记整理算法

由于复制算法在对象存活率较高时,就必须要进行较多的复制操作,效率就会变的很低,更关键的是如果不想浪费50%的内存,就需要有额外的空间进行内存分配担保,来应对超过90&对象存活的极端情况。所以在老年代中一般是不能直接采用这种算法。

根据老年代的特点,提出的标记-整理算法、其实就是加强版的标记-清除算法。只不过是不是直接将可回收的对象直接清除。是将存活对象向同一端移动,然后回收边界以外的内存。

4)分代收集算法

当前主流的jvm虚拟机,都是采用“分代收集”算法,这种算法并不是新的思想,只是根据对象存活周期的特点,将java堆分为几块。新生代和老年代。“新生代”一般是对象短时间是大量废弃,采用复制算法效率较高。“老年代”一般对象存活率高没有额外的空间内存进行内存分配担保,就必须使用“标记-清理”或“标记-清除”算法了。

三、垃圾收集器

如图是作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,就可以搭配使用。虚拟机所在的区域,则表示它是属于新生代收集器还是老年代收集器

“Stop the World“会在任何一种GC算法中发生。“Stop the World“意味着 JVM 因为要执行GC而停止了应用程序的执行。

  • Serial收集器: 单线程,新生代收集器,使用复制算法。它只会使用一个CPU或一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须“Stop the World“,暂停替他所有的工作线程,直到它收集结束。

  • ParNew收集器: Serial收集器的多线程版本,控制参数、收集算法、Stop the World、对象分配规则、回收策略都与Serial收集器完全一样

  • Parallel Scavenge收集器: 生代收集器,使用复制算法,并行多线程。

  • Serial Old收集器: Serial收集器的老年代版本,单线程,使用标记-整理算法。

  • Parallel Old收集器: Parallel Scavenge收集器的老年代版本,多线程,使用标记-整理算法

  • CMS收集器: 一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法。运作过程分四个步骤:初始标记 、并发标记、重新标记、并发清除。

    初始标记、重新标记这两个步骤仍然需要“Stop the World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这一阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间段。

    整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,所以,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    优点: 并发收集,低停顿

    缺点: 对CPU资源非常敏感、无法处理浮动垃圾、基于标记清除算法,收集结束时有大量控件碎片产生

  • G1收集器: G1收集器是当今收集器技术发展最前沿成果之一,一种面向服务端应用的垃圾收集器。

    G1的特点: 并行与并发、分代手机、空间整合、可预测的停顿

    运作过程如下: 初始标记、并发标记、最终标记、筛选回收。

    初始标记阶段: 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

    并发标记阶段: 是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

    而最终标记阶段: 则是则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

    最后在筛选回收阶段: 首先对各个Region的回收价值和成本进行排序。根据用户所期望的GC停顿时间来制定回收计划。

四、内存分配与回收策略

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

大对象直接进入老年代: 大对象是指需要大量连续内存控件的Java对象,最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代: 虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代。

动态对象年龄判定: 虚拟机并不是永远要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保: 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

五、总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。

猜你喜欢

转载自juejin.im/post/5d3085eaf265da1ba2529857