JVM初探之 堆和垃圾回收

6 篇文章 0 订阅

在这里插入图片描述

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存(The heap is the runtime data area from which memory for all class instances and arrays is allocated)。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

堆的基本结构

Java堆大概可以分为两个部分,年轻代(占堆空间的1/3),老年代(占堆空间的2/3)。

年轻代又分为Eden区、From Survivor区(s0)、To Survivor区(s1),他们的占比为8:1:1

对象都产生在Eden区。每个线程都会在堆中预先分配一块内存,称为本地线程分配缓冲TLAB线程私有,通过-XX:+/-UseTLAB参数来设定),需要内存时先从TLAB中获取,TLAB中不够了再从堆中获取。

对象进入老年代的条件:

  • 对象年龄达到15也会进入老年代

    对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置(默认为15)。

  • Survivor区中相同年龄的对象所占空间的总和大于Survivor区的一半时,大于该年龄的对象会直接进入老年代
  • MinorGC后的对象太多,Survivor区放不下时也会直接进入老年代
  • 大对象会直接进入老年代(分配担保机制)

    HotSpot虚拟机提供了 -XX:PretenureSizeThreshold 参数(只对 SerialParNew 两款新生代收集器有效),指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区 之间来回复制,产生大量的内存复制操作。

分配担保机制

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC是有风险的;如果小于,或者 -XX:HandlePromotionFailure 设置不允许“冒险”,那这时就要改为进行一次 Full GC。
上面提到的“冒险”指:新生代使用标记-复制收集算法,但为了内存利用率,只使用其中一个 Survivor 空间来作为轮换备份,因此当出现大量对象在 Minor GC 后仍然存活的情况 (最极端的情况就是内存回收后新生代中所有对象都存活),需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多空间。
取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次 Minor GC 存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次 Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将 -XX:HandlePromotionFailure 开关打开,避免Full GC过于频繁。
在允许担保失败并尝试进行 Minor GC 后,可能会出现三种情况:

  1. Minor GC 后,存活对象小于 survivor 大小,此时存活对象进入 survivor 区中。
  2. Minor GC 后,存活对象大于 survivor 大小,但是小于老年大可用空间大小,此时直接进入老年代。
  3. Minor GC 后,存活对象大于 survivor 大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,触发 Full GC。如果 Full GC 后,老年代还是没有足够的空间,此时就OOM了。

在这里插入图片描述

对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头实例数据对齐填充

对象头包括两部分,一部分是自身的运行时数据(Mark Work),如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特;另一部分是类型指针,指向方法区中的类元信息。

例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,1个比特固定为0,2个比特用于存储锁标志位(未锁定、轻量级锁定、重量级锁定、GC标记、可偏向):
在这里插入图片描述在64位虚拟机下,Mark Word是64bit大小的,其存储结构是这样:
在这里插入图片描述
实例数据存储的是对象的真正有效的数据。

对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

通过代码查看对象的内存布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

有这样一个类:

class Clazz {
    private String _string;
    private boolean _boolean;
    private char _char;
    private byte _byte;
    private short _short;
    private int _int;
    private long _long;
    private float _float;
    private double _double;
}

查看对象内存布局:

System.out.println(ClassLayout.parseInstance(new Clazz()).toPrintable());
Clazz object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4                    (object header)                           b8 2f e3 14 (10111000 00101111 11100011 00010100) (350433208)
      8     8               long Clazz._long                               0
     16     8             double Clazz._double                             0.0
     24     4                int Clazz._int                                0
     28     4              float Clazz._float                              0.0
     32     2               char Clazz._char                                
     34     2              short Clazz._short                              0
     36     1            boolean Clazz._boolean                            false
     37     1               byte Clazz._byte                               0
     38     2                    (alignment/padding gap)                  
     40     4   java.lang.String Clazz._string                             null
     44     4                    (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

在这里插入图片描述
说一下填充:

4个字节的外部填充好理解,是为了把对象的大小填充为8的倍数;

前面的2个字节的内部填充是为了保证“mS原则”,即一个对象的大小为m字节,那么它的偏移量S应为m的整数倍。在这个例子里,byte类型的实例下一个是String类型的实例,站4个字节,但是由于前面的所有实例加起来占了38个字节,由于38不是4的倍数,因此需要在内部填充两个字节,此时String的实例的偏移量就变成了40,40是4的倍数。

对象的访问定位

句柄访问:从堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息:在这里插入图片描述
直接指针访问:reference中存储的直接就是对象地址:在这里插入图片描述

垃圾判断算法

引用计数算法

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

HotSpot没有使用引用计数法。

public class ReferenceCountingGC {
    public Object instance = null;
    private byte[] bigSize = new byte[1024 * 1024];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.gc();
    }
	
	public static void main(String[] args){
		testGC();
	}
}

