一、程序计数器
1、定义: Program Counter Register 程序计数器(寄存器),程序计数器在物理上通过寄存器实现。
2、作用: 记住下一条JVM指令的执行地址,根据执行地址找到对应的指令。
3、特点
- 线程私有:每个程序都有自己的程序计数器
- 不会存在内存溢出
4、程序执行流程: 二进制字节码(jvm指令)-》解释器-》机器码-》CPU
二、虚拟机栈
1、定义: Java Virtual Machine Stacks (Java 虚拟机栈),每个线程运行时所需要的内存,称为虚拟机栈。
2、栈帧: 每个方法运行时需要的内存(参数 局部变量 返回地址),一个虚拟栈可有多个栈帧
3、活动栈帧: 线程正在调用的方法栈帧
4、调用流程: 使用方法时将栈帧压入虚拟栈,使用完后释放内存
5、注意:
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
6、问题
- 垃圾回收是否涉及栈内存?
- 垃圾回收只回收堆内存,不回收栈内存
- 栈内存分配越大越好吗?
- 栈内存分配越大,线程内存越少,因此不是越多越好
- 方法内的局部变量是线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
7、栈内存溢出
- 栈帧过多导致栈内存溢出(例如方法递归调用结束条件不正确)
- 栈帧过大导致内存溢出
8、线程运行诊断
- 案例一:CPU占用过多(LInux环境下)
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号(nid为16进制)
- 案例二:程序运行没有结果(线程死锁)
- Jstack 进程id deadlock
三、本地方法栈
作用:给本地方法调用提供内存空间
- 例如:Object类中的native方法
四、堆
1、定义: Heap 堆,通过new关键字创建对象都会使用堆内存
2、特点
- 线程共享,堆中对象需要考虑线程安全问题
- 有垃圾回收机制
3、堆内存溢出: 例如new的对象过多导致堆内存溢出
4、堆内存诊断
- 打开IDEA 控制台
- Jps-查看java进程
- Jmap-查看堆内存占用情况 jamp -heap 进程id Eden Space used
- Jconsole-图形界面 多功能检测 连续监测 Jconsole打开建立连接
5、案例
- 垃圾回收后,内存占用仍然很高
- 使用Java VisualVm工具
- 监视-》堆dump-》查找
五、方法区
1、定义: 所有Java虚拟机共享的一块区域,存储类结构有关的信息。方法区在虚拟器启动时创建,逻辑上是堆的一部分。方法区也存在内存溢出的错误。
- 1.6方法区实现被称为永久代
- 1.8以后方法区实现被称为元空间
2、内存溢出
- 1.8以前永久代空间不足导致内存溢出
- 1.8以后元空间不足(默认使用系统内存,没有限制)导致内存溢出
- 实际场景:框架使用(spring、mybatis)
3、运行时常量池
- 反编译查看字节码:Java -p 类名 反编译
- 常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
4、StringTable
-
定义:实际为串池,结构为hashtable,不能扩容
-
特点
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制(延迟加载),来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
- 面试题
String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; // ab String s4 = s1 + s2; // new String("ab") String s5 = "ab"; String s6 = s4.intern(); // 问 System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true System.out.println(s3 == s6); // true String x2 = new String("c") + new String("d"); // new String("cd") x2.intern(); String x1 = "cd"; // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 System.out.println(x1 == x2);//true,调换后为false 1.6为false
-
位置
- 1.6 是常量池的一部分
- 1.8 是StringTable的一部分
-
性能调优
- 调整 -XX:StringTableSize=桶个数(字符串常量越多,桶个数相应调大)
- 考虑将字符串对象是否入池 (存在重复)
六、直接内存
1、定义: Direct Memory
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
- Java文件读取流程
- 使用直接内存读取流程,相比于IO操作少了一次复制操作
- Java文件读取流程
-
不受 JVM 内存回收管理
2、内存溢出:直接内存也会存在内存一出问题
3、内存释放原理
- 使用了 Unsafe 对象完成直接内存的分配(allocateMemory)和回收,并且回收需要主动调用 freeMemory 方法
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
- ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦 ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
- 禁用显示垃圾回收会导致直接内存得不到释放
- 解决:unsafe对象调用freeMemory释放