JVM内存结构图
1. 程序计数器(Program Counter Register)
物理上,用寄存器实现
1.1 定义
一块较小的内存空间,可以当成当前线程所执行字节码的行号指示器。
1.2 作用
二进制字节码(jvm指令)通过解释器变成机器码,再由CPU执行
存放下一条jvm指令的执行地址
1.3 特点
- 线程私有
- 唯一一个不存在内存溢出的区域
2. 虚拟机栈
栈:一种先进后出的数据结构
2.1 定义
- 每个线程运行时需要的内存空间,称为虚拟机栈
- 每个栈由栈帧(拥有局部变量表、操作数栈、动态链接、方法出口信息)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧(位于栈顶),对应当前正在执行的方法
2.2 栈内存溢出 StackOverFlowError
-Xss256k 设置栈内存大小
- 栈帧过多:方法递归调用没有终止条件
- 栈帧过大
2.3 线程运行诊断
case 1: cpu占用过高
定位
top
定位占用cpu过高的进程ps H -eo pid,tid,%cpu | grep 进程id
定位进程中占用cpu过高的线程jstack 进程id
,查找转换成16进制后的线程id,进一步定位到问题代码行数
case 2: 程序运行很长时间没有结果
jstack 进程id
查看底部打印信息,是否有死锁等信息。
3. 本地方法栈
调用本地方法(一般为C/C++方法)时提供的内存空间
4. 堆
JVM管理的最大一块内存,线程共享。
4.1 定义
存放对象实例,几乎所有对象实例以及数组都在这里分配内存
特点
- 线程共享,堆中对象需考虑线程安全问题
- 有垃圾回收机制
4.2 堆内存溢出
-Xmx8M 修改最大堆内存
// java.lang.OutOfMemoryError: Java heap space
void testOOM() {
List<String> list = new ArrayList<>();
while (true) {
list.add("hello");
}
}
4.3 堆内存诊断
工具
- jps工具
- 查看当前系统中有哪些java进程
- jmap工具
- 查看堆内存占用情况
jmap -heap 进程id
- 查看堆内存占用情况
- jconsole工具
- 图形界面,多功能监测工具,可以连续监测
案例:垃圾回收后,内存占用仍然很高
jvisualvm
工具- 堆dump
- 查找较大的对象
5. 方法区
5.1 定义
- 与堆一样,线程共享。
- 存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 逻辑上属于堆的一部分,具体JVM实现不一定
方法区与永久代,元空间的关系
方法区是一种规范,永久代或元空间是它的一种实现。(就类似接口与实现了该接口的类那样)
Orcale公司的HotSpot虚拟机
- JDK8以前,永久代实现了方法区,它在堆中
- JDK8以后,移除永久代,元空间实现了方法区,用的是操作系统的直接内存
5.2 组成
5.3 内存溢出
- JDK8以前导致永久代内存溢出
java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m 设置最大永久代大小
- JDK8以后导致元空间内存溢出
-XX:MaxMetaspaceSize=8m 设置最大元空间大小
java.lang.OutOfMemoryError: Metaspace
场景 (动态加载类 cglib)
- spring
- mybatis
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据此表找到要执行的类目、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址
二进制字节码组成
- 类基本信息
- 常量池
- 类方法定义(包含了虚拟机指令)
javap -v HelloWorld
反编译字节码文件
5.5 串池 StringTable
hashtable结构,不能扩容
// StringDemo.java
public class StringDemo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
反编译上述文件的字节码文件javap -v StringDemo
,得到如下片段(只取其中一部分)
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // Solution
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LSolution;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 Solution.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 Solution
#29 = Utf8 java/lang/Object
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
常量池与串池的关系
- 常量池中的信息,都会被加载到运行时常量池中,此时a b ab 只是常量池中的符号,没有变为java字符串对象
- 指令
ldc #2
在串池中查找"a"对象,没有则把符号a变成"a"对象放入串池中;如果已存在,则使用串池中的对象。
特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池机制,避免创建重复字符串
- 字符串变量拼接原理是StringBuilder (JDK8)
- 字符串常量拼接原理是编译器优化
- 使用intern方法,主动将串池中还没有的字符串对象放入串池
- JDK8 将这个字符串对象尝试放入串池,如果有则不放入,返回串池中的对象;如果没有则放入串池,并返回这个字符串对象的引用。
- JDK6 将这个字符串对象尝试放入串池,如果有则不放入,返回串池中的对象;如果没有则复制这个对象一份放入,并返回串池中的对象。
调优
- -XX:StringTableSize=桶个数
- 如果有大量字符串,可以考虑入池,节约堆内存空间
6. 直接内存
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,读写性能高
- 不受JVM内存回收管理
内存溢出
java.lang.OutOfMemoeryError: Direct buffer memory
6.1 分配和回收原理
- 使用了
Unsafe
对象完成直接内存的分配回收,回收需要主动调用freeMemory方法 ByteBuffer
的实现类内部,使用了Cleaner
虚引用来检测ByteBuffer对象,一旦它被垃圾回收,就会由ReferenceHandler
线程通过Cleaner
的clean
方法调用freeMemory
来释放直接内存
禁用显示垃圾回收
-XX: +DisableExplicitGC