带你了解JVM到底是什么(二)
前言上一篇我们讲了类加载子系统、JVM运行时数据区的相关知识这篇来讲一下Java对象内存布局与创建流程、垃圾回收体系和内存模型JMM的相关核心内容一、Java对象内存布局与创建流程我们日常写的User user new User();看似简单的一行代码JVM 内部却完成了一系列复杂操作而创建出的对象在堆内存中也有固定的结构。这两部分内容我们分开讲解1.Java对象创建流程以User user new User();为例JVM 从执行这行代码到对象可用总共分为 6 个核心步骤步骤1检查类的加载状态JVM 首先会检查User 类是否已经被类加载器加载、链接、初始化过若已加载方法区已有 User 类的元数据直接进入下一步若未加载先执行类加载的完整流程加载→验证→准备→解析→初始化将 User 类的元数据存入方法区再继续。通俗理解就像要生产一个产品先确认产品的“设计图纸”类元数据是否已经存在没有就先绘制图纸。步骤2在堆内存分配空间类加载完成后JVM 会在堆内存中为 User 对象分配一块合适的内存空间。内存大小由 User 类的结构决定比如成员变量的数量和类型。分配内存有两种常用方式指针碰撞堆内存连续无碎片时使用直接移动指针划分内存空闲列表堆内存有碎片时使用JVM 维护空闲内存列表从中找到合适的内存块分配。步骤3默认零值初始化内存分配完成后JVM 会给对象的所有成员变量赋上默认初始值不是我们写的初始值是 JVM 规定的默认值。常见默认值基本类型int→0、boolean→false、char→\u0000引用类型String、数组等null。步骤4初始化对象头对象头是对象的“身份标识”JVM 通过它管理对象主要存储两类核心信息MarkWord核心信息区、类型指针具体见下面步骤5执行构造方法这是对象“成型”的关键一步JVM 会执行 User 类的构造方法默认或自定义给成员变量赋上我们写的初始值执行构造方法中的业务逻辑。示例如果我们定义public User() { this.age 18; this.name 张三; }此时 age 会从 0 变成 18name 会从 null 变成“张三”。步骤6引用赋值对象创建完成后JVM 会把堆内存中对象的内存地址赋值给栈中的 user 变量局部变量。此时 user 不再是 null而是指向堆中真正的 User 对象我们可以通过 user 调用对象的方法和字段。2.Java对象内存布局在HotSpot虚拟机中堆内存中的对象布局固定分为三部分1对象头Object Header对象的“身份证”占用内存空间固定64位 JVM 默认16字节是 JVM 管理对象的核心包含两部分MarkWord8字节核心信息区存储对象的哈希码、锁状态无锁、偏向锁等、GC 分代年龄JVM 靠这些信息判断对象是否需要回收、是否被锁定。类型指针8字节指向方法区中对象所属类的元数据确保对象能找到自己的“类模板”调用类中的方法和字段。2实例数据Instance Data对象的“核心内容”占用内存不固定是对象真正存储数据的地方存放对象的所有成员变量包括从父类继承来的成员变量。核心特点成员变量的存储顺序会根据类型对齐规则排列目的是提高内存利用率JVM 会优化内存布局无需我们手动处理。3对齐填充Padding凑数用的“占位符”占内存不固定作用是保证对象的总内存大小是 8 字节的整数倍HotSpot 虚拟机的要求提高内存访问效率。二、垃圾回收GC核心体系对象创建后总有“无用”的时候比如 user null这些无用对象会占用堆内存若不及时回收会导致内存溢出OOM。JVM 的垃圾回收GC机制就是自动识别并回收这些无用对象释放内存。注意回收的范围是堆内存对象、数组示例都存放在队中而方法区、虚拟机程序计数器等区域不参与GC1.哪些对象要被回收没有任何有效引用指向的对象可被GC回收判断算法有1已淘汰引用计数法给每个对象设置一个“引用计数器”有引用指向对象时计数器1引用失效时计数器-1当计数器为 0 时认为对象无用可被回收。优点简单高效无需遍历引用链致命缺点无法解决对象循环引用问题比如 A 引用 BB 引用 A两者都没有其他有效引用但计数器都为 1无法被回收。因为这个缺点HotSpot不使用该方法public class Test { public static void main(String[] args) { A a new A(); B b new B(); a.b b; // A 引用 B b.a a; // B 引用 A // 此时 a 和 b 都没有其他引用但计数器都为 1引用计数法无法回收 a null; b null; } } class A { B b; } class B { A a; }2主流算法可达性分析算法以“GC Roots根对象”为起点向下遍历所有引用链能被遍历到的对象是“可达对象”有用不回收无法被遍历到的对象是“不可达对象”无用可回收。这种算法完美解决了循环引用问题——即使 A 和 B 互相引用但只要没有 GC Roots 指向它们就会被判定为不可达对象可被回收。这里的GC Roots是什么是不会被回收的根对象常见的有虚拟机栈局部变量引用比如方法里定义的 User user new User()user 就是 GC Roots方法区静态变量引用比如 static User user new User()方法区常量引用比如 public static final User USER new User()本地方法栈 Native 方法引用JVM 调用本地方法时本地方法中的引用对象。2.四大引用体系Java的引用分为四种优先级从高到低回收时机不同对应不同的使用场景引用类型核心特点回收时机适用场景强引用默认最常用比如 User user new User()永不回收即使内存不足也不会回收会抛出 OOM日常业务对象比如用户、订单对象软引用SoftReference用 SoftReference 包装内存充足时保留内存不足时会被 GC 回收缓存比如图片缓存、接口结果缓存弱引用WeakReference用 WeakReference 包装生命周期短下次 GC 时无论内存是否充足必回收临时数据比如临时缓存、无需长期保留的数据虚引用PhantomReference用 PhantomReference 包装无法通过引用获取对象仅做回收通知对象回收时触发通知几乎不用仅用于监测对象回收3.核心垃圾回收算法GC算法的核心目的高效回收无用对象释放内存同时减少对程序运行的影响不同内存区域采用不同的算法1标记-清除算法最基础的算法分为“标记”和“清除”两步标记所有不可达对象然后直接清除。优点简单高效无需移动对象缺点会产生大量内存碎片后续分配大对象时可能无法找到连续内存触发频繁 GC。2复制算法新生代专用将新生代分为 Eden 区和两个 Survivor 区比例 8:1:1仅使用 Eden 区和一个 Survivor 区回收时将存活对象复制到另一个 Survivor 区然后清空 Eden 区和当前 Survivor 区。优点无内存碎片回收效率高新生代对象朝生夕死复制的对象少缺点浪费部分内存一个 Survivor 区始终空闲。3标记-整理算法老年代专用结合标记-清除和复制算法的优点先标记不可达对象然后将所有存活对象压缩到内存一端再清除不可达对象。优点无内存碎片充分利用内存缺点需要移动对象回收效率比复制算法低老年代对象存活久移动对象多。4.常见垃圾收集器垃圾收集器是GC算法的具体实现重点有以下四种Serial单线程收集器简单高效适合客户端模式比如桌面应用Parallel Scavenge/Parallel Old并行收集器追求吞吐量单位时间内回收内存多适合后端服务CMS并发标记清除追求低停顿回收时不影响程序运行老年代经典收集器G1分区收集可预测停顿时间JDK9 及以上默认收集器兼顾吞吐量和低停顿。三、JVM内存模型JMMJMMJava 内存模型是 JVM 规范的一部分核心解决多线程环境下共享变量的读写可见性、原子性、有序性问题——这也是并发编程中线程安全问题的根源。1.什么是JMMJMM不是实际的内存区域而是一套“规范”定义了多线程之间共享变量的访问规则所有共享变量非局部变量都存放在主内存中每个线程有自己的工作内存存储主内存中变量的副本线程只能读写自己的工作内存线程操作读取、赋值共享变量时需先从主内存读取到工作内存修改后再同步回主内存。通俗理解主内存是“公共仓库”线程的工作内存是“个人工作台”线程要操作公共仓库的货物必须先拿到自己的工作台修改后再放回公共仓库。图片来源于网络2.JMM三大特性原子性操作不可分割要么全部执行要么全部不执行比如i不是原子操作可能被线程打断可见性一个线程修改了共享变量其他线程能立刻看到修改后的值比如 volatile 关键字可保证可见性有序性禁止指令重排保证程序执行顺序和代码编写顺序一致指令重排是 JVM 优化手段可能导致多线程安全问题