1. 概述
- 垃圾收集(Garbage Collection,GC)
- 程序计数器、虚拟机栈、本地方法栈
- 每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的
- 这几个区域的内存分配和回收都具备确定性
- 当方法结束或者进程结束时,内存自然就跟着回收了
- Java 堆、方法区
- 一个接口的多个实现类需要的内存可能会不同
- 一个方法所执行的不同条件分支所需要的内存可能不同
- 只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象
- 这部分内存的分配和回收时动态的
2. 对象已死
- 垃圾回收器在对堆进行回收前,第一件事就是要确定这些对象之中哪些还 “存活” ,哪些已经 “死去”(不可能再被任何途径使用的对象)
2.1 引用计数算法(代码运行不出来)
-
引用计数算法(Reference Counting)
- 占用一些额外的内存空间来进行计数
- 原理简单,判定效率较高
-
实现:
- 在对象中添加一个引用计数器
- 每当有一个地方引用它时,计数器就加 1
- 当引用失效时,计数器就减 1
- 任何时刻计数器为 0 的对象就是不可能再被使用的
- 在对象中添加一个引用计数器
-
主流 Java 虚拟机不使用此算法管理内存的原因
-
必须要配合大量额外处理才能保证正确地工作
- 单纯的引用计数就很难解决对象之间相互循环引用的问题
/** * @author BlackOrnate */ public class JVM_Test { public Object instance = null; private static final int value = 1024 * 1024; private byte[] bigSize = new byte[2 * value]; public static void main(String[] args) { JVM_Test objA = new JVM_Test(); JVM_Test objB = new JVM_Test(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
-
2.2 可达性分析算法
-
可达性分析(Reachability Analysis)
-
通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,则证明此对象是不可能再被使用的
对象 object 5 、 object 6 、 object 7 虽然互有关联,但是它们到 GC Roots 是不可达的,因此它们将会被判定为可回收对象
-
-
固定可作为 GC Roots 的对象
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象
- 各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象
- Java 类的引用类型静态变量
- 在方法区中常量引用的对象
- 字符串常量池(String Table)里的引用
- 在本地方法栈中 JNI(Native 方法)引用的对象
- Java 虚拟机内部的引用
- 基本数据类型对应的 Class 对象
- 一些常驻的异常对象(NullPointException 、OutOfMemoryError)等
- 系统类加载器
- 所有被同步锁(synchronized 关键字)持有的对象
- 反映 Java 虚拟机内部情况的 JMXBean 、JVMTI 中注册的回调、本地代码缓存等
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象
-
根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象 “临时性” 地加入
2.3 再谈引用
-
Java 的 4 种引用强度(依次减弱)
-
强引用(Strongly Reference)
-
在程序代码之中普遍存在的引用赋值
Object obj = new Object();
-
无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
-
-
软引用(Soft Reference)
- 还有用,但非必须的对象
- 只被软引用联系着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收
- 如果这次回收还没有足够的内存,才会抛出内存溢出异常
- 用 SoftReference 类来实现软引用
-
弱引用(Weak Reference)
- 非必须的对象,但是强度比软引用更弱一些,只能生存到下一次垃圾收集发生为止
- 当垃圾收集开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 用 WeakReference 类来实现弱引用
-
虚引用(Phantom Reference)
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例
- 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
- 用 PhantomReference 类来实现虚引用
-
2.4 生存与死亡
-
在可行性分析算法中判定为不可达的对象,也不一定会被回收
-
真正判定为回收,需要经过 2 次标记过程
- 如果对象在进行可行性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,删选条件是此对象是否有必要执行 finalize() 方法
- 如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么虚拟机将其视为 “没有必要执行”
- 如果有必要执行 finalize() 方法,那么该对象将被放置在一个名为 F-Queue 的队列中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法
- 执行:虚拟机会触发此方法的执行,但是不一定会等待此方法运行结束
- 防止方法运行缓慢或出现死循环,导致队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃
- 执行:虚拟机会触发此方法的执行,但是不一定会等待此方法运行结束
- 稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记
- 不被标记的方法:对象需要重新与引用链上的任何一个对象建立关联
- 如果对象在进行可行性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,删选条件是此对象是否有必要执行 finalize() 方法
/**
* @author BlackOrnate
*/
public class JVM_Test {
public static JVM_Test value = null;
public void isAlive() {
System.out.println("Alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize");
JVM_Test.value = this;
}
public static void main(String[] args) throws InterruptedException {
value = new JVM_Test();
// 对象第一次自救
value = null;
System.gc();
// 暂停0.5秒等待 Finalizer 方法执行
Thread.sleep(500);
if (value != null) {
value.isAlive();
} else {
System.out.println("Dead");
}
// 自救失败
value = null;
System.gc();
// 暂停0.5秒等待 Finalizer 方法执行
Thread.sleep(500);
if (value != null) {
value.isAlive();
} else {
System.out.println("Dead");
}
}
}
/*
结果:
Finalize
Alive
Dead
*/
- 第二次自救失败的原因
- 任何一个对象的 finalize() 方法都只会被系统自动调用一次
- 如果对象面临下一次回收,它的 finalize() 方法不会被再次执行
2.5 回收方法区
-
方法区垃圾收集的 “性价比” 通常是比较低的
-
方法区的垃圾收集主要回收两部分内容
- 废弃的常量
- 没有任何对象引用常量池中的该常量,而且垃圾收集器判断确有必要
- 常量池中的其他类(接口)、方法、字段的符号引用与此类似
- 不再使用的类型
- 满足三个条件
- 该类所有的实例都已经被回收
- Java 堆中不存在该类及其任何派生自类的实例
- 加载该类的类加载器已经被回收
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- 该类所有的实例都已经被回收
- Java 虚拟机被允许对满足上诉三个条件的无用类进行回收
- 满足三个条件
- 废弃的常量
3. 垃圾收集算法
- 垃圾收集算法
- 引用计数式垃圾收集(Reference Counting GC ,直接垃圾收集)
- 追踪式垃圾收集(Tracing GC ,间接垃圾收集)
3.1 分代收集理论
-
分代收集(Generational Collection)
-
建立在的两个分代假说
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是照升夕灭的
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
-
以上两个假说奠定了多款常用的垃圾收集器的一致的设计原则
- 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
- 一个区域中大多数对象都是照升夕灭,难以熬过垃圾收集过程,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间
- 一个区域中都是难以消亡的对象,虚拟机可以使用较低的频率来回收
- 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
-
Java 堆被划分为 2 个区域
- 新生代(Young Generation)
- 每次垃圾收集时都发现有大笔对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
- 老年代(Old Generation)
- 新生代(Young Generation)
-
对于分代收集理论添加的第 3 条法则
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于通带引用来说仅占极少数
-
隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
-
在新生代上建立的一个全局的数据结构(记忆集,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用
3.2 标记 - 清除算法
-
标记 - 清除(Mark - Sweep)
-
标记
- 标记出所有需要回收的对象 / 标记出所有存活的对象
-
清除
- 统一回收掉所有被标记的对象 / 统一回收掉所有未被标记的对象
-
-
主要缺点
- 执行效率不稳定
- 如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作
- 导致标记和清除两个过程的执行效率都随对象数量增长而降低
- 内存空间的碎片化管理问题
- 标记、清除后会产生大量不连续的内存碎片
- 空间碎片过多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
- 执行效率不稳定
3.3 标记 - 复制算法
-
标记 - 复制(Mark - Copy)
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
- 当这一块的内存用完,就将还活着的对象复制到另外一个块上面,然后再把已使用过的内存空间一次清理掉
-
每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可
-
主要缺点
- 将可用内存缩小为了原来的一半
-
采用这种算法回收新生代
3.4 标记 - 整理算法
- 标记 - 整理(Mark - Compact)
- 标记
- 与 “标记 - 清除” 算法相同
- 整理
- 让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
- 标记
是否移动回收后的存活对象
-
标记 - 清除算法:非移动式的回收算法
-
标记 - 整理算法:移动式的回收算法
-
移动存活对象
- 在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作
- 这种对象移动操作必须全程暂停用户应用程序才能进行
- Stop The World
-
不移动i存活对象
- 弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决
-
垃圾收集的停顿时间
- 不移动对象停顿时间会更短,甚至可以不需要停顿
-
整个程序的吞吐量(赋值器(Mutator ,使用垃圾收集的用户程序)与收集器的效率总和)
- 移动对象更划算
- 不移动对象会使得收集器的效率提升,但因内存分配和访问相比垃圾收集频率要高得多,总吞吐量仍然下降
4. HotSpot 的算法细节实现
4.1 根节点枚举
4.2 安全点
4.3 安全区域
4.4 记忆集与卡表
4.5 写屏障
4.6 并发的可达性分析
5. 经典垃圾收集器
5.1 Serial 收集器
- 单线程工作的收集器
- 单线程
- 只会使用一个处理器或一条收集线程去完成垃圾收集工作
- 在进行垃圾收集时,必须暂停其他所有工作线程,直至收集结束
- Stop The World:(3.4)
- 单线程
- 优点
- 简单而高效,对于内存资源受限的环境,它是所有收集器里额外**内存消耗(Memory Footprint)**最小的
- 对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销,专心做来及收集自然可以获得最高的单线程收集效率
5.2 ParNew 收集器
- Serial 收集器的多线程并行版本
- 同时使用多条线程进行垃圾收集
- 其余的行为都与 Serial 收集器完全一致
- 控制参数
- 收集算法
- Stop The World
- 对象分配规则
- 回收策略
5.3 Parallel Scavenge 收集器
-
并行收集多线程收集器。标记 - 复制算法
-
特点
-
达到一个可控制的吞吐量(Throughput)
- 吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值
吞吐量 = 运行用户代码时间 运行用户代码时间 + 运行垃圾收集时间 吞吐量 = \frac{运行用户代码时间}{运行用户代码时间 + 运行垃圾收集时间} 吞吐量=运行用户代码时间+运行垃圾收集时间运行用户代码时间
- 吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值
-
-
停顿时间越短就越适合需要与用户交互或需要保证服务相应质量的程序,良好的响应速度能提升用户体验
-
高吞吐量可以最高效地利用处理器资源,尽快完成程序的运算任务
5.4 Serial Old 收集器
- 单线程收集器,标记 - 整理算法
5.5 Parallel Old 收集器
- 多线程并发收集,标记 - 整理算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUb3Ehph-1665192778816)(第3章 垃圾收集器与内存分配策略.assets/Parallel Old 收集器.jpg)]
5.6 CMS 收集器
-
CMS(Concurrent Mark Sweep) 收集器
- 以获取最短回收停顿时间为目标
- 标记 - 清除算法
-
工作流程
- 初始标记(CMS initial mark)
- 标记 GC Roots 能直接关联到的对象
- 速度很快
- 并发标记(CMS concurrent mark)
- 从 GC Roots 的直接关联对象开始遍历整个对象图
- 耗时较长
- 不需要停顿用户线程
- 可以与垃圾收集线程一起并发运行
- 重新标记(CMS remark)
- 修正并发标记期间,因用户程序继续运作而导致标记产生变动过的那一部分对象的标记记录
- 时间比初始标记稍长,远比并发标记短
- 并发清除(CMS concurrent sweep)
- 清理删除掉标记阶段判断的已经死亡的对象
- 可以与用户线程同时并发
- 初始标记(CMS initial mark)
-
从整体来说 CMS 收集器的内存回收过程是与用户线程一起并发执行的
-
优点
- 并发收集
- 低停顿
-
缺点
- 对处理器资源非常敏感
- 默认回收线程数为 处理器核心数量 + 3 4 \frac{处理器核心数量 + 3}{4} 4处理器核心数量+3
- 占用一部分线程(处理器的计算能力)而导致应用程序变慢
- 无法处理 “浮动垃圾”(Floating Garbage),有可能出现 “Concurrent Mode Failure” 失败进而导致另一次完全 “Stop The World” 的 Full GC 的产生
- 浮动垃圾:在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行时会伴随新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束之后,CMS 无法在当次收集中处理掉它们,只能留待下一次垃圾收集时再清理掉
- 收集结束时会有大量空间碎片产生
- 对处理器资源非常敏感
5.7 Garbage First 收集器
-
Garbage First(G1)
- 面向服务端应用的垃圾收集器
-
Mixed GC 模式
- 面向堆内存任何部分来组成回收集(Collection Set,CSet)进行回收
- 衡量标准:哪块内存中存放的垃圾数量最多,回收收益最大
-
工作流程
- 初始标记(Initial marking)
- 标记 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地再可用的 Region 中分配新的对象
- 停顿线程,但耗时很短
- 并发标记(Concurrent marking)
- 从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找到要回收的对象
- 耗时较长
- 可以与用户程序并发运行
- 扫描完成后,需要重新处理 SATB 记录下的再并发时有引用变动的对象
- 最终标记(Final marking)
- 对用户线程做另一个短暂的暂停
- 用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录
- 筛选回收(Live Data Counting and Evacuation)
- 更新 Region 的统计数据
- 对各个 Region 的回收价值和成本进行排序
- 根据用户所期望的停顿时间来指定回收计划
- 可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间
- 暂停用户线程,由多条收集器并行完成
- 初始标记(Initial marking)
-
在延迟可控的情况下获得尽可能高的吞吐量
6. 低延迟垃圾收集器
-
衡量垃圾收集器的三个重要指标
- 内存占用(Footprint)
- 吞吐量(Throughput)
- 延迟(Latency)
- 三者构成 ”不可能三角“
- 延迟最为重要
-
各款收集器的并发情况
6.1 Shenandoah 收集器
-
不仅进行并发的垃圾标记,还要并发地进行对象清理后的整理工作
-
与 G1 至少有 3 个明显的不同之处
-
支持并发的整理算法
- G1:回收阶段可以多线程并行,不能与用户线程并发
- Shenandoah:下方补充
-
分代收集
-
G1:使用分代集
-
Shenandoah:默认不使用分代收集
-
-
记忆集
- G1:使用耗费大量内存和计算资源去维护的记忆集
- Shenandoah:使用 **“连接矩阵”(Connection Matrix)**的全局数据结构来记录跨 Region 的引用关系
- 降低处理跨代指针时的记忆集维护消耗
- 降低了伪共享问题的发生概率
-
- 工作流程
- 初始标记(Initial Marking):首先标记与 GC Roots 直接关联的对象
- Stop The World
- 停顿时间与堆大小无关,只与 GC Roots 的数量相关
- 并发标记(Concurrent Marking):遍历对象图,标记出全部可达的对象
- 与用户线程一起并发
- 时间长短你取决于堆中存活对象的数量以及对象图的结构复杂程度
- 最终标记(Final Marking):处理剩余的 SATB 扫描,并在这个阶段统计出回收价值最高的 Region ,将这些 Region 构成一组回收集(Collection Set)
- 会有一小段短暂的停顿
- 并发标记(Concurrent Cleanup):清理那些整个区域内连一个存活对象都没有找到的 Region
- 这类 Region 被称为 Immediate Garbage Region
- 并发回收(Concurrent Evacuation)(核心差异):把回收集里面的存活对象先复制一份到其他未被使用的 Region 之中
- 通过读屏障和 “Brooks Pointers” 的转发指针来解决并发问题
- 运行的时间取决于回收集的大小
- 初始引用更新(Initial Update Reference):
- 引用更新:把堆中所有指向旧对象的引用修正到复制后的新地址
- 建立一个线程集合点,确保是所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务
- 会产生一个非常短暂的停顿
- 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作
- 与用户线程一起并发
- 运行的时间取决于内存中设计的引用数量的多少
- 不需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值
- 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于 GC Roots 中的引用
- 最后一次停顿
- 停顿时间只与 GC Roots 的数量相关
- 并发清理(Concurrent Cleanup):回收回收集中那些 Region 的内存空间,供以后新对象分配使用
- 初始标记(Initial Marking):首先标记与 GC Roots 直接关联的对象
6.2 ZGC 收集器
-
ZGC(Z Garbage Collector):基于 Region 内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等计数来实现可并发的标记 - 整理算法的,以低延迟为首要目标的一款垃圾收集器
-
内存布局:基于 Region 的堆内存布局
-
具有动态性:动态创建和销毁,动态的区域容量大小
-
小型 Region (Small Region):容量固定为 2 MB
- 用于放置小于 256 KB 的小对象
-
中型 Region (Medium Region):容量固定为 32 MB
- 用于放置大于等于 256 KB 但小于 4 MB 的对象
-
大型 Region (Large Region):容量不固定,可以动态变化,但必须为 2 MB 的整数倍
- 用于放置 4 MB 或以上的大对象
- 每个大型 Region 中只会存放一个大对象
- 不会被重分配(复制对象的收集器,下方补充)
-
-
并发整理算法的实现
-
使用读屏障和染色指针
-
染色指针:直接将标记信息记在引用对象的指针上
-
在 Linux 中的染色指针,将其高 4 位提取出来存储 4 个标志信息
- 通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(被移动过)、是否之呢通过 finalize() 方法才能被访问到
- 压缩了原本 46 位的地址空间,导致可以管理的内存不可以超过 2 4 2 = 4 T B 2 ^ 42 = 4 TB 242=4TB
-
优点
- 一旦某个 Region 的存活对象被移走后,这个 Region 立即就能够被释放和重用掉,而不用等待整个堆中所有指向该 Region 的引用都被修正后才能清理
- 大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,就可以省去一些专门的记录操作
- 作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能
-
-
-
工作流程
- 并发标记(Concurrent Mark):遍历对象图做可达性分析
- 经过初始标记、最终标记的短暂停顿
- 标记是在指针上而不是在对象上进行的,标记阶段回更新染色指针中的 Marked 0 、Marked 1 标志位
- 并发预备重分配(Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region ,将这些 Region 组成重分配集(Relocation Set)
- 每次回收都会扫描所有的 Region
- ZGC 的重分配集只是决定了里面的存活对象会被重新复制到其他的 Region 中,里面的 Region 会被释放
- 标记的过程是针对全堆的
- 并发重分配(Concurrent Relocate)(核心部分):把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系
- 指针的 “自愈(Self - Healing)” 能力
- 由于染色指针的存在,ZGC 收集器能仅从引用上就明确得知一个对象是否处于重分配集之中
- 如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据 Region 上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象
- 只有第一次访问旧对象会陷入转发,只慢一次
- 一旦重分配集中某个 Region 的存活对象都复制完毕后,这个 Region 就可以立即释放用于新对象的分配(转发表会保留)
- 即使堆中有很多指向这个对象的未更新指针也没有问题,这些旧指针一旦被使用,它们都是可以自愈的
- 指针的 “自愈(Self - Healing)” 能力
- 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用
- 不是一个必须要 “迫切” 去完成的任务
- 最多只是第一次使用时多一次转发和修正操作
- 清理旧引用的目的是为了不变慢
- 不是一个必须要 “迫切” 去完成的任务
- 并发标记(Concurrent Mark):遍历对象图做可达性分析
-
短暂停顿也只与 GC Roots 大小相关而与堆内存大小无关
-
与 G1 垃圾收集器比较
-
优点
- G1 :通过写屏障来维护记忆集,次啊能处理跨代指针,得以实现 Region 的增量回收
- 记忆集占用大量的内存空间,写屏障会对正常程序运行造成额外负担
- ZGC :没有使用记忆集,没有分代
- 完全没有用到写屏障,减少给用户程序带来的运行负担
- G1 :通过写屏障来维护记忆集,次啊能处理跨代指针,得以实现 Region 的增量回收
-
缺点
- ZGC :能承受的对象分配速率不高
-
指针可以存储额外信息的原因
- 64位系统中
- 理论可以访问的内存为 2 64 = 16 E B 2 ^ {64} = 16 EB 264=16EB 字节
- 实际上在 AMD64 架构中只支持到 52 位的地址总线和 48 位的虚拟地址空间,所以目前 64 位的硬件实际上能够支持的最大内存只有
2
48
=
256
T
B
2 ^ {48} = 256 TB
248=256TB
- 基于 3 方面考虑
- 需求:用不到那么多内存
- 性能:地址越宽在做地址转换时需要的页表级数越多
- 成本:消耗更多的晶体管
- 基于 3 方面考虑
- 操作系统会施加自己的约束
- Linux
- 进程虚拟地址空间:47 位
- 物理地址空间:46 位
- Windows
- 物理地址空间:44 位
- Linux
Java 虚拟机可以重新定义内存中的指针的原因(P116)(后期补)
- 无论中间过程如何,程序代码最终都要转换为机器指令流交付给处理器去执行,处理器只会把整个指针都视为一个内存地址来对待