JVM 的简化架构
JVM 的简化架构,内存区域被称为运行时数据区
运行时数据区
主要包括:PC 寄存器、Java 虚拟机栈、Java 堆、方法区、运行时常量池、本地方法栈等
PC(Program Counter)寄存器
- 每个线程拥有一个 PC 寄存器,是线程私有的,用来存储的是当前线程下一条即将执行的指令的地址(而不是当前正在执行的指令的地址)
- 在创建一个新线程时,会自动创建一个 PC 寄存器,并将其初始化为 0,表示从字节码的第一条指令开始执行
- 执行本地方法时,PC 寄存器的值为 undefined
- PC 寄存器是一个较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域
- PC 寄存器的下一条指令地址是由 JVM 根据当前指令的“操作码”和“操作数”计算出来的,并由 JVM 自动将 PC 寄存器中的值更新为下一条指令的地址,以便继续执行下一条指令
Java 栈(Java Stack)
- 栈由一系列帧 Frame 组成的(Java 栈也叫做帧栈),是线程私有的,具有“先进后出”的特性
- 每个线程在执行方法时,都会创建一个对应的栈,用于存储该线程执行方法的信息。及线程每一次方法调用都会创建一个帧,并进行压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁
- 当线程调用的方法又调用了其他方法,则 JVM 会在当前的栈中创建一个新的栈,以此类推
- 一个线程可以同时对应多个栈,并且线程中的 Java 栈相互独立,当一个线程执行完一个方法后,该方法的栈会被销毁,线程继续执行其他方法的栈
- 帧保存的信息主要有:局部变量、操作数栈(所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等
- 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个插槽 slot 存放 32 位的数据,long、double 占两个插槽
- 栈的存取速度比堆快,仅次于 PC 寄存器
- 在栈中的数据的大小、生命周期是在编译期决定的,缺乏灵活性。栈的大小是由 Java 虚拟机在启动时,根据方法调用的嵌套深度和栈的大小限制等因素设置的,通常只有几百到几千字节
- 栈空间不足时,JVM 会抛出 StackOverflowError,而当 JVM 无法分配更多的栈空间时,会抛出 OutOfMemoryError
操作数栈(Operand Stack)
操作数栈是 JVM 在执行方法的过程中使用的临时数据区域,先进后出的结构。用于存储方法执行过程中的操作数、中间结果以及返回值等信息,JVM 在执行方法时,需要将操作数栈中的数据作为操作数进行相应的计算
Java 栈和操作数栈的区别
Java 栈和操作数栈是 JVM 执行方法时使用的两种不同的内存区域
- Java 栈主要用于存储方法的局部变量、操作数栈、方法调用信息等
- 操作数栈主要用于存储方法执行过程中的操作数和返回值
Java 堆(Java Heap)
- Java 堆(Java Heap)是 JVM 中用于存储对象实例和数组的内存区域,是 Java 中最大的一块内存区域。在 Java 程序运行期间,所有通过 new 关键字创建的对象都会被分配到 Java 堆中
- Java 堆是一个运行时数据区,它是在 JVM 启动时创建的,是可以被所有线程共享的内存区域
- Java 堆的大小可以通过虚拟机的启动参数来设置,并且可以动态扩展
- 在 Java 堆中分配对象实例时,Java 虚拟机会自动进行内存管理,负责对象的分配、回收等操作,开发人员不需要手动管理 Java 堆中的内存
- Java 堆中不仅仅存储了 Java 对象的实例数据,还包括对象头信息、数组长度等数据。在 Java 堆中分配对象实例时,JVM 会为每个对象分配一个对象头,用于存储对象的运行时数据和类型信息。同时,如果该对象是数组,JVM 还会为该对象分配一个额外的空间,用于存储数组的长度信息
- GC 主要是管理堆空间,对分代 GC 来说,堆也是分代的
- 堆的效率相对较慢
方法区(Method Area)
- JDK 7 时 HotSpot 虚拟机使用永生代(PermGen)实现的方法区
- JDK 8 移除了永生代,使用了元空间(Metaspace)实现的方法区,一部分数据存在元空间,一部分存在 Java 堆中
- 元空间与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中,它的别名是 Non-heap(非堆)
- 方法区是线程共享的内存区域,在 JVM 启动时创建的
- 方法区的大小可以通过虚拟机的启动参数来设置,并且可以动态扩展
- JDK 7:
- -XX:Permsize 设置永久代初始分配空间
- -XX:MaxPermsize 设定永久代最大可分配空间
- JDK 8:
- -XX:MetaspaceSize:设置初始的元空间大小
- -XX:MaxMetaspaceSize:设置元空间最大的大小
- JDK 7:
- 通常用来保存的信息有:
- 装载的类型信息:各类全限定名、类型标志、类的访问描述符等
- 类型的常量池
- 字段信息:修饰符、类型、名称等
- 方法信息:修饰符、返回类型、方法名、参数等
- 静态变量
- 指向类加载器的引用
- 指向 Class 实例的引用
- 方法表
- 运用时常量池
- Java 方法区和 Java 堆之间存在交集关系,Java 方法区并不等同于 Java 堆
元空间(Metaspace)
Java 元空间是从 JDK 8 开始引入的一种新的内存区域,用于存储类的元数据信息。在之前的版本中,Java 类的元数据信息是存储在永久代中的,在 JDK 8 中,永久代被移除,元数据信息被存储在了元空间中,解决了永久代大小有限的问题
运行时常量池(Runtime Constant Pool)
- 运行时常量池是方法区的一部分
- JDK 6 时运行时常量池存放在永久代,JDK 7 开始存放在堆中
- 运行时常量池是 Class 文件中每个类或接口的常量池表,在运行期间的表示形式
- 通常包括:类的版本、字段、方法、接口等信息
- 通常在加载类和接口到 JVM 后,就创建了相应的运行时常量池
本地方法栈(Native Method Stack)
本地方法栈是在 JVM 中用来支持 native 方法执行的栈
栈、堆、方法区之间的交互关系
todo
堆与方法区的关系
Java 堆内存
- Java 堆用来存放应用系统创建的对象和数组,所有线程共享 Java 堆
- Java 堆需要在逻辑上连续
- Java 堆是在运行期动态分配内存大小,自动进行垃圾回收
- Java 垃圾回收主要是回收堆内存,对分代 GC 来说,堆也是分代的
Java 堆的结构
- 新时代:用来存放新分配的对象,新生代中经过垃圾回收,没有被回收的对象将被复制到老年代
- 老年代存储对象比新生代对象的年龄大得多
- 老年代会存储一些大对象
- 整个堆大小 = 新生代 + 老年代
- 新时代 = Eden + 存活区
- JDK 8 之前的永生代,用来存放 Class、Method 等元信息,JDK 8 使用元空间(MetaSpace)代替,元空间并不存在于虚拟机中,而是直接使用本地内存
对象的内存布局
- 对象在内存中存储的布局(HotSpot),分为:对象头、实例数据和对齐填充
- 对象头:
- Mark Word:存储对象自身的运行数据,如:HashCode、GC 分代年龄、锁状态标志等
- 类型指针:对象指向它的类元数据的指针
- 实例数据:
- 真正存放对象实例数据的地方
- 对齐填充
- 这部分不一定存在,仅仅是占位符,HotSpot 要求对象的起始地址都是 8 字节的整数倍,如果不是就需要对齐填充
对象的访问定位
- 在 JVM 规范中只规定了 reference 类型是一个指向对象的引用,但没有规定这个引用具体如何去定位、访问堆中对象的具体位置
- 因此对象的访问方式取决于 JVM 的实现,目前主流的有:使用“句柄”和“指针”两种方式
- 句柄 在 Java 堆中会划分出一块内存来做句柄池,reference 中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址
- 指针 Java 堆中会存放访问类元数据的地址,reference 存储的是对象的地址
Java 内存分配参数
Trace 跟踪参数
- 打印 GC 的简要信息:-Xlog:gc、PrintGC(JDK 8)
- 打印 GC 的详细信息:-Xlog:gc*
- 指定 GC log 的位置,以文件输出:-Xlog:gc:flieName.log
- 每次 GC 后,都打印堆信息:-Xlog:gc+heap=debug
GC 日志格式
- GC 发生的时间,JVM 从启动以来经过的秒数
- 日志级别信息和日志类型标记
- GC 识别号
- GC 类型和说明 GC 的原因
- 容量:GC 前容量 -> GC 后容量(该区域总容量)
- GC 持续时间,单位秒
Java 堆的参数
- Xms:初始堆大小,默认是物理内存的 1/64
- Xmx:最大堆大小,默认物理内存的 1/4
- Xmn:新生代大小。默认是堆的 3/8,新生代过小会频繁的 GC,新生代过大会导致过多的 Full GC,程序停顿时间长
- -XX:+HeapDumpOnOutOfMemoryError:OOM 时导出堆到文件
- -XX:+HeapDumpPath:导出 OOM 的路径
- -XX:NewRatio:;老年代与新生代的比值,如果 xms = xmx,且设置了 xmn 的情况下,该参数不用设置
- -XX:SurvivorRatio:Eden 区和 survivor 区的大小比值,设置为 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 占整个新生代的 1/10
- -XX:OnOutOfMemoryError:在 OOM 时,执行一个脚本
Java 栈的参数
- Xss:通常只有几百 K,决定了函数调用的深度
元空间的参数
- -XX:MetaspaceSize:初始空间大小
- -XX:MaxMetaspaceSize:最大空间,默认没有限制
- -XX:MinMetaspaceFreeRatio:在 GC 之后,最小的 Metaspace 剩余空间容量的百分比
- -XX:MaxMetaspaceFreeRatio:在 GC 之后,最大的 Metaspace 剩余空间容量的百分比
字节码执行引擎
- JVM 字节码执行引擎,基本功能就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果
- 实现方式:
- 通过解释器直接解释执行字节码
- 通过编译器产生本地代码,编译执行
栈帧
- 是用于支持 JVM 进行方法调用和方法执行的数据结构
- 栈帧随着方法的调用而创建,随着方法结束而销毁
- 栈帧用于存放方法的局部变量、操作数栈、动态连接、方法返回地址等信息
- 局部变量表:用来存放方法参数和方法内部定义的局部变量的存储空间
- 以变量槽 slot 为单位,目前一个 slot 存放 32 位以内的数据类型
- 对于 64 位数据占 2 个 slot
- 对于实例方法,第 0 位 slot 存放的是 this,然后从 1 到 n,依次分配给参数列表
- 然后根据方法体内部定义的变量顺序和作用域分配 slot
- slot 是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾回收行为
- 操作数栈:用来存放方法运行期间,各个指令操作的数据
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
- 虚拟机在实现栈帧的时候可能会让两个栈帧出现部分重叠区域,以存放公用的数据
- 动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接
- 静态解析:类加载的时候,符号引用转换为直接引用
- 动态连接:运行期间转换为直接引用
- 方法返回地址:方法执行后返回的地址
- 方法调用:方法调用是确定具体调用那一个方法,并不涉及方法内部的执行过程
- 部分方法是直接在类加载的解析节点就确定了直接引用关系
- 对于实例方法,也称虚方法,因为重载和多态,需要运行期间动态委派
- 分派:静态分派和动态分派
- 静态分派:所有依赖静态类型来定位方法执行版本的分派方式,如:方法重载
- 动态分派:根据运行期的实际类型来定位方法执行版本的分配方式,如:方法覆盖
- 单分派和多分派:多余一个的就是多分派,只有一个的就是单分派
- 局部变量表:用来存放方法参数和方法内部定义的局部变量的存储空间
- JVM 通过基于栈的字节码解释执行引擎来执行指令,JVM 的指令集也是基于栈的
JVM 内存模型详解
1. JVM 架构概述
1.1 JVM 简化架构
JVM(Java Virtua### 2.2 Java 虚拟机栈(Java Virtual Machine Stack)
2.2.1 基本特征
- 线程私有:每个线程都有独立的虚拟机栈
- 数据结构:由栈帧(Frame)组成,遵循"后进先出"(LIFO)原则
- 生命周期:与线程相同,线程创建时创建,线程结束时销毁
2.2.2 栈帧结构
每个方法调用都会创建一个栈帧,栈帧包含以下组件:
局部变量表(Local Variable Table):
- 存储方法参数和局部变量
- 以变量槽(Slot)为单位,每个 Slot 存储 32 位数据
- long 和 double 类型占用 2 个 Slot
- 对于实例方法,第 0 个 Slot 存储 this 引用
操作数栈(Operand Stack):
- 存储方法执行过程中的操作数和中间结果
- 遵循后进先出原则
- 用于字节码指令的操作数传递
动态链接(Dynamic Linking):
- 每个栈帧都包含一个指向运行时常量池中该方法的引用
- 支持方法调用过程中的动态连接
方法返回地址(Return Address):
- 存储方法执行完毕后的返回地址
- 包括正常退出和异常退出两种情况
2.2.3 性能特点
- 访问速度:仅次于程序计数器,比堆内存快
- 大小限制:通常只有几百到几千字节
- 编译时确定:数据大小和生命周期在编译期确定
2.2.4 异常情况
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError:虚拟机栈可以动态扩展时,扩展时无法申请到足够的内存
2.2.5 操作数栈详解
- 定义:JVM 执行方法过程中使用的临时数据区域
- 特点:先进后出的数据结构
- 作用:存储方法执行过程中的操作数、中间结果和返回值
- 与 Java 栈的关系:操作数栈是栈帧的一部分,每个栈帧都有自己的操作数栈
2.3 本地方法栈(Native Method Stack)
2.3.1 基本特征
- 作用:为虚拟机使用到的本地(Native)方法服务
- 实现:虚拟机规范对本地方法栈没有强制规定,由虚拟机实现自由实现
- HotSpot 实现:将本地方法栈和虚拟机栈合二为一
2.3.2 异常情况
与虚拟机栈相同,可能抛出 StackOverflowError 和 OutOfMemoryError 异常。运行时数据区(Runtime Data Area),是 Java 程序运行时所需的各种内存空间的总称。
![JVM架构图]
1.2 运行时数据区组成
JVM 运行时数据区主要包括以下几个部分:
- 程序计数器(PC Register)
- Java 虚拟机栈(Java Virtual Machine Stack)
- 本地方法栈(Native Method Stack)
- Java 堆(Java Heap)
- 方法区(Method Area)
- 运行时常量池(Runtime Constant Pool)
1.3 内存区域分类
1.3.1 线程私有区域
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
1.3.2 线程共享区域
- Java 堆
- 方法区(包含运行时常量池)
2. 线程私有内存区域
2.1 程序计数器(PC Register)
2.1.1 基本特征
- 线程私有:每个线程都有独立的程序计数器
- 作用:存储当前线程下一条将要执行的字节码指令的地址
- 大小:是一个较小的内存空间
2.1.2 工作原理
- 初始化:线程创建时,PC 寄存器初始化为 0,从字节码第一条指令开始执行
- 指令跳转:JVM 根据当前指令的操作码和操作数计算下一条指令地址
- 自动更新:JVM 自动更新 PC 寄存器中的值为下一条指令的地址
2.1.3 特殊情况
- 执行 Java 方法:PC 寄存器记录正在执行的字节码指令地址
- 执行本地方法:PC 寄存器的值为 undefined(未定义)
- 异常处理:PC 寄存器是唯一不会出现 OutOfMemoryError 的内存区域 JVM 的简化架构,内存区域被称为运行时数据区
运行时数据区
主要包括:PC 寄存器、Java 虚拟机栈、Java 堆、方法区、运行时常量池、本地方法栈等
PC(Program Counter)寄存器
- 每个线程拥有一个 PC 寄存器,是线程私有的,用来存储的是当前线程下一条即将执行的指令的地址(而不是当前正在执行的指令的地址)
- 在创建一个新线程时,会自动创建一个 PC 寄存器,并将其初始化为 0,表示从字节码的第一条指令开始执行
- 执行本地方法时,PC 寄存器的值为 undefined
- PC 寄存器是一个较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域
- PC 寄存器的下一条指令地址是由 JVM 根据当前指令的“操作码”和“操作数”计算出来的,并由 JVM 自动将 PC 寄存器中的值更新为下一条指令的地址,以便继续执行下一条指令
Java 栈(Java Stack)
- 栈由一系列帧 Frame 组成的(Java 栈也叫做帧栈),是线程私有的,具有“先进后出”的特性
- 每个线程在执行方法时,都会创建一个对应的栈,用于存储该线程执行方法的信息。及线程每一次方法调用都会创建一个帧,并进行压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁
- 当线程调用的方法又调用了其他方法,则 JVM 会在当前的栈中创建一个新的栈,以此类推
- 一个线程可以同时对应多个栈,并且线程中的 Java 栈相互独立,当一个线程执行完一个方法后,该方法的栈会被销毁,线程继续执行其他方法的栈
- 帧保存的信息主要有:局部变量、操作数栈(所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等
- 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个插槽 slot 存放 32 位的数据,long、double 占两个插槽
- 栈的存取速度比堆快,仅次于 PC 寄存器
- 在栈中的数据的大小、生命周期是在编译期决定的,缺乏灵活性。栈的大小是由 Java 虚拟机在启动时,根据方法调用的嵌套深度和栈的大小限制等因素设置的,通常只有几百到几千字节
- 栈空间不足时,JVM 会抛出 StackOverflowError,而当 JVM 无法分配更多的栈空间时,会抛出 OutOfMemoryError
操作数栈(Operand Stack)
操作数栈是 JVM 在执行方法的过程中使用的临时数据区域,先进后出的结构。用于存储方法执行过程中的操作数、中间结果以及返回值等信息,JVM 在执行方法时,需要将操作数栈中的数据作为操作数进行相应的计算
Java 栈和操作数栈的区别
Java 栈和操作数栈是 JVM 执行方法时使用的两种不同的内存区域
- Java 栈主要用于存储方法的局部变量、操作数栈、方法调用信息等
- 操作数栈主要用于存储方法执行过程中的操作数和返回值
3. 线程共享内存区域
3.1 Java 堆(Java Heap)
3.1.1 基本特征
- 最大内存区域:Java 堆是 JVM 中最大的一块内存区域
- 线程共享:所有线程共享 Java 堆内存
- 对象存储:存储所有对象实例和数组
- 垃圾收集:垃圾收集器主要管理的内存区域
3.1.2 内存管理
- 动态分配:运行期动态分配内存大小
- 自动管理:JVM 自动进行内存管理,包括对象分配和回收
- 可调整大小:可以通过 JVM 启动参数设置堆大小,支持动态扩展
- 逻辑连续:在逻辑上必须是连续的内存空间
3.1.3 存储内容
- 对象实例数据:通过 new 关键字创建的所有对象
- 对象头信息:包含对象的运行时数据和类型信息
- 数组长度信息:数组对象的长度数据
- 实例变量:对象的成员变量数据
3.1.4 性能特点
- 访问速度:相对较慢,但容量大
- 内存效率:支持大容量数据存储
- 分代管理:支持分代垃圾收集策略
3.2 方法区(Method Area)
3.2.1 版本演进
JDK 7 及之前:
- 使用永久代(PermGen)实现方法区
- 永久代是堆内存的一部分
- 大小受限,容易发生 OutOfMemoryError
JDK 8 及之后:
- 移除永久代,引入元空间(Metaspace)
- 元空间使用本地内存,不再受堆大小限制
- 一部分数据存在元空间,一部分存在 Java 堆中
3.2.2 基本特征
- 线程共享:所有线程共享方法区
- 创建时机:JVM 启动时创建
- 可扩展:大小可以通过启动参数设置,并支持动态扩展
- 别名:也被称为 Non-heap(非堆)
3.2.3 存储内容
方法区主要存储以下信息:
类型信息:
- 类的全限定名
- 类的直接超类的全限定名
- 类的访问修饰符
- 类的类型(类或接口)
方法信息:
- 方法名称
- 方法的返回类型
- 方法参数的数量和类型
- 方法的访问修饰符
- 方法的字节码、操作数栈和局部变量表的大小
字段信息:
- 字段名称
- 字段类型
- 字段的访问修饰符
其他信息:
- 静态变量
- 类的常量池
- 指向类加载器的引用
- 指向 Class 实例的引用
- 方法表(用于支持动态分派)
3.2.4 JVM 参数配置
JDK 7 参数:
-XX:PermSize=<size> # 设置永久代初始大小
-XX:MaxPermSize=<size> # 设置永久代最大大小
JDK 8 参数:
-XX:MetaspaceSize=<size> # 设置元空间初始大小
-XX:MaxMetaspaceSize=<size> # 设置元空间最大大小
3.2.5 元空间(Metaspace)详解
- 定义:JDK 8 引入的新内存区域,用于存储类的元数据信息
- 位置:使用本地内存(直接内存),不在 JVM 堆中
- 优势:
- 避免了永久代大小限制的问题
- 减少了 GC 对元数据的影响
- 提高了类加载和卸载的性能
- 大小管理:默认情况下没有大小限制,受系统内存限制
3.3 运行时常量池(Runtime Constant Pool)
3.3.1 基本概念
- 定义:方法区的一部分,是 Class 文件中常量池表的运行时表示
- 来源:每个类或接口的常量池表在运行期的表示形式
- 创建时机:在类和接口被加载到 JVM 后创建
3.3.2 版本变化
- JDK 6:运行时常量池存放在永久代中
- JDK 7:字符串常量池移到 Java 堆中
- JDK 8:随着永久代的移除,运行时常量池移至元空间
3.3.3 存储内容
- 字面量:文本字符串、声明为 final 的常量值等
- 符号引用:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 运行时解析的常量:动态生成的常量
3.3.4 特性
- 动态性:相对于 Class 文件常量池的一个重要特征是具备动态性
- 可扩展性:运行期间也可以将新的常量放入池中
- 典型应用:String 类的 intern()方法
4. 内存区域交互关系
4.1 栈、堆、方法区的关系
![内存区域交互图]
4.1.1 对象创建过程
- 类加载检查:检查类是否已被加载、解析和初始化
- 内存分配:在 Java 堆中为新生对象分配内存
- 初始化零值:将分配的内存空间初始化为零值
- 设置对象头:设置对象的对象头信息
- 执行初始化:执行对象的初始化方法
4.1.2 引用关系
- 栈到堆:局部变量表中的引用指向堆中的对象
- 堆到方法区:对象头中的类型指针指向方法区中的类信息
- 方法区到堆:Class 对象存储在堆中,静态变量也在堆中
4.2 堆与方法区的详细关系
4.2.1 数据分布
- 对象实例:存储在 Java 堆中
- 对象类型数据:存储在方法区中
- 对象引用:存储在栈的局部变量表中
4.2.2 访问过程
- 通过栈中的 reference 定位到堆中的对象
- 通过对象头中的类型指针定位到方法区中的类信息
- 根据类信息进行方法调用和字段访问
5. Java 堆内存详解
5.1 堆内存特性
- 线程共享:Java 堆被所有线程共享,用于存放应用系统创建的对象和数组
- 逻辑连续:Java 堆在逻辑上必须是连续的内存空间
- 动态管理:运行期动态分配内存大小,自动进行垃圾回收
- 分代设计:对于分代 GC 来说,堆采用分代结构
5.2 Java 堆的结构
5.2.1 分代划分
- 新生代(Young Generation):用来存放新分配的对象
- Eden 区:对象首次分配的区域
- Survivor 区:分为 From 和 To 两个区域,用于存放经过一次 GC 后存活的对象
- 老年代(Old Generation):存放生存时间较长的对象和大对象
- 永久代/元空间:JDK 8 之前使用永久代存放类元信息,JDK 8 之后使用元空间替代
5.2.2 大小关系
- 整个堆大小 = 新生代 + 老年代
- 新生代 = Eden 区 + Survivor 区(From + To)
- 默认比例:新生代:老年代 = 1:2,Eden:Survivor = 8:1
- 老年代会存储一些大对象
- 整个堆大小 = 新生代 + 老年代
- 新时代 = Eden + 存活区
- JDK 8 之前的永生代,用来存放 Class、Method 等元信息,JDK 8 使用元空间(MetaSpace)代替,元空间并不存在于虚拟机中,而是直接使用本地内存
对象的内存布局
- 对象在内存中存储的布局(HotSpot),分为:对象头、实例数据和对齐填充
- 对象头:
- Mark Word:存储对象自身的运行数据,如:HashCode、GC 分代年龄、锁状态标志等
- 类型指针:对象指向它的类元数据的指针
- 实例数据:
- 真正存放对象实例数据的地方
- 对齐填充
- 这部分不一定存在,仅仅是占位符,HotSpot 要求对象的起始地址都是 8 字节的整数倍,如果不是就需要对齐填充
对象的访问定位
- 在 JVM 规范中只规定了 reference 类型是一个指向对象的引用,但没有规定这个引用具体如何去定位、访问堆中对象的具体位置
- 因此对象的访问方式取决于 JVM 的实现,目前主流的有:使用“句柄”和“指针”两种方式
5.4.1 句柄访问
- 实现方式:在 Java 堆中划分一块内存作为句柄池
- 访问过程:reference 存储句柄地址,句柄中包含对象实例数据与类型数据的具体地址信息
- 优点:reference 中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针
- 缺点:增加了一次指针定位的时间开销
5.4.2 直接指针访问
- 实现方式:reference 直接存储对象地址
- 访问过程:通过 reference 直接访问对象,对象头中存储类型数据的指针
- 优点:访问速度快,节省了一次指针定位的时间开销
- 缺点:对象被移动时需要修改 reference 本身
- 应用:HotSpot 虚拟机采用的方式
6. JVM 参数配置
6.1 GC 跟踪参数
6.1.1 JDK 8 及之前版本
-XX:+PrintGC # 打印GC简要信息
-XX:+PrintGCDetails # 打印GC详细信息
-XX:+PrintGCTimeStamps # 打印GC时间戳
-Xloggc:gc.log # 指定GC日志文件
6.1.2 JDK 9 及之后版本
-Xlog:gc # 打印GC简要信息
-Xlog:gc* # 打印GC详细信息
-Xlog:gc:fileName.log # 指定GC日志文件位置
-Xlog:gc+heap=debug # 每次GC后打印堆信息
6.1.3 GC 日志格式说明
- 时间戳:GC 发生的时间,JVM 从启动以来经过的秒数
- 日志级别:日志级别信息和日志类型标记
- GC 标识:GC 识别号
- GC 类型:GC 类型和产生 GC 的原因
- 内存变化:GC 前容量 -> GC 后容量(该区域总容量)
- 持续时间:GC 持续时间,单位为秒
6.2 Java 堆参数配置
6.2.1 基本堆参数
-Xms<size> # 初始堆大小,默认物理内存的1/64
-Xmx<size> # 最大堆大小,默认物理内存的1/4
-Xmn<size> # 新生代大小,默认是堆的3/8
6.2.2 分代比例参数
-XX:NewRatio=<ratio> # 老年代与新生代的比值
-XX:SurvivorRatio=<ratio> # Eden区与Survivor区的大小比值
6.2.3 调试和监控参数
-XX:+HeapDumpOnOutOfMemoryError # OOM时自动导出堆转储文件
-XX:HeapDumpPath=<path> # 设置堆转储文件的保存路径
-XX:OnOutOfMemoryError=<cmd> # OOM时执行指定脚本
6.3 Java 栈参数配置
-Xss<size> # 设置线程栈大小,通常几百KB
# 决定了函数调用的最大深度
6.4 元空间参数配置
-XX:MetaspaceSize=<size> # 元空间初始大小
-XX:MaxMetaspaceSize=<size> # 元空间最大大小,默认无限制
-XX:MinMetaspaceFreeRatio=<percent> # GC后最小剩余空间百分比
-XX:MaxMetaspaceFreeRatio=<percent> # GC后最大剩余空间百分比
7. 字节码执行引擎
7.1 执行引擎概述
JVM 字节码执行引擎的基本功能是输入字节码文件,进行解析和处理,最后输出执行结果。
7.1.1 实现方式
- 解释执行:通过解释器直接解释执行字节码
- 编译执行:通过编译器产生本地代码,编译执行
- 混合模式:解释执行与编译执行并存
7.2 栈帧(Stack Frame)
7.2.1 栈帧概念
- 定义:用于支持 JVM 进行方法调用和方法执行的数据结构
- 生命周期:随着方法调用而创建,随着方法结束而销毁
- 存储内容:方法的局部变量、操作数栈、动态连接、方法返回地址等信息
7.2.2 局部变量表(Local Variable Table)
功能:存放方法参数和方法内部定义的局部变量
存储单位:
- 以变量槽(Slot)为基本单位
- 一个 Slot 存放 32 位以内的数据类型
- 64 位数据类型占用 2 个 Slot
分配规则:
- 对于实例方法:第 0 位 Slot 存放 this 引用
- 参数列表:从第 1 位到第 n 位依次分配给参数
- 局部变量:根据方法体内定义顺序和作用域分配
- Slot 复用:为节省栈帧空间,Slot 可以复用,可能影响垃圾回收行为
7.2.3 操作数栈(Operand Stack)
功能:存放方法运行期间各个指令操作的数据
特性:
- 操作数栈中元素的数据类型必须与字节码指令严格匹配
- 虚拟机实现时可能让两个栈帧出现部分重叠区域,以存放公用数据
7.2.4 动态连接(Dynamic Linking)
功能:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用
连接方式:
- 静态解析:类加载时,符号引用转换为直接引用
- 动态连接:运行期间转换为直接引用
7.2.5 方法返回地址(Return Address)
功能:存储方法执行后返回的地址信息
7.3 方法调用
7.3.1 方法调用概念
- 定义:确定具体调用哪一个方法,不涉及方法内部的具体执行过程
- 解析时机:部分方法在类加载的解析阶段就确定了直接引用关系
- 动态特性:实例方法(虚方法)因重载和多态需要运行期动态委派
7.3.2 分派机制
静态分派:
- 定义:依赖静态类型来定位方法执行版本的分派方式
- 典型应用:方法重载(Overload)
- 解析时机:编译期确定
动态分派:
- 定义:根据运行期实际类型来定位方法执行版本的分派方式
- 典型应用:方法覆盖(Override)
- 解析时机:运行期确定
单分派与多分派:
- 单分派:根据一个宗量对目标方法进行选择
- 多分派:根据多个宗量对目标方法进行选择
7.4 基于栈的字节码解释执行引擎
7.4.1 执行模型
- JVM 通过基于栈的字节码解释执行引擎来执行指令
- JVM 的指令集采用基于栈的设计
- 操作数栈作为计算过程中操作数的临时存储区域
7.4.2 指令执行过程
- 取指令:从字节码流中读取下一条指令
- 解码:解析指令的操作码和操作数
- 执行:根据指令类型执行相应操作
- 更新程序计数器:指向下一条指令
8. 总结
8.1 JVM 内存模型核心要点
- 内存区域划分:PC 寄存器、Java 栈、本地方法栈、堆、方法区、运行时常量池
- 线程私有区域:PC 寄存器、Java 栈、本地方法栈
- 线程共享区域:Java 堆、方法区、运行时常量池
- 堆内存管理:分代设计、对象内存布局、访问定位机制
- 执行引擎:基于栈的字节码解释执行、栈帧结构、方法调用机制
8.2 性能调优关键点
- 合理配置堆大小:根据应用特点设置 Xms、Xmx 参数
- 分代比例调整:通过 NewRatio、SurvivorRatio 优化 GC 性能
- 栈大小配置:使用 Xss 参数避免 StackOverflowError
- 元空间监控:JDK 8+需要关注 MetaspaceSize 配置
- GC 日志分析:通过 GC 日志分析内存使用模式和性能瓶颈
8.3 最佳实践建议
- 监控内存使用:定期检查各内存区域的使用情况
- 优化对象生命周期:减少不必要的对象创建和长生命周期对象
- 合理使用本地缓存:避免内存泄漏
- 定期分析 GC 日志:识别性能问题和优化机会
- 压测验证配置:在类似生产环境中验证 JVM 参数设置