深入理解java虚拟机——垃圾收集器与内存分配策略

程序计数器、虚拟栈、本地方法栈 3个部分随线程而生,随线程而灭。所以这3个区域就不需要过多的考虑垃圾回收的问题。当方法结束或线程结束时,内存自然就回收了。

1.判断对象存活

1.1 引用计数器法

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

缺点:当对象之间相互循环引用时。假设对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA。即使这两个对象已经不可能再被访问,但是该方法无法通知GC收集器回收它们。

1.2 可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些对象开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

在Java语言中,可作为 GC Roots 的对象包括下面几种:

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

2. 引用

  • 强引用:在程序代码中普遍存在,垃圾收集器永远不会回收该引用的对象,例如“Object obj = new Object()”。
  • 软引用:描述一些还有用但并非必需的对象。在系统发生内存溢出异常之前,会把该引用的对象列进回收的范围,进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
  • 虚引用:能在这个对象被收集器回收时收到一个系统通知。

3. finalize()

当对象被宣告死亡时,存在finalize()方法并没有调用过的对象会进入F-Queue队列中,并调用finalize()。当它在finalize()中和 GC Roots 进行连接时可以逃脱死亡。

任何一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。

4. 回收方法区

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

废弃常量:假如常量池中存在一个字符串“abc”,但系统中没有任何一个String对象引用常量池中的“abc”常量,也没有其它地方引用这个字面量。这个时候发生内存回收,这个常量就会被清理出常量池。

无用的类,满足下面三个条件:

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

5. 垃圾收集算法

5.1 标记—清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

5.2 复制算法

将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

不足:将内存缩小为原来的一半。

实际上我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。操作过程和上述一样。
当复制的那一块Survivor没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

问题:为什么当Eden和其中一块Survivor放满以后,不先释放Survivor,再释放Eden。这样当Survivor被释放以后可以空出空间来给Eden回收。

5.3 标记—整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

5.4 分代收集算法

根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大批对象死去,只要少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”或者“标记—整理”算法来进行回收。

6. 垃圾收集器

6.1 Serial 收集器

单线程收集器。它只会使用一个CPU或一条收集线程区完成垃圾收集工作,而且它在进行垃圾收集时,必须暂停其它所有工作线程,直到它收集结束。“Stop The World”

优点:简单而高效(与其它收集器的单线程比)

6.2 ParNew 收集器

Serial 收集器的多线程版本,除了使用了多线程进行收集之外,其余行为和 Serial 收集器一样。

tips:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上。

6.3 Parallel Scavenge 收集器

新生代,复制算法,并行的多线程收集器,和 ParNew 一样。不同于它可精确控制吞吐量。

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

6.4 Serial Old 收集器

Serial 收集器的老年代版本,“标记—整理”算法,单线程收集器。

6.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本,“标记—整理”算法,多线程收集器。

6.6 CMS 收集器

以获取最短回收停顿时间为目标的收集器,基于“标记—清除”算法实现。步骤如下:

  1. 初始标记(标记 GC Roots 能直接关联到的对象)
  2. 并发标记(GC Roots Tracing,可达性分析)
  3. 重新标记(修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录)
  4. 并发清除

优点:并发收集、低停顿

缺点:

  • CMS 收集器对 CPU 资源非常敏感,并发阶段会导致应用程序变慢。CMS 默认启动的回收线程数是(CPU数量 + 3)/ 4。
  • CMS收集器无法处理浮动垃圾,可能出现 “Concurrent Mode Failure“ 失败而导致另一次 Full GC 的产生。
    ”浮动垃圾“:由于 CMS 并发清除阶段用户线程还在运行着,会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好等下一次 GC 时再清除掉,这一部分垃圾叫做浮动垃圾。
  • CMS是基于 ”标记—清除“ 算法实现的。

6.7 G1 收集器

它是一款面向服务器应用的垃圾收集器。它具有如下特点:

  • 并行与并发:使用多个CPU来缩短 Stop The World 停顿时间。
  • 分代收集
  • 空间整合:运行期间不会产生内存空间碎片。
  • 可预测的停顿

G1 收集器运作方式:初始标记,并发标记,最终标记,筛选回收。

7. 内存分配与回收策略

tips:
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多数都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢10倍以上。

7.1 对象优先在 Eden 分配

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

7.2 大对象直接进入老年代

所谓大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

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

如果对象在 Eden 出生并经历第一次 Minor GC 后依然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1。对象在 Survivor 区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

7.4 动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

7.5 空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时需要进行一次 Full GC。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/ice__cookie/article/details/79057702
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