在这里插入图片描述
可以看到,这两个对象被回收了。因此HotSpot不是通过引用计数法来判断垃圾的。

可达性分析算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。在这里插入图片描述
可作为GC Roots的对象有:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  3. 在方法区中常量引用的对象,譬如字符串常量池里的引用
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  6. 所有被同步锁(synchronized关键字)持有的对象
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

引用的分类

  • 强引用:传统引用,只要对象存在强引用,永不回收
  • 软引用(SoftReference):堆内存不满时,不会回收软引用的对象;堆内存已满需要腾出空间时,软引用的对象会被清理
  • 弱引用(WeakReference):弱引用的对象只要发生垃圾回收就会被清理
  • 虚引用(PhantomReference):是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

finalize()方法

在可达性分析中被判定为“不可达”的对象也不会被立即回收,如果该对象重写了finalize()方法并且该方法没有被调用过,同时finalize()方法中还存在对引用链上的任何对象的引用,该对象就不会被回收:
在这里插入图片描述
比如这张图中,Object5、Object6、Object7会被标记为垃圾,而Object8由于重写了finalize()方法并关联到了引用链(Object4)上,因此在第一次垃圾标记时不会被标记为垃圾,但是如果之后再进行一次垃圾标记,Object8还是会被标记为垃圾。即finalize()方法只能“保它一次性命”。

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型

判断废弃的常量:没有任何字符串对象引用常量池中的某个常量,且虚拟机中也没有其他地方引用这个字面量,那么这个常量就会被标记为垃圾
判断不再使用的类

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

分代收集理论:

弱分代假说:绝大多数对象都是朝生夕灭的
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

垃圾回收方式:

Minor GC/Young GC:新生代垃圾回收
Old GC:老年代垃圾回收(目前只有CMS收集器会有单独收集老年代的行为)
Mixed GC:混合垃圾回收(目前只有G1收集器会有这种行为)
Full GC:对整个堆内存进行垃圾回收
Major GC:结合语境,有可能时Old GC,也有可能是Full GC

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象;也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
在这里插入图片描述
问题:

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

标记-复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
在这里插入图片描述
问题:

  1. 内存浪费(将可用内存缩小为了原来的一半)
  2. 如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销

标记-整理算法

提前判断好存活的对象所占内存,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
在这里插入图片描述
问题:

对象移动后,它的内存地址也发生了变化。因此就需要对所有对象的引用进行更新,这种操作代价很高,并且需要停止程序运行(Stop The World)

分代收集算法

将Java堆分为新生代和老年代,每次对新生代进行垃圾回收后存活下来的对象“年龄”+1,当年龄达到15(对象头中有4个bit用于存储分代年龄,最大为二进制1111 -> 十进制15)时,就采用标记-复制算法将其移动到老年代,老年代的垃圾回收采取标记-清除算法或标记-整理算法(依据不同的垃圾收集器而定)。

新生代的标记-复制算法并不是将新生代的内存1:1划分,而是按照8:1:1分成了Eden区、From Survivor区、To Survivor区,每次垃圾回收时回收Eden区和一个Survivor区并将存活下来的对象采用标记-复制算法移动到另一个Survivor区,下一次垃圾回收时就是回收Eden区和另一个Survivor区。

垃圾收集器

在这里插入图片描述
(如果两个收集器之间存在连线,就说明它们可以搭配使用)

Serial收集器(标记-复制)

在JDK 1.3.1之前是HotSpot虚拟机新生代收集器的唯一选择。

单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(Stop The World)。
在这里插入图片描述

ParNew收集器(标记-复制)

ParNew收集器实质上是Serial收集器的多线程并行版本(默认开启的收集线程数与处理器核心数量相同),除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
在这里插入图片描述
除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作

Parallel Scavenge收集器(标记-复制)

Parallel Scavenge收集器的关注点与其他收集器不同。其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(处理器用于运行用户代码的时间与处理器总消耗时间的比值)。

Serial Old收集器(标记-整理)

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
在这里插入图片描述

Parallel Old收集器(标记-整理)

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
在这里插入图片描述

CMS收集器(标记-清除)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

运作过程:

  1. 初始标记(CMS initial mark):需要“Stop The World”。标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3. 重新标记(CMS remark):需要“Stop The World”。为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
  4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS 垃圾收集器的问题
  1. 并发回收导致CPU资源紧张

    在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。

  2. 无法清理浮动垃圾

    在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。

  3. 并发失败(Concurrent Mode Failure)

    由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX:CMSInitiatingOccupancyFraction 参数来设置。
    这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。

  4. 内存碎片问题:

    CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
    为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。

G1(Garbage First)收集器(混合收集)

目标:在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒(停顿时间模型)

