ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JVM 核心基础|运行时数据区、类加载过程、双亲委派模型

JVM 核心基础|运行时数据区、类加载过程、双亲委派模型

前言

JVM 是 Java 体系的基石,也是后端面试必问模块。很多面试上来直接提问:JVM 内存区域分为哪几块?哪些线程共享?类加载流程是什么?双亲委派是什么?

本篇把 JVM 基础核心全部梳理完毕,包含概念、原理、代码示例,文末附带面试真题,看完可以直接应对面试。

一、什么是 JVM

JVM 全称 Java Virtual Machine,Java 虚拟机。JVM 运行在操作系统之上,不直接和硬件打交道,由操作系统为 JVM 提供硬件能力。

JVM 两大核心能力

  1. 加载并执行字节码文件(.class文件)Java 源码.java经过javac编译,生成平台无关的字节码.class,JVM 负责解析执行字节码,实现一次编译,到处运行。同一个 class 文件,可以在 Windows、Linux、Mac 不同操作系统的 JVM 上运行。
  2. 内存管理负责内存分配、内存回收,实现自动垃圾回收,开发者不需要手动释放对象内存。

重点:.java是源码;.class是字节码;JVM 只识别 class 字节码。

市面上主流 JVM 实现

JVM 是一套规范,不同厂商有不同实现:

表格

虚拟机归属 & 说明
HotSpot最主流,OpenJDK 开源,OracleJDK 商用版本,我们日常开发使用最多
JRockitBEA 公司,主打高性能,现在逐步退出市场
J9IBM 推出,内存占用低,IBM 产品大量使用
Dragonwell(龙井 JDK)阿里开源 JDK,基于 OpenJDK 改造,针对电商、金融高并发场景优化

JVM 整体架构总览

JVM 由 4 大子模块组成:

  1. 类加载器子系统:读取磁盘 class 字节码,加载进内存。
  2. 运行时数据区:内存核心区域,程序运行时的数据全部存放在这里。
  3. 执行引擎:解释器 + JIT 即时编译器 + 垃圾回收器;把字节码翻译成机器指令交给 CPU 执行。
  4. 本地方法接口 + 本地方法栈:调用native本地方法,对接操作系统底层 C/C++ 实现。

结构图:

流程简述:类加载器加载 class 字节码放入运行时数据区 → 执行引擎读取字节码执行,需要操作系统能力时调用本地方法接口。

二、JVM 运行时数据区

必背区分:线程共享、线程私有

  • 线程共享:方法区、堆,多个线程共用同一块内存。
  • 线程私有:虚拟机栈、本地方法栈、程序计数器 PC 寄存器;每新建一个线程,就分配一份私有内存,线程之间互不干扰。
内存区域线程归属存储内容
方法区线程共享类 / 接口元数据、静态变量、运行时常量池、JIT 代码缓存
堆线程共享对象实例、数组;GC 垃圾回收主要区域
虚拟机栈 (Java 栈)线程私有Java 方法执行,存储栈帧
本地方法栈线程私有native 本地方法执行信息
程序计数器 PC 寄存器线程私有字节码行号指示器,唯一一个不会发生 OOM 的内存区域

2.1 程序计数器 PC Register

  1. 线程私有,内存空间极小,可以忽略不计。
  2. 存储当前线程下一条要执行字节码指令的地址。
  3. 多线程切换的时候,会保存当前执行位置,线程切回来继续从该地址执行。
  4. 该区域没有 OOM。

举例子:字节码一共 10 行,现在执行到第 3 行,PC 寄存器记录 4,告诉执行引擎下一条执行第 4 行。

2.2 虚拟机栈 Java Stack

  1. 线程私有,线程创建的时候同步创建,线程销毁栈内存就释放。
  2. JVM 对虚拟机栈只有两种操作:栈帧入栈、栈帧出栈,先进后出。
  3. 每调用一个 Java 方法,就会创建一个栈帧(Stack Frame)压入栈;方法执行完毕,栈帧出栈销毁。
栈帧 Stack Frame 内部结构

栈帧就是一个方法执行需要的内存集合,包含 4 部分:

  1. 局部变量表:方法入参、方法内部局部变量。存储 8 大基本数据类型、对象引用 reference。
  2. 操作数栈:字节码执行的临时运算空间,做计算、赋值。
  3. 动态链接:指向方法区常量池符号引用,运行期转为直接引用。
  4. 方法返回地址:方法执行结束后回到调用者的位置。

