JVM 简介

Java 程序的运行环境(Java 二进制字节码的运行环境)

好处:

  1. 一次编写,到处运行
  2. 自动内存管理,垃圾回收功能
  3. 数组下标越界检查
  4. 多态的基石

image-20250604144358172

image-20250604144409866


运行时数据区

1 PC Register(程序计数器)

PC Register,Program Counter Register,程序计数器

Java 中代码的执行流程

Java 代码首先被编译成字节码(JVM 指令),然后这些字节码交由 JVM 执行引擎的解释器进行解释。解释器将字节码转换为机器码,最终交由 CPU 执行。

image-20250604144906516

程序计数器的作用

负责记住下一条 JVM 指令的执行地址,从而保证程序执行的有序性

负责记录下一条 JVM 指令的执行地址,从而确保程序执行的有序性。

在多线程环境中,每个线程都有自己的程序计数器,记录该线程上次执行结束的位置。当线程被调度时,程序计数器指示从上次停止的位置继续执行。

程序计数器的实现

程序计数器是通过“寄存器”实现的。寄存器是 CPU 中访问速度最快的存储单元,Java 将寄存器作为程序计数器来存储和读取指令的内存地址,因为指令的读取频率很高。

程序计数器的特点

  • 线程私有
  • 唯一一个不会存在内存溢出的内存结构

2 JVM Stack(虚拟机栈)

概念

:线程运行需要的内存空间,一个线程一个栈
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)

一个栈由多个栈帧组成,一个栈帧是一个方法

每个线程只能有一个活动栈帧,对应着正在执行的那个方法

如何判断方法内的局部变量是否线程安全?

  • 如果方法内的局部变量没有逃离方法的作用范围,且是线程私有的,就是线程安全的
  • 如果局部变量引用了对象,并逃离了方法的作用范围(比如将局部变量返回),就需要考虑线程安全

产生栈内存溢出的原因

  1. 栈帧过多
  2. 栈帧多大

线程运行诊断

案例1:CPU 占用过多

定位办法:

  1. 用 top 命令定位哪个进程对 CPU 的占用过高

  2. 用 ps 命令进一步定位是哪个线程引起的 CPU 占用过高:ps H -eo pid,tid,%cpu | grep 进程id

  3. jstack 进程id
    可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号

案例2:程序运行很长时间没有结果

死锁现象!

3 Native Method Stack(本地方法栈)

image-20250604144954064

作用:在 JVM 调用一些本地方法时为其提供内存空间

4 Heap(堆)

前面学习的程序计数器、虚拟机栈、本地方法栈都是线程私有的

堆的特点

  1. 线程共享,堆中的对象都需要考虑线程安全问题
  2. 有垃圾回收机制

堆内存溢出

可以设置参数 -Xmx8m 设置堆内存小一些就可以看到多久会有堆内存溢出的问题

堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 Java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap -heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

案例:垃圾回收后,内存占用仍然很高
-> 使用 JVisualVM 来监控 Java 程序

扩展:JConsole 和 JVisualVM 能监控到 JVM 信息和 Java 程序,都是通过 JMX 实现的

5 Method Area(方法区)

5.1 简介

定义

方法区存储:

  1. 类的二进制字节码数据
  2. 类加载器
  3. 运行时常量池

注意:方法区不是堆的一部分,它与堆一样是所有线程共享的

JDK 1.6

image-20250604145153782

JDK 1.8 及以后

image-20250604145206831

核心理解

HotSpot 虚拟机:

JDK 1.8 之前,方法区的实现叫做永久代(使用堆的一部分内存作为方法区);
JDK 1.8 及以后,移除了永久代,换了一种实现,叫做元空间(用的不是堆的内存了,而是本地内存,即操作系统的内存)

方法区只是一种规范,永久代和元空间都是它的一种实现

JDK 1.8 之后的方法区不是由 JVM 来管理它的内存结构,因为使用的是本地操作系统的内存。

5.2 方法区_内存溢出

  • 1.8 之前会导致永久代内存溢出
    可以通过参数 -XX:MaxPermSize=8m 控制其大小
    java.lang.OutOfMemoryError: PermGen space
  • 1.8 及之后会导致元空间内存溢出
    可以通过参数 -XX:MaxMetaspaceSize=8m 控制其大小
    java.lang.OutOfMemoryError: Metaspace

场景:

  • Spring
  • MyBatis 等代理对象等

5.3 常量池

运行时常量池内部可能包含 StringTable

class 常量池(存在于一个类中)

就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

编译器将源文件编译为 .class 二进制字节码文件,该文件包括:

  • 类的基本信息
  • 类的常量池信息(即 class 常量池,保存在 .class 文件中,运行时会加载到内存中)
  • 类方法定义(包含 JVM 指令)

class 常量池的作用(以类为单位):

用于为 JVM 字节码指令提供必要的常量符号支持,保证虚拟机指令能够成功执行各类操作
存储:字面值(整数、浮点数、字符串等)和符号引用(类、方法、字段的符号名等)

运行时常量池(存在于内存中)

  • 当类加载后,JVM 会将 class 文件中的常量池内容加载到内存中,构成该类独有的“运行时常量池”
  • 它的大部分数据来自 class 常量池,但具备动态性,即运行过程中可加入新常量(如 String.intern())
  • 类加载过程(加载 → 连接 → 初始化)中,JVM 会在解析阶段将符号引用替换为直接引用(真实地址)
  • 每个类都有一份运行时常量池,存储位置为方法区(JDK 1.8 以后为元空间,使用本地内存)

(JDK 1.8 及之后)运行时常量池存在于由元空间实现的方法区中,是全局的

StringTable 底层是 HashTable,不支持动态扩容