前言JVM 是 Java 体系的基石也是后端面试必问模块。很多面试上来直接提问JVM 内存区域分为哪几块哪些线程共享类加载流程是什么双亲委派是什么本篇把 JVM 基础核心全部梳理完毕包含概念、原理、代码示例文末附带面试真题看完可以直接应对面试。一、什么是 JVMJVM 全称 Java Virtual MachineJava 虚拟机。JVM 运行在操作系统之上不直接和硬件打交道由操作系统为 JVM 提供硬件能力。JVM 两大核心能力加载并执行字节码文件.class文件Java 源码.java经过javac编译生成平台无关的字节码.classJVM 负责解析执行字节码实现一次编译到处运行。同一个 class 文件可以在 Windows、Linux、Mac 不同操作系统的 JVM 上运行。内存管理负责内存分配、内存回收实现自动垃圾回收开发者不需要手动释放对象内存。重点.java是源码.class是字节码JVM 只识别 class 字节码。市面上主流 JVM 实现JVM 是一套规范不同厂商有不同实现表格虚拟机归属 说明HotSpot最主流OpenJDK 开源OracleJDK 商用版本我们日常开发使用最多JRockitBEA 公司主打高性能现在逐步退出市场J9IBM 推出内存占用低IBM 产品大量使用Dragonwell龙井 JDK阿里开源 JDK基于 OpenJDK 改造针对电商、金融高并发场景优化JVM 整体架构总览JVM 由 4 大子模块组成类加载器子系统读取磁盘 class 字节码加载进内存。运行时数据区内存核心区域程序运行时的数据全部存放在这里。执行引擎解释器 JIT 即时编译器 垃圾回收器把字节码翻译成机器指令交给 CPU 执行。本地方法接口 本地方法栈调用native本地方法对接操作系统底层 C/C 实现。结构图流程简述类加载器加载 class 字节码放入运行时数据区 → 执行引擎读取字节码执行需要操作系统能力时调用本地方法接口。二、JVM 运行时数据区必背区分线程共享、线程私有线程共享方法区、堆多个线程共用同一块内存。线程私有虚拟机栈、本地方法栈、程序计数器 PC 寄存器每新建一个线程就分配一份私有内存线程之间互不干扰。内存区域线程归属存储内容方法区线程共享类 / 接口元数据、静态变量、运行时常量池、JIT 代码缓存堆线程共享对象实例、数组GC 垃圾回收主要区域虚拟机栈 (Java 栈)线程私有Java 方法执行存储栈帧本地方法栈线程私有native 本地方法执行信息程序计数器 PC 寄存器线程私有字节码行号指示器唯一一个不会发生 OOM 的内存区域2.1 程序计数器 PC Register线程私有内存空间极小可以忽略不计。存储当前线程下一条要执行字节码指令的地址。多线程切换的时候会保存当前执行位置线程切回来继续从该地址执行。该区域没有 OOM。举例子字节码一共 10 行现在执行到第 3 行PC 寄存器记录 4告诉执行引擎下一条执行第 4 行。2.2 虚拟机栈 Java Stack线程私有线程创建的时候同步创建线程销毁栈内存就释放。JVM 对虚拟机栈只有两种操作栈帧入栈、栈帧出栈先进后出。每调用一个 Java 方法就会创建一个栈帧Stack Frame压入栈方法执行完毕栈帧出栈销毁。栈帧 Stack Frame 内部结构栈帧就是一个方法执行需要的内存集合包含 4 部分局部变量表方法入参、方法内部局部变量。存储 8 大基本数据类型、对象引用 reference。操作数栈字节码执行的临时运算空间做计算、赋值。动态链接指向方法区常量池符号引用运行期转为直接引用。方法返回地址方法执行结束后回到调用者的位置。演示代码理解栈帧入栈出栈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执行); } }执行流程main 方法调用创建 main 栈帧压栈main 调用 test创建 test 栈帧压栈test 执行完毕test 栈帧弹出销毁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使用操作系统本地内存堆外内存。方法区存储内容被虚拟机加载的类、接口元数据类名、方法信息、字段信息static 静态变量运行时常量池JIT 即时编译器编译后的代码缓存JDK7 重要改动字符串常量池、静态变量从方法区移至 Java 堆。注意方法区虽然逻辑上属于堆概念HotSpot 把它叫 Non‑Heap非堆物理上和堆分开。方法区也可以发生 GC主要用来卸载不再使用的类条件苛刻FullGC 的时候才会触发。2.5 Java 堆 Heap线程共享JVM 启动时创建。几乎所有对象实例、数组分配在堆上是 GC 垃圾回收最主要的区域。下一篇博客详细讲解堆分代模型。三、类加载器 ClassLoaderclass 字节码文件开头固定魔数CA FE BA BE用来校验文件是不是合法 class。重要一句话ClassLoader 只负责字节码的加载这个类能不能正常运行由执行引擎负责。加载完成之后类元信息存放在方法区。3.1 类完整生命周期一个类从磁盘被加载到最后被卸载完整生命周期分为加载 → 链接验证、准备、解析 → 初始化 → 使用 → 卸载① 加载通过类全限定名获取 class 二进制字节流字节流来源可以是磁盘、网络、数据库。将字节流静态存储结构转换为方法区运行时数据结构。在堆中生成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 个条件该类的所有实例全部被回收堆不存在该类对象。该类对应的Class对象没有任何地方被引用。加载这个类的 ClassLoader 没有被引用。普通 AppClassLoader 加载的类不会卸载自定义类加载器场景经常会发生类卸载比如热部署。3.2 四大类加载器关键点加载器之间的父子不是类继承 extends 关系是逻辑委派关系。BootstrapClassLoader 启动类加载器C 语言实现没有 Java 对象获取它的 classLoader 会返回null。加载 JDK 核心模块java.base等核心类库比如String、Object。PlatformClassLoader 平台类加载器JDK9 之后替代旧的 ExtClassLoader 扩展类加载器Java 代码实现。加载 JDK 平台模块。AppClassLoader 应用程序类加载器系统类加载器Java 实现我们自己写的业务代码默认由它加载。加载‑cpclasspath 下的 class 文件、项目引入的 jar 包。自定义类加载器继承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输出nullC实现 System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent()); //String由Bootstrap加载输出null System.out.println(String.class.getClassLoader()); } }输出示例jdk.internal.loader.ClassLoaders$AppClassLoader2437c6dc jdk.internal.loader.ClassLoaders$PlatformClassLoader1b6d3586 null null3.3 双亲委派模型沙箱安全机制什么是双亲委派核心思想收到类加载请求先向上委托父加载器加载父加载器全部加载失败自己才尝试加载。完整流程“双亲” 并不是指两个父加载器是向上一层一层委托的含义。双亲委派两大核心目的1、安全性最重要防止我们自定义java.lang.String类去替换 JDK 原生核心类。当你写一个自己的 String 类加载请求会委托给 BootstrapBootstrap 已经加载过原生 java.lang.String直接返回不会加载我们写的恶意 String 类。2、避免重复加载类父加载器已经加载过的类子类不会重复加载保证同一个全限定名类在 JVM 只有一份。破坏双亲委派模型双亲委派不是强制约束可以被破坏。 重写ClassLoader的loadClass()方法就可以破坏。常见场景SPI 机制、热部署、插件化框架。JDK9 模块化之后对双亲委派做了部分改动。四、本篇面试高频题汇总JVM 运行时数据区分为哪 5 块哪些线程共享哪些线程私有程序计数器会不会 OOM虚拟机栈会不会 GC栈帧里面包含哪些内容StackOverflowError 什么场景产生简述类完整生命周期链接分为哪三步每一步干什么准备阶段静态变量赋值特点final static 常量是什么时候赋值四大类加载器分别是什么Bootstrap 为什么获取返回 null双亲委派模型流程两大作用如何破坏双亲委派什么场景会破坏什么样的类才会被卸载三个条件小贴士运行时数据区、类加载、双亲委派是 JVM 面试的第一道门槛务必理解记忆。下一篇我们进入堆内存学习分代模型、GC 触发条件、OOM 现象。