(待更新)📝JVM-1:JVM 简介 & 运行时数据区
JVM 简介
Java 程序的运行环境(Java 二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态的基石
运行时数据区
PC Register(程序计数器)
PC Register,Program Counter Register,程序计数器
Java 中代码的执行流程
Java 代码首先被编译成字节码(JVM 指令),然后这些字节码交由 JVM 执行引擎的解释器进行解释。解释器将字节码转换为机器码,最终交由 CPU 执行。
程序计数器的作用
负责记住下一条 JVM 指令的执行地址,从而保证程序执行的有序性
负责记录下一条 JVM 指令的执行地址,从而确保程序执行的有序性。
在多线程环境中,每个线程都有自己的程序计数器,记录该线程上次执行结束的位置。当线程被调度时,程序计数器指示从上次停止的位置继续执行。
程序计数器的实现
程序计数器是通过“寄存器”实现的。寄存器是 CPU 中访问速度最快的存储单元,Java 将寄存器作为程序计数器来存储和读取指令的内存地址,因为指令的读取频率很高。
程序计数器的特点
- 线程私有
- 唯一一个不会存在内存溢出的内存结构
JVM Stack(虚拟机栈)
概念
栈:线程运行需要的内存空间,一个线程一个栈
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)
一个栈由多个栈帧组成,一个栈帧是一个方法
每个线程只能有一个活动栈帧,对应着正在执行的那个方法
如何判断方法内的局部变量是否线程安全?
- 如果方法内的局部变量没有逃离方法的作用范围,且是线程私有的,就是线程安全的
- 如果局部变量引用了对象,并逃离了方法的作用范围(比如将局部变量返回),就需要考虑线程安全
产生栈内存溢出的原因
- 栈帧过多
- 栈帧多大
线程运行诊断
案例1:CPU 占用过多
定位办法:
用 top 命令定位哪个进程对 CPU 的占用过高
用 ps 命令进一步定位是哪个线程引起的 CPU 占用过高:
ps H -eo pid,tid,%cpu | grep 进程id
jstack 进程id
可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果
死锁现象!
Native Method Stack(本地方法栈)
作用:在 JVM 调用一些本地方法时为其提供内存空间
Heap(堆)
前面学习的程序计数器、虚拟机栈、本地方法栈都是线程私有的。
堆的特点
- 线程共享,堆中的对象都需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出
可以设置参数 -Xmx8m
设置堆内存小一些就可以看到多久会有堆内存溢出的问题
堆内存诊断
- jps 工具
查看当前系统中有哪些 Java 进程 - jmap 工具
查看堆内存占用情况jmap -heap 进程id
- jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
案例:垃圾回收后,内存占用仍然很高
-> 使用 JVisualVM 来监控 Java 程序
扩展:JConsole 和 JVisualVM 能监控到 JVM 信息和 Java 程序,都是通过 JMX 实现的
Method Area(方法区)
简介
定义
方法区存储:
- 类的二进制字节码数据
- 类加载器
- 运行时常量池
注意:方法区不是堆的一部分,它与堆一样是所有线程共享的
JDK 1.6
JDK 1.8 及以后
核心理解
HotSpot 虚拟机:
JDK 1.8 之前,方法区的实现叫做永久代(使用堆的一部分内存作为方法区);
JDK 1.8 及以后,移除了永久代,换了一种实现,叫做元空间(用的不是堆的内存了,而是本地内存,即操作系统的内存)
方法区只是一种规范,永久代和元空间都是它的一种实现。
JDK 1.8 之后的方法区不是由 JVM 来管理它的内存结构,因为使用的是本地操作系统的内存。
方法区_内存溢出
- 1.8 之前会导致永久代内存溢出
可以通过参数-XX:MaxPermSize=8m
控制其大小
java.lang.OutOfMemoryError: PermGen space - 1.8 及之后会导致元空间内存溢出
可以通过参数-XX:MaxMetaspaceSize=8m
控制其大小
java.lang.OutOfMemoryError: Metaspace
场景:
- Spring
- MyBatis 等代理对象等
常量池
运行时常量池内部可能包含 StringTable
class 常量池(存在于一个类中)
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
编译器将源文件编译为 .class 二进制字节码文件,该文件包括:
- 类的基本信息
- 类的常量池信息(即 class 常量池,保存在 .class 文件中,运行时会加载到内存中)
- 类方法定义(包含 JVM 指令)
class 常量池的作用(以类为单位):
用于为 JVM 字节码指令提供必要的常量符号支持,保证虚拟机指令能够成功执行各类操作
存储:字面值(整数、浮点数、字符串等)和符号引用(类、方法、字段的符号名等)
运行时常量池(存在于内存中)
- 当类加载后,JVM 会将 class 文件中的常量池内容加载到内存中,构成该类独有的“运行时常量池”
- 它的大部分数据来自 class 常量池,但具备动态性,即运行过程中可加入新常量(如 String.intern())
- 类加载过程(加载 → 连接 → 初始化)中,JVM 会在解析阶段将符号引用替换为直接引用(真实地址)
- 每个类都有一份运行时常量池,存储位置为方法区(JDK 1.8 以后为元空间,使用本地内存)
(JDK 1.8 及之后)运行时常量池存在于由元空间实现的方法区中,是全局的
StringTable 底层是 HashTable,且不支持动态扩容
StringTable
StringTable
- 数据结构
- 位置
- 垃圾回收
- 调优
数据结构(略)
JDK 1.8 vs JDK 1.6
JDK 1.8 intern()
尝试将该字符串对象放入串池:如果有 -> 不放入;如果没有 -> 放入串池,并把串池中的对象返回
JDK 1.6 intern()
尝试将该字符串对象放入串池,如果有 -> 不放入;如果没有 -> 拷贝一份,将新的字符串对象放入串池并返回
即:使用 intern() 的字符串对象和最终放入串池中的对象是两个对象
面试题
- JDK 1.8
(TODO:补充)
- JDK 1.8
(TODO:补充)
- JDK 1.6
(TODO:补充)
StringTable 的位置
JDK1.6 。。。,所以为了优化,JDK 1.7 之后将 StringTable 从方法区移到了推内存中 -> 堆内存中垃圾回收效率更高
垃圾回收的一个规则:
(TODO:补充)
StringTable 垃圾回收
StringTable 底层是哈希表(HashTable?)数据结构实现的,当内存满了时触发垃圾回收
StringTable 性能调优
主要就是调增桶 bucket 的个数(哈希表数组个数/数组长度)
底层是哈希表实现的,哈希表的性能取决于它的大小:
桶的个数越多 -> 存放的元素越分散 -> 哈希冲突概率越小 -> 查找速度快
总结:
- 如果系统中的字符串常量非常多的话,可以将桶的个数设置大一些:
-XX: StringTableSize=桶的个数
- 考虑是否将字符串对象入池:intern()
6 Direct Memory(直接内存)
介绍
不属于 Java 虚拟机管理的内存,属于系统内存
Direct Memory 内存特点:
- 常见于 NIO 操作时,用作数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
基本使用
为什么 Direct Buffer 比传统 IO 快?为什么本地内存读取更快?
-> 原理分析:
- 传统 IO 的方式:
Java 本身并不具备磁盘读写的能力,需要调用操作系统的函数来进行磁盘读写(即需要调用本地方法)。读取磁盘时会在操作系统中画出一块缓冲区,称为系统缓冲区,磁盘内容会先读取到系统缓冲区中,但 Java 代码在这里是不会运行的。JVM 会在堆内存中划分一块Java 缓冲区,然后再将数据从系统缓冲区读取到 Java
缓冲区。
- Direct Buffer 的方式:
在操作系统中划分出一块操作系统和 Java 代码都可以共享的区域
-> 不需要先读到系统缓冲区后再读到 Java 缓冲区了,Java 代码可以直接读
内存溢出
Direct Memory 不受 JVM 内存回收管理 -> 不手动释放内存就会溢出
下面的例子中,在循环中不断申请一个 100 MB 的直接内存空间,把其加入 list 中,最终会抛出直接内存 OOM 异常:
释放原理
直接内存的释放是通过一个 Unsafe 对象来管理的,无法通过垃圾回收释放
(这个对象一般是 JDK 内部使用的)
ByteBuffer 的实现类内部:
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- 使用了 Cleaner(虚引用) 来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放内存
禁用显式回收对直接内存的影响
-XX:+DisableExplicitGC
让代码中的 System.gc() 失效
这种方式会对直接内存的垃圾回收造成影响