演示代码理解栈帧入栈出栈

public class StackDemo { public static void main(String[] args) { System.out.println("main()开始"); test(); System.out.println("main()结束"); } public static void test(){ System.out.println("test执行"); } }

执行流程:

  1. main 方法调用,创建 main 栈帧,压栈;
  2. main 调用 test,创建 test 栈帧,压栈;
  3. test 执行完毕,test 栈帧弹出销毁;
  4. main 执行完毕,main 栈帧弹出销毁。

栈溢出 StackOverflowError

当递归调用深度过大,不断产生栈帧入栈,栈内存耗尽,抛出:Exception in thread "main" java.lang.StackOverflowError

栈大小可以通过 JVM 参数-Xss设置,比如-Xss256k。

两个面试小问题:

1、虚拟机栈会不会 GC?

不会。方法执行完成栈帧直接出栈释放,不靠垃圾回收。

2、方法内部局部变量是否线程安全?

如果局部变量不逃逸出方法作用域,线程安全。每个线程有自己虚拟机栈,每个线程的局部变量是私有副本;如果变量是 static 静态变量,属于线程共享,线程不安全。

2.3 本地方法栈

和虚拟机栈几乎一样,区别:虚拟机栈服务 Java 方法;本地方法栈专门服务 native 标记的本地方法。 native 方法底层是 C/C++ 实现,用来调用操作系统底层 API,比如Thread.start()底层就有 native 方法。线程私有。

2.4 方法区

重点:方法区是 JVM 规范定义的逻辑概念。 HotSpot 虚拟机实现变迁:

  • JDK1.7 及之前:永久代 PermGen,放在堆内存。
  • JDK1.8 及以后:废弃永久代,改用元空间 Metaspace,使用操作系统本地内存(堆外内存)。

方法区存储内容:

  1. 被虚拟机加载的类、接口元数据(类名、方法信息、字段信息)
  2. static 静态变量
  3. 运行时常量池
  4. JIT 即时编译器编译后的代码缓存

JDK7 重要改动:字符串常量池、静态变量从方法区移至 Java 堆。

注意:方法区虽然逻辑上属于堆概念,HotSpot 把它叫 Non‑Heap(非堆),物理上和堆分开。

方法区也可以发生 GC:主要用来卸载不再使用的类,条件苛刻,FullGC 的时候才会触发。

2.5 Java 堆 Heap

线程共享,JVM 启动时创建。几乎所有对象实例、数组分配在堆上,是 GC 垃圾回收最主要的区域。下一篇博客详细讲解堆分代模型。

三、类加载器 ClassLoader

class 字节码文件开头固定魔数:CA FE BA BE,用来校验文件是不是合法 class。

重要一句话:ClassLoader 只负责字节码的加载,这个类能不能正常运行由执行引擎负责。加载完成之后类元信息存放在方法区。

3.1 类完整生命周期

一个类从磁盘被加载,到最后被卸载,完整生命周期分为:加载 → 链接(验证、准备、解析) → 初始化 → 使用 → 卸载

① 加载
  1. 通过类全限定名获取 class 二进制字节流;字节流来源可以是磁盘、网络、数据库。
  2. 将字节流静态存储结构,转换为方法区运行时数据结构。
  3. 在堆中生成java.lang.Class对象,作为访问方法区类元数据的入口。

Java 类是懒加载:不是程序启动全部加载,用到的时候才加载。

② 链接,分为验证、准备、解析三步

1、验证校验 class 字节流格式、语义合法性,保证不会破坏虚拟机安全,防止恶意字节码攻击。

2、准备给类的static 静态变量分配内存,赋默认初始值。

注意:这里赋的是 JVM 默认零值,不是代码写的值。

  • static int a = 10;,准备阶段 a = 0;初始化阶段赋值为 10。
  • final static编译期常量,编译阶段直接赋值,准备阶段不会赋零值。
  • 实例变量不属于准备阶段,实例变量是 new 对象的时候,分配到堆内存赋值。

3、解析把常量池中的符号引用替换为直接引用。

  • 符号引用:只是一组字面描述,不知道内存真实地址。比如类名、方法名字符串。
  • 直接引用:真实内存指针,指向已经加载进内存的目标。
