とんぼの気持ちとんぼの気持ち
Home
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Cloud Netflix
  • Zookeeper

    • Zookeeper
  • 分布式锁

    • 分布式锁
  • 分布式事务

    • 分布式事务
GitHub
Home
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Cloud Netflix
  • Zookeeper

    • Zookeeper
  • 分布式锁

    • 分布式锁
  • 分布式事务

    • 分布式事务
GitHub
  • JVM 内存模型详解

JVM 的简化架构

JVM 的简化架构,内存区域被称为运行时数据区 Snipaste_2023-04-10_22-26-17.png

运行时数据区

主要包括:PC 寄存器、Java 虚拟机栈、Java 堆、方法区、运行时常量池、本地方法栈等

PC(Program Counter)寄存器

  1. 每个线程拥有一个 PC 寄存器,是线程私有的,用来存储的是当前线程下一条即将执行的指令的地址(而不是当前正在执行的指令的地址)
  2. 在创建一个新线程时,会自动创建一个 PC 寄存器,并将其初始化为 0,表示从字节码的第一条指令开始执行
  3. 执行本地方法时,PC 寄存器的值为 undefined
  4. PC 寄存器是一个较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域
  5. PC 寄存器的下一条指令地址是由 JVM 根据当前指令的“操作码”和“操作数”计算出来的,并由 JVM 自动将 PC 寄存器中的值更新为下一条指令的地址,以便继续执行下一条指令