面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大(Mixed GC)。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

运作过程:

  1. 初始标记(Initial Marking):需要“Stop The World”,但是时间可以短到忽略不计。标记GC Roots能直接关联到的对象
  2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行
  3. 最终标记(Final Marking):需要“Stop The World”。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(虽然并发标记结束时会进行一次类似操作,但还是难免会有“漏网之鱼”)。
  4. 筛选回收(Live Data Counting and Evacuation):需要“Stop The World”。负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
    在这里插入图片描述

低延迟垃圾收集器

基本都处于试验阶段

Shenandoah收集器

OracleJDK看不到,OpenJDK可能有

ZGC收集器(Z Garbage Collector)

Epsilon收集器

除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责(自动内存管理子系统)

三色标记算法

把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

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

在这里插入图片描述三色标记算法,用于垃圾回收器升级,将STW变为并发标记。STW就是在标记垃圾的时候,必须暂停程序,而使用并发标记,就是程序一边运行,一边标记垃圾。

并发标记一共会有两个问题:一个是错标,标记过不是垃圾的,变成了垃圾(也叫浮动垃圾);第二个是漏标,即本来已经当做垃圾了,但是又有新的引用指向它。

错标

标记过不是垃圾的,变成了垃圾(也叫浮动垃圾),如下图:标记完了E是D的一个引用,也就是说此时E是灰色的,但是D断开的对E的引用。这个浮动垃圾的问题影响不是很大,可能就是暂时的浪费一点内存,它肯定抗不过下一轮GC。
在这里插入图片描述

漏标

D是黑色的,E是灰色的,但是D又指向了G,和E断开了指向G。 因为D已经标记了是黑色,但是E断开了引用,所以G就当做了是白色的。这个时候如果不操作的话,就会把G错杀掉。
在这里插入图片描述

漏标的解决方案

漏标只有在以下两个条件同时发生时才会发生:

  1. 黑色对象指向了白色对象
  2. 本来指向这个白色对象的灰色对象断开了对它的连接。

解决方案就是破坏以上两个条件的任意一个。

G1:写屏障 + 原始快照

D想要指向G,这是一个写操作。

当对象E的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:

void pre_write_barrier(oop* field) {
    oop old_value = *field;      // 获取旧值
    remark_set.add(old_value);   // 记录原来的引用对象
}

这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻的 GC Roots 确定后,当时的对象图就已经确定了。

比如 当时D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。

CMS:写屏障 + 增量更新

当对象D的成员变量的引用发生变化时(objD.field = G;),使用写屏障将D新的成员变量引用对象G记录下来:

void post_write_barrier(oop* field, oop new_value) {  
  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
      remark_set.add(new_value);        // 记录新引用的对象
  }
}

这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)

oop oop_field_load(oop* field) {
    pre_load_barrier(field);     // 读屏障-读取前操作
    return *field;
}
ZGC:读屏障

读屏障是直接针对第一步:var G = objE.fieldG;,当读取成员变量时,一律记录下来:

void pre_load_barrier(oop* field, oop old_value) {  
  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
      oop old_value = *field;
      remark_set.add(old_value);     // 记录读取到的对象
  }
}

这种做法是保守的,但也是安全的。因为条件二中黑色对象重新引用了该白色对象,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVMJava虚拟机)内存模型和垃圾回收Java程序中重要的概念。JVM内存模型定义了Java程序在运行时所使用的内存结构,而垃圾回收是一种自动化的内存管理机制,用于回收不再使用的对象以释放内存空间。 JVM内存模型主要包括以下几个部分: 1. Heap):JVM中最大的一块内存区域,用于存储对象实例。在中分配的内存由垃圾回收器自动管理。 2. 方法区(Method Area):方法区用于存储类的信息、常量、静态变量等数据。在JDK 8及之前的版本中,方法区被实现为永久代(Permanent Generation),而在JDK 8之后,被改为元空间(Metaspace)。 3. 虚拟机栈(VM Stack):每个线程在运行时都会创建一个虚拟机栈,用于存储局部变量、方法调用和返回信息等。每个方法在执行时都会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量表、操作数栈、动态链接、返回地址等信息。 4. 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈类似,但用于执行本地方法(Native Method)。 垃圾回收JVM的一项重要功能,它负责自动回收不再使用的内存。JVM中的垃圾回收器会定期扫描中的对象,标记出不再被引用的对象,并将其回收释放。垃圾回收可以有效地避免内存泄漏和内存溢出的问题,提高程序的性能和稳定性。 JVM内存模型和垃圾回收Java程序员需要了解和理解的重要概念,它们直接影响到Java程序的性能和内存使用情况。合理地管理内存和优化垃圾回收对于编写高效、稳定的Java程序至关重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值