JVM 笔记(一):JVM 简介 & 运行时数据区
JVM 简介
Java 程序的运行环境(Java 二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态的基石
运行时数据区
1 PC Register(程序计数器)
PC Register,Program Counter Register,程序计数器
Java 中代码的执行流程
Java 代码首先被编译成字节码(JVM 指令),然后这些字节码交由 JVM 执行引擎的解释器进行解释。解释器将字节码转换为机器码,最终交由 CPU 执行。
程序计数器的作用
负责记住下一条 JVM 指令的执行地址,从而保证程序执行的有序性
负责记录下一条 JVM 指令的执行地址,从而确保程序执行的有序性。
在多线程环境中,每个线程都有自己的程序计数器,记录该线程上次执行结束的位置。当线程被调度时,程序计数器指示从上次停止的位置继续执行。
程序计数器的实现
程序计数器是通过“寄存器”实现的。寄存器是 CPU 中访问速度最快的存储单元,Java 将寄存器作为程序计数器来存储和读取指令的内存地址,因为指令的读取频率很高。
程序计数器的特点
- 线程私有
- 唯一一个不会存在内存溢出的内存结构
2 JVM Stack(虚拟机栈)
概念
栈:线程运行需要的内存空间,一个线程一个栈
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)
一个栈由多个栈帧组成,一个栈帧是一个方法
每个线程只能有一个活动栈帧,对应着正在执行的那个方法
如何判断方法内的局部变量是否线程安全?
- 如果方法内的局部变量没有逃离方法的作用范围,且是线程私有的,就是线程安全的
- 如果局部变量引用了对象,并逃离了方法的作用范围(比如将局部变量返回),就需要考虑线程安全
产生栈内存溢出的原因
- 栈帧过多
- 栈帧多大
线程运行诊断
案例1:CPU 占用过多
定位办法:
用 top 命令定位哪个进程对 CPU 的占用过高
用 ps 命令进一步定位是哪个线程引起的 CPU 占用过高:
ps H -eo pid,tid,%cpu | grep 进程id
jstack 进程id
可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果
死锁现象!
3 Native Method Stack(本地方法栈)
作用:在 JVM 调用一些本地方法时为其提供内存空间
4 Heap(堆)
前面学习的程序计数器、虚拟机栈、本地方法栈都是线程私有的。
堆的特点
- 线程共享,堆中的对象都需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出
可以设置参数 -Xmx8m
设置堆内存小一些就可以看到多久会有堆内存溢出的问题
堆内存诊断
- jps 工具
查看当前系统中有哪些 Java 进程 - jmap 工具
查看堆内存占用情况jmap -heap 进程id
- jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
案例:垃圾回收后,内存占用仍然很高
-> 使用 JVisualVM 来监控 Java 程序
扩展:JConsole 和 JVisualVM 能监控到 JVM 信息和 Java 程序,都是通过 JMX 实现的
5 Method Area(方法区)
5.1 简介
定义
方法区存储:
- 类的二进制字节码数据
- 类加载器
- 运行时常量池
注意:方法区不是堆的一部分,它与堆一样是所有线程共享的
JDK 1.6
JDK 1.8 及以后
核心理解
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,不支持动态扩容