一、JVM 核心组成:7 大 “功能模块”
JVM 就像一个精密的 “运行工厂”,由 7 个核心部分协同工作,分别是程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区、运行时常量池、直接内存。
- 程序计数器:线程的 “指令导航仪”
Java 代码编译成字节码后,字节码引擎执行指令时,程序计数器就是 “导航仪”—— 专门记录当前执行的字节码指令地址,通过修改计数器的值定位下一条要执行的指令。
它是线程私有的 “专属导航”,每个线程都有自己的计数器,记录自身执行进度;若执行的是 Native 方法,计数器则会 “空白待机”。此外,该区域不会出现 OutOfMemoryError。
- Java 虚拟机栈:方法的 “临时工作间”
线程私有,生命周期和线程一致,就像线程的 “专属工作间”。每当一个方法执行,就会创建一个 “工作看板”(栈帧),里面存放着局部变量表(基本类型变量、对象引用)、操作栈、动态链接、方法出口等关键信息。
局部变量表的内存在编译时就已分配好,运行中不会改变大小。特别注意:若逃逸分析判定对象不会被外部引用且小于阈值,对象会直接在栈上分配;当线程请求的栈深度超过 JVM 允许范围,会抛出 StackOverflowError;若虚拟机栈扩展时无法申请到足够内存,则抛出 OutOfMemoryError。
- 本地方法栈:Native 方法的 “专属工作间”
功能和异常情况与 Java 虚拟机栈类似,相当于 “特殊工作间”,不过它只服务于 Native 方法的执行。
- Java 堆:对象的 “存储仓库”
是 JVM 内存中最大的一块区域,属于线程共享,虚拟机启动时就会创建,堪称对象的 “专属仓库”—— 几乎所有对象实例都在这里分配内存。
它也是垃圾回收(GC)的主要 “清扫区域”,又称 GC 堆,可细分为新生代和老年代;物理上允许处于不连续的内存空间。当堆内存无法完成实例分配且不能扩展时,会抛出 OutOfMemoryError。
- 方法区(非堆 / 永久代):类信息的 “档案库”
线程共享,就像 JVM 的 “档案库”,存储已加载的类信息、常量、静态变量、编译后的代码。
注意:JDK 8 后,常量池被移到 Java 堆中,类信息则存到了元区域(metaspace),“永久代” 的概念从此退出舞台。
- 运行时常量池:方法区的 “常量抽屉”
是方法区的一部分,相当于 “专属抽屉”,存储编译期生成的各种字面量和符号引用。当无法申请到足够内存时,会抛出 OutOfMemoryError。
- 直接内存:JVM 的 “外部存储区”
不属于 JVM 运行时数据区,但常被频繁使用,可理解为 JVM 的 “外部仓库”,用于提升内存操作效率。
二、JVM 运行原理:以 Tomcat 部署项目为例
用 Tomcat 部署项目的过程,就像一场 “业务处理流水线”,各模块协同运作:
- 类加载阶段:类加载器如同 “档案员”,将项目中的类信息加载到方法区(元区域),完成 “档案入库”。
- 实例创建阶段:Spring 容器化身 “工匠”,通过反射创建对应类的实例,并把这些实例对象存到 Java 堆这个 “存储仓库” 中。
- 请求处理阶段:当浏览器发起请求,Tomcat 会为其 “分配专属工人”(创建线程),每个线程都有自己的 “工作间”(栈内存)。
- 方法执行阶段:线程处理请求时,执行对象方法的过程就像 “堆叠工作看板”—— 每个方法按顺序压入栈,生成对应的栈帧,方法的局部变量都存放在栈帧里。
- 对象引用阶段:栈帧中的对象引用好比 “仓库钥匙”,通过它找到 Java 堆中对应的对象实例,进而完成业务逻辑处理。
- 资源释放阶段:请求处理完毕后,方法会从栈中 “移除看板” 并销毁,释放栈内存空间,完成 “工作间清理”。
关键提醒:整个过程中,程序计数器始终扮演 “导航仪” 角色,记录线程正在执行的 JVM 字节码指令地址。