只想当扫地机器人的老王

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 日程表

3、判断对象是否存活

发表于 2019-06-29 | 分类于 看书笔记 , 深入理解Java虚拟机

1. 判断方式与方法

垃圾收集器对 Java堆里的对象 是否进行回收的判断准则:

Java对象是存活 or 死亡:判断对象为死亡才会进行回收

在Java虚拟机中,判断对象是否存活有2种方法:

  • 引用计数法
  • 引用链法(可达性分析法)

1.1. 引用计数法

方法描述

给 Java 对象添加一个引用计数器,每当有一个地方引用它时,计数器 +1;引用失效则 -1;

判断对象存活准则

当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。

优点

实现简单、判断高效

缺点

无法解决对象间相互循环引用的问题 : 即该算法存在判断逻辑的漏洞

背景

1
2
3
4
// 对象objA 和 objB 都有字段 name
// 两个对象相互进行引用,除此之外这两个人对象没有任何引用
objA.name = objB;
objB.name = objA;

问题

实际上这两个对象已经不可能再被访问,应该要被垃圾收集器进行回收

但因为他们相互引用,所以导致计数器不为0,这导致引用计数算法无法通知垃圾收集器回收该两个对象

++正由于该算法存在判断逻辑漏洞,所以Java虚拟机没有采用该算法判断Java是否存活++。

2.2、引用链法(可达性分析法)

