目录
[一、 JVM 是什么?](#一、 JVM 是什么?)
[二、 JVM 的核心架构:运行时数据区](#二、 JVM 的核心架构:运行时数据区)
[1. 程序计数器 (Program Counter Register)](#1. 程序计数器 (Program Counter Register))
[2. Java 虚拟机栈 (Java Virtual Machine Stacks)](#2. Java 虚拟机栈 (Java Virtual Machine Stacks))
[3. 本地方法栈 (Native Method Stack)](#3. 本地方法栈 (Native Method Stack))
[4. Java 堆 (Java Heap)](#4. Java 堆 (Java Heap))
[5. 方法区 (Method Area)](#5. 方法区 (Method Area))
[三、 核心概念:对象的生命周期](#三、 核心概念:对象的生命周期)
[四、 垃圾回收 (Garbage Collection, GC)](#四、 垃圾回收 (Garbage Collection, GC))
[1. 如何判断对象可以回收?("What")](#1. 如何判断对象可以回收?(“What”))
[2. 垃圾回收算法](#2. 垃圾回收算法)
[五、 类加载过程 (Class Loading)](#五、 类加载过程 (Class Loading))
一、 JVM 是什么?
JVM (Java Virtual Machine) 是一个虚拟的计算机 ,它通过软件模拟了硬件计算机的功能(如处理器、堆栈、寄存器等)。它是 Java 技术体系的核心,是实现 Java "一次编写,到处运行" (Write Once, Run Anywhere) 理念的关键。
- 作用 :加载
.class
字节码文件,并解释或编译执行它。 - 特点 :
- 基于栈:大部分指令的操作数都在操作数栈上进行。
- 屏蔽底层细节:为 Java 字节码提供了一个统一的、与底层操作系统和硬件无关的运行环境。
- 自动内存管理:内置垃圾回收器 (GC),自动管理内存的分配与回收。
二、 JVM 的核心架构:运行时数据区
JVM 在执行 Java 程序时会把它所管理的内存划分为若干个不同的数据区域。这些区域各有用途,是理解 JVM 的重点。
你可以将 JVM 内存结构主要分为两类:
- 线程私有:生命周期与线程相同,随线程而生,随线程而灭。
- 线程共享:所有线程都能访问,生命周期与 JVM 进程相同。
1. 程序计数器 (Program Counter Register)
- 特点 :线程私有、占用内存小、无
OutOfMemoryError
。 - 作用 :
- 可以看作是当前线程所执行的字节码的行号指示器。
- 字节码解释器通过改变这个计数器的值来选取下一条需要执行的指令。
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
2. Java 虚拟机栈 (Java Virtual Machine Stacks)
- 特点 :线程私有、生命周期与线程相同、会抛出
StackOverflowError
和OutOfMemoryError
。 - 作用 :描述 Java 方法执行的内存模型 。每个方法在执行的同时都会创建一个栈帧 (Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 栈帧的组成 :
- 局部变量表 (Local Variable Table) :存放编译期可知 的各种基本数据类型(
boolean
,byte
,char
,short
,int
,float
,long
,double
)、对象引用(reference
类型)和returnAddress
类型。以变量槽 (Slot) 为单位。 - 操作数栈 (Operand Stack):方法执行的工作区,用于存放方法执行过程中的中间计算结果。
- 动态链接 (Dynamic Linking):指向运行时常量池中该栈帧所属方法的引用。
- 方法返回地址 (Return Address):方法正常退出或异常退出的定义。
- 局部变量表 (Local Variable Table) :存放编译期可知 的各种基本数据类型(
3. 本地方法栈 (Native Method Stack)
- 作用 :与虚拟机栈非常相似,其区别不过是虚拟机栈为执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务(如用 C/C++ 编写的方法)。
- HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一。
4. Java 堆 (Java Heap)
- 特点 :线程共享 、JVM 管理的最大一块内存区域、在虚拟机启动时创建、GC 的主要管理区域 (因此常被称为 GC 堆 )、会抛出
OutOfMemoryError
。 - 作用 :存放对象实例和数组。几乎所有对象实例都在这里分配内存。
- 分代设计 (基于垃圾回收的角度):
- 新生代 (Young Generation) :新创建的对象首先在这里分配。分为 Eden 区和两个 Survivor 区 (S0, S1)。
- 老年代 (Old Generation/Tenured):在新生代中经历多次 GC 后仍然存活的对象会被晋升到这里。
- 元空间 (Metaspace) (JDK8+) / 永久代 (PermGen) (JDK7-):注意:方法区(见下文)的逻辑规范由堆的这部分来实现,但本质上不属于堆。
5. 方法区 (Method Area)
- 特点 :线程共享、会抛出
OutOfMemoryError
。 - 作用 :存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 实现变迁 :
- JDK7 及以前 :使用堆的永久代 (PermGen) 来实现方法区。
- JDK8 及以后 :彻底移除了永久代,使用本地内存 (Native Memory) 来实现,称为 元空间 (Metaspace)。
- 运行时常量池 (Runtime Constant Pool) :是方法区的一部分。Class 文件中除了有类的版本、字段、方法等描述信息外,还有一项是常量池表 (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
三、 核心概念:对象的生命周期
- 创建 (Creation) :使用
new
关键字时,JVM 首先在堆 中为对象分配内存(指针碰撞或空闲列表方式),然后执行构造函数 (<init>
) 进行初始化。 - 使用 (Usage) :对象被线程通过虚拟机栈上的引用 (reference) 来操作和访问。访问方式有句柄 和直接指针两种(HotSpot 使用直接指针,更快)。
- 不可达 (Unreachable):当对象不再被任何栈上的引用所指向(即 GC Roots 无法到达该对象)时,该对象就成为垃圾回收的候选对象。
- 回收 (Collection):由垃圾回收器在某个时刻(GC发生时)回收其占用的内存。
四、 垃圾回收 (Garbage Collection, GC)
GC 主要负责三件事:
- 哪些内存需要回收? (What)
- 什么时候回收? (When)
- 如何回收? (How)
1. 如何判断对象可以回收?("What")
- 引用计数法 :简单但无法解决循环引用问题,Java 未采用。
- 可达性分析算法 (Java 采用):
- 通过一系列称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,所走过的路径称为"引用链"。
- 当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
- 可作为 GC Roots 的对象包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中常量引用的对象。
- 方法区中类静态属性引用的对象。
- 本地方法栈中 JNI(即 Native 方法)引用的对象。
2. 垃圾回收算法
- 标记-清除 (Mark-Sweep) :先标记所有需要回收的对象,然后统一回收。问题:产生内存碎片。
- 复制 (Copying) :将内存分为两块,每次只使用一块。当这一块用完了,就将还存活的对象复制 到另一块上,然后把已使用的内存一次清理掉。优点:无碎片。缺点:浪费一半空间 。主要用于新生代(Eden 和 Survivor 的比例通常是 8:1:1)。
- 标记-整理 (Mark-Compact) :先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。优点:无碎片。缺点:有移动开销 。主要用于老年代。
- 分代收集 (Generational Collection) (现代GC器的主流算法):根据对象存活周期的不同将堆划分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
- 新生代 :每次 GC 都有大批对象死去,只有少量存活。→ 复制算法效率最高。
- 老年代 :对象存活率高。→ 标记-清除 或标记-整理算法。
五、 类加载过程 (Class Loading)
JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型。这个过程称为类加载。
类生命周期包括:加载 (Loading) -> 链接 (Linking) -> 初始化 (Initialization) -> 使用 (Using) -> 卸载 (Unloading) 。其中链接又分为验证 (Verification)、准备 (Preparation)、解析 (Resolution) 三步。
- 加载 :
- 通过类的全限定名获取定义此类的二进制字节流(
Class
文件或其他来源)。 - 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在堆 中生成一个代表这个类的
java.lang.Class
对象,作为方法区这些数据的访问入口。
- 通过类的全限定名获取定义此类的二进制字节流(
- 验证:确保 Class 文件的字节流符合当前 JVM 规范且不会危害 JVM 自身安全。
- 准备 :为类变量(static 变量) 在方法区 分配内存并设置初始零值 (如
0
,false
,null
)。注意 :static final
修饰的常量(ConstantValue
属性)会在此阶段被显式初始化。 - 解析 :将常量池内的符号引用 替换为直接引用的过程。
- 初始化 :执行类构造器
<clinit>()
方法的过程,该方法由编译器自动收集类中的所有类变量的赋值动作 和静态语句块 (static{}
) 中的语句合并产生。这时才真正开始执行类中定义的 Java 代码。
总结
JVM 是一个复杂的系统,但其核心基础可以概括为:
- 内存管理 :理解 堆、栈、方法区 的作用和区别是基石。
- 垃圾回收 :理解 分代收集思想 和 可达性分析原理 是关键。
- 类加载 :理解 双亲委派模型 (未在本文展开,但极其重要)和类加载的 五个阶段 是理解 Java 动态性的基础。