在日常的编程活动中,我们经常使用 Java 编程语言编写代码,那么你们知道每天使用的 Java 语言中对象是分配在内存的何处呢,今天就带大家了解一下 JVM 的内存结构。

JVM 的内存区域主要可以分为线程隔离的数据区域和线程共享的数据区域,还有一块经常被我们忽略的直接内存区域。

JVM 运行时数据区域

线程隔离的数据区域

  • 程序计数器(PC):占用内存空间较小,主要存储字节码的行号指示器,控制程序的执行
  • JVM 栈:默认一个线程的栈大小为 1 MB
    • 栈帧
      • 局部变量表:储存基本数据类型和对象引用(地址信息)
        • 方法参数
        • 局部变量
      • 操作树栈(基于栈的指令集执行)
      • 动态连接
        • 一个指向运行时常量池中该栈帧所属方法的引用
      • 方法出口
        • 方法返回地址:主调方法的 PC 计数器值或者发生异常时异常处理器表确定的返回地址
  • 本地方法栈:为本地方法服务,在 Hotspot 虚拟机中与 JVM 栈合而为一

线程共享的数据区域

  • Java 堆:存储对象的实例
    • 运行时常量池(Java 8+)
      • 字面量
      • 符号引用
        • 类或接口
        • 字段
        • 类中方法
        • 接口中方法
对象的内存布局
  • 对象头
    • 对象运行时数据(64 位虚拟机为 64 bit,32 位虚拟机则为 32 bit)
      • 对象哈希码
      • 分代年龄
      • 锁指针
      • 线程 ID 和时间戳
      • 锁偏向模式
      • 锁标志
    • 类型元数据指针(4 byte,64 位虚拟机不开启指针压缩为 8 byte)
  • 实例数据
  • 对齐填充

直接内存

  • 元空间(metaspace):存储已加载类的元数据(Java 8 引入,代替以前的永久代)
  • 代码缓存:存储即时编译(JIT)后的代码缓存
  • 垃圾收集器占用
  • 直接字节缓存:主要用于 NIO 拷贝数据

2. 垃圾收集算法

a. 分代收集

  • Partial GC:部分区域的垃圾收集
    • Minor GC:新生代垃圾收集
    • Major GC:老年代垃圾收集
    • Mixed GC:整个新生代和部分老年代的垃圾收集
  • Full GC:Java 堆和方法区的垃圾收集

b. 垃圾收集算法

  • 标记-清除(Mark-Sweep)
  • 标记-复制(Mark-Copy)
  • 标记-整理(Mark-Compact)

3. 垃圾收集器

迄今为止,所有的垃圾收集器在根节点枚举的过程中都必须暂停用户线程

  • Serial + Serial Old:客户端模式下的默认选择,标记-复制收集新生代,标记-整理收集老年代
  • ParNew + CMS
  • Parallel Scavenge + Parallel Old:Java 9 之前的服务端默认配置
  • G1:Java 9 后的服务端默认配置

三大性能指标

  • 内存占用
  • 吞吐量
  • 延迟

CMS(Concurrent Mark Sweep)

  • 初始标记
  • 并发标记
  • 重新标记:使用增量更新
  • 并发清除

初始标记和重新标记都是需要暂停用户线程的

CMS 的几个缺陷

  • 并发对处理器的资源比较敏感,比较适合处理器核数较多的情况(大于 4 核)
  • 在并发阶段,用户线程可能会分配内存,进而造成老年代区域需要提前进行 GC 操作,一般又一个触发的阈值,太大或者太小都有性能问题
  • 标记-清除存在内存碎片的问题

G1

  • 初始标记
  • 并发标记
  • 最终标记:使用原始快照
  • 筛选回收

G1 为了维护不同 Region 之间的引用关系,至少需要花费大约相当于 Java 堆内存的 10% ~ 20% 的额外内存来维持收集器的正常工作,比较适合比较大的堆内存使用

低延迟垃圾收集器

Shenandoah 收集器

只在 OpenJDK 12+ 的版本出现,使用读屏障和转发指针来实现并发整理

  • 初始标记
  • 并发标记
  • 最终标记
  • 并发清理
  • 并发回收
  • 初始引用更新
  • 并发引用更新
  • 最终引用更新
  • 并发清理

ZGC 收集器

堆内存布局分为:小,中和大三种 Region。

使用读屏障和染色指针实现并发整理,但不支持 32 位平台,且不能开启指针压缩

将垃圾收集的停顿时间限制在 10 ms 以内

  • 初始标记
  • 并发标记)
  • 最终标记
  • 并发预备重分配:扫描所有的 region 区域,确定重分配集
  • 初始重分配:对重分配集中的 root 对象进行重分配,并更新引用地址信息
  • 并发重分配:对重分配集中剩下的对象进行重分配(复制到新的 Region),并维护一张转发表,维护旧引用地址信息到新引用地址信息的映射
  • 并发重映射:将对象引用地址信息进行更新,在并发标记过程中完成
染色指针

染色指针

  • Finalizable:对象只能被 finalizer 访问
  • Remapped:对象引用地址是新的
  • Marked 0 和 Marked 1:标记对象是可访问的,同时用于对比前后两次 GC 的结果
读屏障

从堆中读取对象的引用信息时,会加入读屏障

  • Remapped 为 1 时,直接返回引用地址
  • 当对象引用不在重分配集中,将 Remapped 位置为1,并返回引用地址
  • 当对象在重分配集中,并没有完成重分配,帮助其重分配,并在转发表中创建旧地址到新地址的记录
  • 根据转发表更新对象的新引用地址,并将 Remapped 设为 1,返回新的引用地址
JVM 相关配置
1
2
3
4
5
6
7
8
9
10
11
12
# 开启 ZGC
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
# 并发回收垃圾的线程。默认是总核数的 1/8,调大后 GC 变快,但会占用程序运行时的CPU资源,吞吐会受到影响
-XX:ConcGCThreads=2
# stop the world 时使用的线程数,默认为总核数的 3/5
-XX:ParallelGCThreads=6
# ZGC 发生的最小时间间隔,单位秒。基于固定时间间隔进行触发 ZGC
-XX:ZCollectionInterval=120
# ZGC 触发基于分配速率的自适应算法的修正系数,默认值为 2,数值越大,越早的触发 ZGC
-XX:ZAllocationSpikeTolerance=5
# 不开启 ZGC 的主动回收
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive

其他商业垃圾收集器

  • IBM J9
  • Azul C4
  • Azul Zing

看完这篇文章,有没有让你对 JVM 的内存结构有更深的了解呢?如果内容对你有所帮助,请点赞支持哦!


参考文献

  1. An Introduction to ZGC: A Scalable and Experimental Low-Latency JVM Garbage Collector
  2. 新一代垃圾回收器ZGC的探索与实践

更多精彩内容请关注扫码

KnowledgeCollision 微信公众号

Knowledge Collision 激发思维碰撞,IDEA 丛生