很多主流商用语言(如Java、C#)都采用引用链法判断Java对象是否存活。

含3个步骤:

  1. 可达性分
  2. 析第一次标记 & 筛选
  3. 第二次标记 & 筛选

2.2.1 可达性分析

方法描述

将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索。

可作为 GC Root 的对象有:

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

Java 进行GC的时候会从GC root进行可达性判断,常见的GC Root有如下:

  1. Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  2. Thread - 活着的线程
  3. Stack Local - Java方法的local变量或参数
  4. JNI Local - JNI方法的local变量或参数
  5. JNI Global - 全局JNI引用
  6. Monitor Used - 用于同步的监控对象
  7. Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于”JVM持有”的了。

向下搜索的路径 = 引用链

判断对象是否可达标准

当一个对象到 GC Roots没有任何引用链相连时,则判断该对象不可达,比如上图右边的形式。

没有任何引用链相连 = GC Root到对象不可达 = 对象不可用

注意:Java9中已经明确弃用了finalize方法

为什么呢?简单说,你无法保证finalize什么时候执行,执行的是否符合预期。使用不当会影响 性能,导致程序死锁、挂起等。千万不要指望 finalize 去承担资源释放的主要职责,多让finalize作为后的“守门员”(也就是如下的行为两次标记筛选)

++可达性分析 仅仅只是判断对象是否可达,但还不足以判断对象是否存活 / 死亡,++
当在 可达性分析 中判断不可达的对象,只是“被判刑” = 还没真正死亡
不可达对象会被放在”即将回收“的集合里。

要判断一个对象真正死亡,还需要经历两个阶段:

第一次标记 & 筛选 finalize方法存在

第二次标记 & 筛选

2.2.2 第一次标记 & 筛选

对象在可达性分析中被判断为不可达后,会被第一次标记 & 准备被筛选

方法描述

  1. 不筛选:继续留在 ”即将回收“的集合里,等待回收;
  2. 筛选:从 ”即将回收“的集合取出

筛选标准

  • 该对象是否有必要执行 finalize()方法
  • 若有必要执行(人为设置),则筛选出来,进入下一阶段(第二次标记 & 筛选);
  • 若没必要执行,判断该对象死亡,不筛选 并等待回收
  • 当对象无 finalize()方法 或 finalize()已被虚拟机调用过,则视为“没必要执行”

2.2.3 第二次标记 & 筛选

当对象经过了第一次的标记 & 筛选,会被进行第二次标记 & 准备被进行 筛选

方法描述

该对象会被放到一个 F-Queue 队列中,并由 虚拟机自动建立、优先级低的Finalizer 线程去执行 队列中该对象的finalize(),finalize()只会被执行一次,但并不承诺等待finalize()运行结束。这是为了防止 finalize()执行缓慢 / 停止 使得 F-Queue队列其他对象永久等待。

筛选标准

在执行finalize()过程中,若对象依然没与引用链上的GC Roots直接关联或间接关联(即关联上与GC Roots 关联的对象),那么该对象将被判断死亡,不筛选(留在”即将回收“集合里)并等待回收

3、引用相关

不管是引用计数还是可达性分析,都和引用有关。 Java中引用包括:

1、强引用

最普遍的引用,只要强引用存在,对象肯定不会被回收。

++obj就是强引用。通过关键字new 创建的对象所关联的引用就是强引用++。 当JVM内存空间不足,JVM宁愿抛出OutOfMemory Error运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对 象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引 用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收 时机还是要看垃圾收集策略。

2、软引用

描述有用但并非必需的对象,当内存不足时,就会回收这些对象。Java中提供SoftReference类。 一般用于内存敏感的高速缓存中。

图片缓存框架中,“内存缓存”中的图片是以这种引用来保存,使得JVM在发生 OOM之前,可以回收这部分缓存

3、弱引用

描述非必需对象,强度比软引用更弱。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。Java中提供WeakReference类。

在静态内部类中,经常会使用虚引用。例如,一个类发送网络请求,承担callbac k的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回 收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏

4、虚引用

又称幽灵引用或幻影引用,最弱的一种引用关系。虚引用对对象的生命周期不会有任何影响,不能通过虚引用取得对象的实例。唯一的目的是,设置虚引用的对象在被回收时会收到

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集 器回收之前会收到一条系统通知。

这种引用的get()方法返回总是null,所以,可以想象,在平常的项目开发肯定 用的少。但是根据这种引用的特点,我想可以通过监控这类引用,来进行一些垃圾清理的动作。可以参考jdk内部cleaner使用

1、Java内存区域与内存溢出异常

发表于 2019-06-29 | 分类于 看书笔记 , 深入理解Java虚拟机

JVM整体分布

JVM 整体组成可分为以下四个部分:

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

各个组成部分的用途:

程序在执行之前先要把java代码转换成字节码(class文件),jvm首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是jvm的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。


而我们通常所说的jvm组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的Heap(堆)模块,那接下来我们来看运行时数据区(Runtime Data Area)是由哪些模块组成的。

主要介绍Java虚拟机内存的各区域,以及它们的作用、服务对象以及可能产生的问题。

按照上图Java虚拟机运行时数据区做个梳理

Java虚拟机运行时数据区

1、程序计数器(线程私有)

程序计数器是一块较小的内存空间,可以看做++当前线程所执行的字节码的行号指示器++。字节码解释器的工作原理就是通过改变程序计数器来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都依赖计数器完成。

2、Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的内存模型:++每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息++。每个方法的被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

通常说的“栈”就是指虚拟机栈,或者说是虚拟机栈中的局部变量表,它可以存放基本数据类型、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

异常情况:线程请求栈深度大于虚拟机所允许深度,StackOverflowError异常;虚拟机栈可以动态扩展,扩展无法申请到足够的内存时抛出OutOfMemoryError异常

1
2
3
4
5
6
7
//例子
public void sayHello() {
String name = "hello";
A a = new A();
}
//就在方法里定义了一个局部变量“name”,存放在栈中。
//a放在栈中,指向对中new A()

虚拟机栈中的局部变量表

虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈,八大数据类型(boolean、byte、char、short、int、float、long、double)。

  1. 对象引用(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
  2. ReturnAddress类型(指向了一条字节码指令的地址) 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。局部变量表所需的内存控件在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  3. 操作数栈:字节码指令指令给予操作数栈的指令架构,执行其指令需要从局部变量表或缓存中加载操作数到操作数栈中,改写再回到对应区域。
  4. 动态链接:多态,运行时根据实际类型进行虚方法分派

3、本地方法栈(线程私有)

本地方法栈与虚拟机栈的作用非常相似,区别是前者++为Native方法服务++,后者为Java方法服务。

4、Java堆(线程共享)

内存中最大一块,虚拟机启动时创建。++它的唯一目的就是存放对象实例,所有对象实例以及数组都要在堆上分配++。它是垃圾收集管理的主要区域,也被叫做GC堆,由于现在基本都采用分代收集算法,Java堆还可以细分为:新生代和老年代;

异常情况:堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常。

1
2
3
4
5
6
7
8
9
10
11
public void sayHello(String name) {
Student student = new Student(name);
student.study();
}
static int a = 10;
String a = "abc";

//“new Student(name)” 这个代码,就是创建了一个Student类型的对象实例放在堆中
//Student的“name”就是属于这个对象实例的数据,也会存放在Java堆内存里
//然后方法的栈帧的局部变量表里,会存放这个引用类型的“student”局部变量,即存放Student对象的地址
//注意:JDK1.8后 类变量和字符常量池都放在堆中,比如说"abc" 和 a=10都在堆中

5、方法区(线程共享)

它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。也被称为“永久代”,内存回收主要针对常量池和对类型的卸载。

异常情况:无法满足内存分配需求将会抛出OutOfMemoryError异常。

注意:方法区存放常量是指class文件中的类的常量,而非实例类中方法中的final常量。

例子:下面类中,abc三个常量存在常量池中,且常量池中只有一个1,abc指向1。de同理只有一个2在常量池,de指向2。

1
2
3
4
5
6
7
8
class Demo{
private final int a = 1;
private final int b = 1;
private final int c = 1;

private int d = 2;
private int e = 2;
}

常量池在CLASS文件中的位置:

常量池表示范围:

  • 运行时也可以添加新的常量,如String.intern()
  • 优点在于避免频繁的创建和销毁对象影响性能,实现对象共享
  • 基本来源于各个class文件中的常量池

6、运行时常量池

它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息,还有一项信息是++常量池,用于存放编译器生成各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中++。

异常情况:常量池无法再申请到内存时会抛出OutOfMemoryError异常。

7、直接内存

不是虚拟机运行一部分,也被规范定义的内存区域。但也会被频繁使用

HotSpot虚拟机在Java堆中对象分配、布局和访问全过程

1、对象创建

创建过程

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果有先执行相应的类加载过程,通过后为新生对象分配内存。

在堆中分配空间方式

指针碰撞
假设Java堆中内存绝对规整,所用内存和空闲内存各方一边,中间放着一个指针作为分界点指示器,所分配内存就是把指针往空闲空间挪动一段与对象大小相同的距离
空闲列表
Java堆中内存并不规整,使用内存和空闲内存相互交错,虚拟机必须维护一个列表,记录哪些内存块可用,在分配时找一块足够大的空间划分给对象,并更新记录表

线程安全

介绍
创建对象频繁,仅修改指针指向位置,在并发时并不是线程安全的,可能在给对象A分配内存,指针没来得及改,对象B又使用原来的指针来分配内存
解决
对分配内存空间的动作进行同步处理,采用CAS配上失败重试的方式保证更新操作原子性
把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲TLAB

后续过程

在内存分配完成后,分配到的内存空间都初始化为零值,对对象进行必要设置,找到对象的元数据等信息,存在在对象头,最后执行方法

2、对象的内存布局

对象头

1、用于存储对象自身运行时的数据如哈希吗,GC分代年龄等,是非固定的数据结构
2、类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪类的实例

实例数据

对象真正存储的有效信息,也是代码中定义的各种类型的字段内容

对齐填充

非必要的存在,起着占位符的作用,实例数据未补齐,就需要通过对齐填充来补全

3、对象的访问定位

关于对象访问,涉及到了Java栈、Java堆、方法区三个重要区域之间的关联关系

Object obj = new Object();

Object obj会在Java栈中的本地变量表,作为一个reference类型数据出现
new Object()在Java堆中形成一块存储了Object类型所有实例数据值
从方法区中查找对应的对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息

两种访问对象的方式,通过句柄访问对象、通过直接指针访问对象

2、对象创建与内存分布

发表于 2019-06-29 | 分类于 看书笔记 , 深入理解Java虚拟机

1、对象创建

创建对象流程图

当遇到关键字new指令时,Java对象创建过程便开始,整个过程如下:

步骤1:类加载检查

  1. 检查 该new指令的参数 是否能在 常量池中 定位到一个类的符号引用
  2. 检查 该类符号引用 代表的类是否已被加载、解析和初始化过

如果没有,需要先执行相应的类加载过程

  • 检查常量池中是否有即将要创建的这个对象所属的类的符号引用;若常量池中没有这个类的符号引用,说明这个类还没有被定义!抛出ClassNotFoundException;若常量池中有这个类的符号引用,则进行下一步工作;
  • 进而检查这个符号引用所代表的类是否已经被JVM加载;若该类还没有被加载,就找该类的class文件,并加载进方法区;若该类已经被JVM加载,则准备为对象分配内存;
  • 根据方法区中该类的信息确定该类所需的内存大小; 一个对象所需的内存大小是在这个对象所属类被定义完就能确定的!且一个类所生产的所有对象的内存大小是一样的!JVM在一个类被加载进方法区的时候就知道该类生产的每一个对象所需要的内存大小。

步骤2:为对象分配内存

  • 虚拟机将为对象分配内存,即把一块确定大小的内存从 Java 堆中划分出来,对象所需内存的大小在类加载完成后便可完全确定
  • 内存分配 根据 Java堆内存是否绝对规整 分为两种方式:指针碰撞 & 空闲列表

Java堆内存 规整:已使用的内存在一边,未使用内存在另一边 Java堆内存 不规整:已使用的内存和未使用内存相互交错

方式1:指针碰撞(规整)

如果内存是绝对规整的,即左右两边分别是已占用内存和闲置内存,中间有分界点的指针指示器,那么内存分配仅仅在于指针的移动,这种分配方式叫做“指针碰撞”。

那么,分配对象内存=把指针向未使用内存移动一段与对象大小相等的距离

方式2:空闲列表(不规整)

如果内存不规整,即已使用和空闲的内存交错分布,那么虚拟机必须维护一个列表,记录哪些内存可用。创建对象时从列表中找到一块足够大的空间划分给对象使用,同时更新列表记录。这种分配方式称为“空闲列表”

对象分配内存会存在线程不安全问题
对象创建在虚拟机中是非常频繁的操作,即使仅仅修改一个指针所指向的位置,在并发情况下也会引起线程不安全。如,正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存

解决线程不安全有两种方案:
1、同步处理分配内存空间的行为
虚拟机采用CAS(Compare and Swap,多线程操作中只有一个线程能成功,其他线程被通知竞争失败)配上失败重试的方式保证操作的原子性

2、把内存分配的动作按线程分在不同的空间中进行,这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全

每个线程在Java堆中预先分配一小块内存,各个线程的内存分配发生在自己的TLAB区域内,只有TLAB用完需要分配新的空间时才需要同步锁定,即每个线程在堆中都会有私有的分配缓冲区(TLAB),分配内存的时候在当前线程的TLAB上分配,只有旧的TLAB用完才给新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可通过-XX:+/-UseTLAB参数设定。

步骤3: 将内存空间初始化为零值

内存分配完成后,虚拟机需要将分配到的内存空间初始化为零(不包括对象头):内存空间分配完成后会初始化为0(不包括对象头)保证了Java代码中不赋初值就可以使用。

  1. 保证了对象的实例字段在使用时可不赋初始值就直接使用(对应值 = 0)
  2. 如使用本地线程分配缓冲(TLAB),这一工作过程也可以提前至TLAB分配时进行。

步骤4: 对对象进行必要的设置

  • 接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象GC分代年龄等信息存入对象头。JVM根据当前运行状态的不同设置不同的对象头。
  • 执行new指令后执行init方法后(初始化)才算一份真正可用的对象创建完成。

2、对象的内存分布

在 HotSpot 虚拟机中,分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

1 对象头区域

此处存储的信息包括两部分:

对象自身的运行时数据

  • 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
  • 该部分数据被设计成1个非固定的数据结构以便在极小的空间存储尽量多的信息(会根据对象状态复用存储空间)

对象类型指针

  • 即对象指向它的类元数据的指针(++方法区加载的类++)
  • 虚拟机通过这个指针来确定这个对象是哪个类的实例

特别注意

如果对象是数组,那么在对象头中还必须有一块用于记录数组长度的数据
因为虚拟机可以通过普通Java对象的元数据信息确定对象的大小,但是从数组的元数据中却无法确定数组的大小。

2 实例数据区域

存储的信息:对象真正有效的信息 即代码中定义的字段内容

3 对齐填充区域

存储的信息:占位符 占位作用
因为对象的大小必须是8字节的整数倍
而因HotSpot VM的要求对象起始地址必须是8字节的整数倍,且对象头部分正好是8字节的倍数。
因此,当对象实例数据部分没有对齐时(即对象的大小不是8字节的整数倍),就需要通过对齐填充来补全。

3、对象的访问定位

问:建立对象后,该如何访问对象呢?实际上需访问的是 对象类型数据 & 对象实例数据

答:Java程序 通过 栈上的引用类型数据(reference) 来访问Java堆上的对象 由于引用类型数据(reference)在 Java虚拟机中只规定了一个指向对象的引用,但没定义该引用应该通过何种方式去定位、访问堆中的对象的具体位置

所以对象访问方式取决于虚拟机实现。目前主流的对象访问方式有两种:
句柄访问、直接指针访问

1、句柄访问

句柄访问方式,会在Java堆中划分一块内存作为句柄池,Java栈中的reference存储对象所对应句柄的地址,而句柄中包含了对象的实例数据的地址和类型数据的地址信息。其实这是一个二级访问的过程。

优点:当对象移动时,reference保存的句柄地址不用改变,只需要修改句柄中实例数据的地址信息。

缺点:速度慢,两次指针定位开销大。

2、直接访问

直接访问,reference中存放的就是对象在堆中的实际地址。

优点:速度快,直接定位,因而hotspot采用此种方式。

比较:使用句柄的最大好处是reference中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁GC那么句柄方法好,如果是对象频繁访问则直接指针访问好。

4、对象分配

新生代 GC (Minor GC):发生在新生代的垃圾回收动作,频繁,速度快。

老年代 GC (Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

1、对象优先在Eden区分配

大多数情况,对象在新生代Eden区分配内存

当Eden区没有足够空间时,将发起一次Minor GC。

2、大对象直接进入老年代

大对象:如很长的字符串和数组。

大对象直接进入老年代可以节省大量的复制开销。

程序员应尽量避免”短命大对象“,”短命大对象“容易引起频繁的GC。

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

当对象进入Survivor区,设置年龄为1,在Survivor区每熬过一次GC,年龄加1。当年龄到某个值(默认15),将被晋升到老年代。

4、动态对象年龄判定

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

这样可以防止Survivor区大量复制和减小空间分配担保的可能性。

5、空间分配担保

进行Minor GC之前,会检查老年代最大可用连续空间是否大于新生代所有对象空间。如果是,那么这次Minor GC是安全的。否则,检查是否允许担保失败:

允许:比较老年代最大可用连续空间是否大于历次晋升到老年代的平均值,如果大于,则尝试Minor GC,显然这是有风险的。如果小于,或者设置不允许冒险,则要先进行一次Full GC。

6、如果老年代的对象需要引用新生代的对象,会发生什么呢?

为了解决这个问题,老年代中存在一个 card table ,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。这个 card table 由一个write barrier 来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。

123

Wfc

23 日志
5 分类
6 标签
GitHub E-Mail Weibo wszgkm
© 2019 Wfc
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Gemini v7.1.2