③ 初始化

执行类构造器<clinit>()方法。 把代码中写的静态变量赋值、执行静态代码块。这一步才执行程序员写的业务赋值逻辑。

<clinit>由编译器收集静态变量赋值语句、static {} 静态代码块合并生成。

④ 使用

实例化对象、调用静态方法、访问字段,业务代码使用这个类。

⑤ 卸载

满足条件,类可以被 GC 卸载,释放方法区内存。 卸载类必须同时满足 3 个条件:

  1. 该类的所有实例全部被回收,堆不存在该类对象。
  2. 该类对应的Class对象没有任何地方被引用。
  3. 加载这个类的 ClassLoader 没有被引用。

普通 AppClassLoader 加载的类不会卸载;自定义类加载器场景经常会发生类卸载,比如热部署。

3.2 四大类加载器

关键点:加载器之间的父子,不是类继承 extends 关系,是逻辑委派关系。

  1. BootstrapClassLoader 启动类加载器
    • C++ 语言实现,没有 Java 对象,获取它的 classLoader 会返回null。
    • 加载 JDK 核心模块,java.base等核心类库,比如String、Object。
  2. PlatformClassLoader 平台类加载器(JDK9 之后,替代旧的 ExtClassLoader 扩展类加载器)
    • Java 代码实现。
    • 加载 JDK 平台模块。
  3. AppClassLoader 应用程序类加载器(系统类加载器)
    • Java 实现,我们自己写的业务代码默认由它加载。
    • 加载‑cpclasspath 下的 class 文件、项目引入的 jar 包。
  4. 自定义类加载器继承ClassLoader,自己实现读取字节码逻辑;用于热部署、插件化、加密 class 解密。

测试代码,查看类加载器层级

public class ClassLoaderTest { public static void main(String[] args) { //自己写的类,AppClassLoader System.out.println(ClassLoaderTest.class.getClassLoader()); //父加载器 PlatformClassLoader System.out.println(ClassLoaderTest.class.getClassLoader().getParent()); //祖父 BootstrapClassLoader,输出null(C++实现) System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent()); //String由Bootstrap加载,输出null System.out.println(String.class.getClassLoader()); } }

输出示例:

jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc jdk.internal.loader.ClassLoaders$PlatformClassLoader@1b6d3586 null null

3.3 双亲委派模型(沙箱安全机制)

什么是双亲委派

核心思想:收到类加载请求,先向上委托父加载器加载;父加载器全部加载失败,自己才尝试加载。

完整流程:

“双亲” 并不是指两个父加载器,是向上一层一层委托的含义。

双亲委派两大核心目的

1、安全性(最重要)防止我们自定义java.lang.String类去替换 JDK 原生核心类。

当你写一个自己的 String 类,加载请求会委托给 Bootstrap,Bootstrap 已经加载过原生 java.lang.String,直接返回,不会加载我们写的恶意 String 类。

2、避免重复加载类父加载器已经加载过的类,子类不会重复加载,保证同一个全限定名类在 JVM 只有一份。

破坏双亲委派模型

双亲委派不是强制约束,可以被破坏。 重写ClassLoader的loadClass()方法,就可以破坏。

常见场景:SPI 机制、热部署、插件化框架。

JDK9 模块化之后对双亲委派做了部分改动。

四、本篇面试高频题汇总

  1. JVM 运行时数据区分为哪 5 块?哪些线程共享,哪些线程私有?
  2. 程序计数器会不会 OOM?虚拟机栈会不会 GC?
  3. 栈帧里面包含哪些内容?StackOverflowError 什么场景产生?
  4. 简述类完整生命周期,链接分为哪三步,每一步干什么?
  5. 准备阶段静态变量赋值特点,final static 常量是什么时候赋值?
  6. 四大类加载器分别是什么,Bootstrap 为什么获取返回 null?
  7. 双亲委派模型流程,两大作用?如何破坏双亲委派,什么场景会破坏?
  8. 什么样的类才会被卸载?三个条件?

小贴士

运行时数据区、类加载、双亲委派是 JVM 面试的第一道门槛,务必理解记忆。下一篇我们进入堆内存,学习分代模型、GC 触发条件、OOM 现象。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进