Java 栈(Java Stack)

  1. 栈由一系列帧 Frame 组成的(Java 栈也叫做帧栈),是线程私有的,具有“先进后出”的特性
  2. 每个线程在执行方法时,都会创建一个对应的栈,用于存储该线程执行方法的信息。及线程每一次方法调用都会创建一个帧,并进行压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁
  3. 当线程调用的方法又调用了其他方法,则 JVM 会在当前的栈中创建一个新的栈,以此类推
  4. 一个线程可以同时对应多个栈,并且线程中的 Java 栈相互独立,当一个线程执行完一个方法后,该方法的栈会被销毁,线程继续执行其他方法的栈
  5. 帧保存的信息主要有:局部变量、操作数栈(所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等
  6. 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个插槽 slot 存放 32 位的数据,long、double 占两个插槽
  7. 栈的存取速度比堆快,仅次于 PC 寄存器
  8. 在栈中的数据的大小、生命周期是在编译期决定的,缺乏灵活性。栈的大小是由 Java 虚拟机在启动时,根据方法调用的嵌套深度和栈的大小限制等因素设置的,通常只有几百到几千字节
  9. 栈空间不足时,JVM 会抛出 StackOverflowError,而当 JVM 无法分配更多的栈空间时,会抛出 OutOfMemoryError

操作数栈(Operand Stack)

操作数栈是 JVM 在执行方法的过程中使用的临时数据区域,先进后出的结构。用于存储方法执行过程中的操作数、中间结果以及返回值等信息,JVM 在执行方法时,需要将操作数栈中的数据作为操作数进行相应的计算

Java 栈和操作数栈的区别

Java 栈和操作数栈是 JVM 执行方法时使用的两种不同的内存区域

  • Java 栈主要用于存储方法的局部变量、操作数栈、方法调用信息等
  • 操作数栈主要用于存储方法执行过程中的操作数和返回值

Java 堆(Java Heap)

  1. Java 堆(Java Heap)是 JVM 中用于存储对象实例和数组的内存区域,是 Java 中最大的一块内存区域。在 Java 程序运行期间,所有通过 new 关键字创建的对象都会被分配到 Java 堆中
  2. Java 堆是一个运行时数据区,它是在 JVM 启动时创建的,是可以被所有线程共享的内存区域
  3. Java 堆的大小可以通过虚拟机的启动参数来设置,并且可以动态扩展
  4. 在 Java 堆中分配对象实例时,Java 虚拟机会自动进行内存管理,负责对象的分配、回收等操作,开发人员不需要手动管理 Java 堆中的内存
  5. Java 堆中不仅仅存储了 Java 对象的实例数据,还包括对象头信息、数组长度等数据。在 Java 堆中分配对象实例时,JVM 会为每个对象分配一个对象头,用于存储对象的运行时数据和类型信息。同时,如果该对象是数组,JVM 还会为该对象分配一个额外的空间,用于存储数组的长度信息
  6. GC 主要是管理堆空间,对分代 GC 来说,堆也是分代的
  7. 堆的效率相对较慢

方法区(Method Area)

  1. JDK 7 时 HotSpot 虚拟机使用永生代(PermGen)实现的方法区
  2. JDK 8 移除了永生代,使用了元空间(Metaspace)实现的方法区,一部分数据存在元空间,一部分存在 Java 堆中
  3. 元空间与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中,它的别名是 Non-heap(非堆)
  4. 方法区是线程共享的内存区域,在 JVM 启动时创建的
  5. 方法区的大小可以通过虚拟机的启动参数来设置,并且可以动态扩展
    1. JDK 7:
      1. -XX:Permsize 设置永久代初始分配空间
      2. -XX:MaxPermsize 设定永久代最大可分配空间
    2. JDK 8:
      1. -XX:MetaspaceSize:设置初始的元空间大小
      2. -XX:MaxMetaspaceSize:设置元空间最大的大小
  6. 通常用来保存的信息有:
    1. 装载的类型信息:各类全限定名、类型标志、类的访问描述符等
    2. 类型的常量池
    3. 字段信息:修饰符、类型、名称等
    4. 方法信息:修饰符、返回类型、方法名、参数等
    5. 静态变量
    6. 指向类加载器的引用
    7. 指向 Class 实例的引用
    8. 方法表
    9. 运用时常量池
  7. Java 方法区和 Java 堆之间存在交集关系,Java 方法区并不等同于 Java 堆

元空间(Metaspace)

Java 元空间是从 JDK 8 开始引入的一种新的内存区域,用于存储类的元数据信息。在之前的版本中,Java 类的元数据信息是存储在永久代中的,在 JDK 8 中,永久代被移除,元数据信息被存储在了元空间中,解决了永久代大小有限的问题

运行时常量池(Runtime Constant Pool)

  1. 运行时常量池是方法区的一部分
  2. JDK 6 时运行时常量池存放在永久代,JDK 7 开始存放在堆中
  3. 运行时常量池是 Class 文件中每个类或接口的常量池表,在运行期间的表示形式
  4. 通常包括:类的版本、字段、方法、接口等信息
  5. 通常在加载类和接口到 JVM 后,就创建了相应的运行时常量池

本地方法栈(Native Method Stack)

本地方法栈是在 JVM 中用来支持 native 方法执行的栈

栈、堆、方法区之间的交互关系

Snipaste_2023-04-12_15-37-09.png todo Snipaste_2023-04-11_22-48-26.png

堆与方法区的关系

Snipaste_2023-04-13_10-52-12.png

Snipaste_2023-04-13_10-52-58.png

Snipaste_2023-04-13_10-53-35.png

Java 堆内存

  • Java 堆用来存放应用系统创建的对象和数组,所有线程共享 Java 堆
  • Java 堆需要在逻辑上连续
  • Java 堆是在运行期动态分配内存大小,自动进行垃圾回收
  • Java 垃圾回收主要是回收堆内存,对分代 GC 来说,堆也是分代的

Java 堆的结构

Snipaste_2023-04-15_12-50-26.png

  • 新时代:用来存放新分配的对象,新生代中经过垃圾回收,没有被回收的对象将被复制到老年代
  • 老年代存储对象比新生代对象的年龄大得多
  • 老年代会存储一些大对象
  • 整个堆大小 = 新生代 + 老年代
  • 新时代 = Eden + 存活区
  • JDK 8 之前的永生代,用来存放 Class、Method 等元信息,JDK 8 使用元空间(MetaSpace)代替,元空间并不存在于虚拟机中,而是直接使用本地内存

对象的内存布局

  • 对象在内存中存储的布局(HotSpot),分为:对象头、实例数据和对齐填充
  • 对象头:
    • Mark Word:存储对象自身的运行数据,如:HashCode、GC 分代年龄、锁状态标志等
    • 类型指针:对象指向它的类元数据的指针
  • 实例数据:
    • 真正存放对象实例数据的地方
  • 对齐填充
    • 这部分不一定存在,仅仅是占位符,HotSpot 要求对象的起始地址都是 8 字节的整数倍,如果不是就需要对齐填充

对象的访问定位

  • 在 JVM 规范中只规定了 reference 类型是一个指向对象的引用,但没有规定这个引用具体如何去定位、访问堆中对象的具体位置
  • 因此对象的访问方式取决于 JVM 的实现,目前主流的有:使用“句柄”和“指针”两种方式
    • 句柄 在 Java 堆中会划分出一块内存来做句柄池,reference 中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址

Snipaste_2023-04-15_14-24-21.png

  • 指针 Java 堆中会存放访问类元数据的地址,reference 存储的是对象的地址

Snipaste_2023-04-15_14-26-53.png

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 字节码执行引擎,基本功能就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果
  • 实现方式:
    • 通过解释器直接解释执行字节码
    • 通过编译器产生本地代码,编译执行

栈帧

Snipaste_2023-04-29_12-58-17.png

  • 是用于支持 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 的简化架构,内存区域被称为运行时数据区 Snipaste_2023-04-10_22-26-17.png

运行时数据区

主要包括:PC 寄存器、Java 虚拟机栈、Java 堆、方法区、运行时常量池、本地方法栈等

PC(Program Counter)寄存器

  1. 每个线程拥有一个 PC 寄存器,是线程私有的,用来存储的是当前线程下一条即将执行的指令的地址(而不是当前正在执行的指令的地址)
  2. 在创建一个新线程时,会自动创建一个 PC 寄存器,并将其初始化为 0,表示从字节码的第一条指令开始执行
  3. 执行本地方法时,PC 寄存器的值为 undefined
  4. PC 寄存器是一个较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域
  5. PC 寄存器的下一条指令地址是由 JVM 根据当前指令的“操作码”和“操作数”计算出来的,并由 JVM 自动将 PC 寄存器中的值更新为下一条指令的地址,以便继续执行下一条指令

Java 栈(Java Stack)

  1. 栈由一系列帧 Frame 组成的(Java 栈也叫做帧栈),是线程私有的,具有“先进后出”的特性
  2. 每个线程在执行方法时,都会创建一个对应的栈,用于存储该线程执行方法的信息。及线程每一次方法调用都会创建一个帧,并进行压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁
  3. 当线程调用的方法又调用了其他方法,则 JVM 会在当前的栈中创建一个新的栈,以此类推
  4. 一个线程可以同时对应多个栈,并且线程中的 Java 栈相互独立,当一个线程执行完一个方法后,该方法的栈会被销毁,线程继续执行其他方法的栈
  5. 帧保存的信息主要有:局部变量、操作数栈(所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等
  6. 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个插槽 slot 存放 32 位的数据,long、double 占两个插槽
  7. 栈的存取速度比堆快,仅次于 PC 寄存器
  8. 在栈中的数据的大小、生命周期是在编译期决定的,缺乏灵活性。栈的大小是由 Java 虚拟机在启动时,根据方法调用的嵌套深度和栈的大小限制等因素设置的,通常只有几百到几千字节
  9. 栈空间不足时,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 对象创建过程

  1. 类加载检查:检查类是否已被加载、解析和初始化
  2. 内存分配:在 Java 堆中为新生对象分配内存
  3. 初始化零值:将分配的内存空间初始化为零值
  4. 设置对象头:设置对象的对象头信息
  5. 执行初始化:执行对象的初始化方法

4.1.2 引用关系

  • 栈到堆:局部变量表中的引用指向堆中的对象
  • 堆到方法区:对象头中的类型指针指向方法区中的类信息
  • 方法区到堆:Class 对象存储在堆中,静态变量也在堆中

4.2 堆与方法区的详细关系

4.2.1 数据分布

  • 对象实例:存储在 Java 堆中
  • 对象类型数据:存储在方法区中
  • 对象引用:存储在栈的局部变量表中

4.2.2 访问过程

  1. 通过栈中的 reference 定位到堆中的对象
  2. 通过对象头中的类型指针定位到方法区中的类信息
  3. 根据类信息进行方法调用和字段访问

5. Java 堆内存详解

5.1 堆内存特性

  • 线程共享:Java 堆被所有线程共享,用于存放应用系统创建的对象和数组
  • 逻辑连续:Java 堆在逻辑上必须是连续的内存空间
  • 动态管理:运行期动态分配内存大小,自动进行垃圾回收
  • 分代设计:对于分代 GC 来说,堆采用分代结构

5.2 Java 堆的结构

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 指令执行过程

  1. 取指令:从字节码流中读取下一条指令
  2. 解码:解析指令的操作码和操作数
  3. 执行:根据指令类型执行相应操作
  4. 更新程序计数器:指向下一条指令

8. 总结

8.1 JVM 内存模型核心要点

  1. 内存区域划分:PC 寄存器、Java 栈、本地方法栈、堆、方法区、运行时常量池
  2. 线程私有区域:PC 寄存器、Java 栈、本地方法栈
  3. 线程共享区域:Java 堆、方法区、运行时常量池
  4. 堆内存管理:分代设计、对象内存布局、访问定位机制
  5. 执行引擎:基于栈的字节码解释执行、栈帧结构、方法调用机制

8.2 性能调优关键点

  1. 合理配置堆大小:根据应用特点设置 Xms、Xmx 参数
  2. 分代比例调整:通过 NewRatio、SurvivorRatio 优化 GC 性能
  3. 栈大小配置:使用 Xss 参数避免 StackOverflowError
  4. 元空间监控:JDK 8+需要关注 MetaspaceSize 配置
  5. GC 日志分析:通过 GC 日志分析内存使用模式和性能瓶颈

8.3 最佳实践建议

  1. 监控内存使用:定期检查各内存区域的使用情况
  2. 优化对象生命周期:减少不必要的对象创建和长生命周期对象
  3. 合理使用本地缓存:避免内存泄漏
  4. 定期分析 GC 日志:识别性能问题和优化机会
  5. 压测验证配置:在类似生产环境中验证 JVM 参数设置
Edit this page
最后更新时间:
贡献者: hyfly233