1.程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器),是线程私有的,不会存在内存溢出。
1.2 作用
Java源代码经过编译器编译为字节码,并在JVM上运行,JVM将字节码解释为JVM指令,然后在CPU上执行。程序计数器是JVM的一部分,用于跟踪当前线程下一条要执行的JVM指令的位置。程序计数器在物理上是寄存器。
2.虚拟机栈
2.1 定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
垃圾回收是否涉及栈内存?
不涉及栈内存。
JVM垃圾回收主要针对堆内存进行回收,堆内存是Java程序中用于存储对象的内存区域。在Java中,栈内存用于存储局部变量和方法调用堆栈,它的生命周期与方法调用相关。在方法调用结束时,栈帧会被释放,这些局部变量也随之被销毁。因此,栈内存的管理不需要垃圾回收器介入。
栈内存分配的大小越大越好吗?
过小的栈内存可能导致栈溢出的问题,而过大的栈内存则会导致资源的浪费。当一个线程的栈空间被分配过大时,可能会占用大量的内存资源,从而减缓程序的运行速度。此外,过大的栈内存还可能导致内存碎片的问题,从而影响程序的性能。
方法内的局部变量是否线程安全?
方法内的局部变量在多线程环境中是否线程安全取决于局部变量的数据类型和作用域。如果是基本数据类型或者不可变对象,并且作用域仅限于方法内部,那么它们是线程安全的;如果是可变对象或者作用域超出方法内部,那么就需要考虑线程安全问题。
2.2 栈内存溢出
栈帧过多导致栈内存溢出(报StackOverflowError)
package com.bbedu.jvm.t1; /** * @projectName: jvm-study * @package: com.bbedu.jvm.t1 * @className: JavaVMStackSOF * @author: BBChen * @description: 测试栈帧过多导致栈内存溢出 * @date: 2023/5/11 19:53 * @version: 1.0 * VM Args: -Xss256k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length: " + oom.stackLength); throw e; } } }
实际中,多个类循环引用时,容易出现上面的问题。
如何在IDEA中设置虚拟机参数:
2.3 线程运行诊断
场景一:CPU占用过高
定位问题代码:
- 用
top
定位是哪个进程 ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令定位是哪个线程引起的CPU占用过高)jstack 进程id
(进一步定位到问题代码的源码行号)
场景二:迟迟得不到结果
同样使用jstack 进程id
,查看是否出现死锁。
3.本地方法栈
Native Method Stacks.例如Object中的clone()、wait()方法等等。
4.堆
4.1 定义
Heap
通过new关键字创建的对象都会使用堆内存
特点
- 是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出
package com.bbedu.jvm.t1;
import java.util.ArrayList;
/**
* @projectName: jvm-study
* @package: com.bbedu.jvm.t1
* @className: HeapOOM
* @author: BBChen
* @description: 测试堆内存溢出异常
* @date: 2023/5/11 20:39
* @version: 1.0
* -Xmx8m
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
ArrayList<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
java.lang.OutOfMemoryError: Java heap space
4.3 堆内存诊断
使用jps、jmap、jconsole、jvisualvm等工具。
jps
查看当前系统中有哪些java进程
jmap
查看堆内存占用情况 jmap -heap 进程id
jconsole、jvisualvm
图形化工具
5.方法区
5.1 定义
Method Area. 所有线程共享,存储预加载类的结构(字段、方法、构造器、运行时常量池等等)
方法区是在虚拟机启动时被创建,逻辑上是堆的一部分,具体实现不一定。
方法区同样会出现OutOfMemoryError,从Java 8开始,方法区已被移除,取而代之的是元空间(MetaSpace)。元空间也会发生OutOfMemoryError异常,原因和方法区类似。
5.2 组成
5.3 方法区内存溢出
1.8之前会出现永久代内存溢出
java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
1.8之后会导致元空间内存溢出
java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=10m
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等消息
- 运行时常量池,常量池保存在*.class中,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 StringTable
- 常量池中的字符串仅是符号,第一-次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
可以使用intern方法,主动将串池中还没有的字符串对象尝试放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,再放入串池,会把串池中的对象返回
package com.bbedu.jvm.t1;
/**
* @projectName: jvm-study
* @package: com.bbedu.jvm.t1
* @className: StringTableTest
* @author: BBChen
* @description: 测试StringTable机制
* @date: 2023/5/13 13:20
* @version: 1.0
*/
public class StringTableTest {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // "ab"存入常量池
String s4 = s1 + s2; // new String("ab")
String s5 = "ab"; // 直接从常量池load
String s6 = s4.intern(); // 存入常量池,返回常量池中已经存在的"ab"
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"),在堆中
String x1 = "cd"; // "cd" 在常量池中
x2.intern(); // 常量池已经存在,放入失败
// 将上面的最后两行交换,将为true
System.out.println(x1 == x2); // false
}
}
5.5 StringTable位置
分为1.7前和1.7后,1.7前StringTable位于堆中永久代(Permanent Generation),1.7后StringTable位于本地内存的元空间(Metaspace)中。可通过OOM错误信息判断。
例如,如果错误信息包含“java.lang.OutOfMemoryError: PermGen space”,则表示发生了永久代内存不足错误,而如果错误信息包含“java.lang.OutOfMemoryError: Metaspace”,则表示发生了元空间内存不足错误。
5.6 StringTable垃圾回收
在进行垃圾回收时,垃圾回收器会扫描内存中的对象,并标记那些仍然被引用的对象。然后,它会清除那些没有被标记为仍然被引用的对象。对于StringTable,垃圾回收器会扫描所有的字符串常量,并标记那些被其他对象引用的字符串,然后清除所有没有被标记为仍然被引用的字符串。这样,不再被引用的字符串就会被垃圾回收器清除,从而释放内存。
需要注意的是,由于字符串池中的字符串常量是被多个对象引用的,因此只有当所有对它的引用都被释放时,才能将其清除。因此,在进行垃圾回收时,垃圾回收器需要仔细处理StringTable中的字符串常量,以确保不会意外地清除仍然被引用的字符串。
5.7 StringTable性能调优
在JVM中,可以通过调整一些参数来优化StringTable的性能,包括以下几个参数:
- -XX:StringTableSize:用于设置StringTable的大小,即StringTable中桶(Bucket)的数量。默认情况下,StringTableSize的值为1009,即默认有1009个桶。可以根据具体情况调整这个参数的值以提高性能。
- -XX:+PrintStringTableStatistics:用于打印StringTable的统计信息,例如StringTable中字符串数量、桶的数量、平均链长等等,可以用来评估StringTable的性能。
- -XX:+PrintFlagsFinal:用于打印JVM中所有的参数及其默认值,可以用来查看StringTableSize以及其他参数的默认值,以帮助确定需要进行哪些调整。
- 还可以使用
intern()
方法来优化字符串的性能和内存占用。因为字符串池中的字符串是唯一的,可以减少相同内容的字符串在内存中的重复存储。
6.直接内存
6.1 定义
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
6.2 分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用frepMemory 方法
- ByteBuffer的实现类内部,使用了Cleaner (虚引用) 来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
6.3 禁用显示GC
-XX:+DisableExplicitGC,无法回收直接内存,使用时需要注意
评论