Java堆溢出
A. 关于 “这里面讲保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象” 理解:
- GC Roots是不是有4类 :
a它可以是方法局部变量表中引用,b方法区:类静态成员引用,常量引用如 static final String。 c. 本地方法栈中 JNI 引用,d. 同步锁持有的对象。 jvm内部引用(如基本类型 Class 对象如 int.class、常驻异常如 NullPointerException 类对象等) - 只要上面4种持有引用,就表示GC Roots到对象之间有可达路径?
B. 当设置-XX:+HeapDumpOnOutOfMemoryError时,出现内存溢出,jvm会dump内存快照到启动 java -jar … 命令时所在目录是吗?
答: 是的。 格式长这样: java_pid.hprof, 通常只对 Java Heap Space 和 Metaspace 溢出触发,直接内存溢出(Direct buffer memory)等不会触发(需额外配置)
如下表示堆出现异常:Java heap space
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof …
Heap dump file created [22045981 bytes in 0.663 secs]
C, 内存溢出分两种,一种是内存泄漏(Memory Leak):GC Roots的引用链一直存在,即对象一直无法被jvm回收。 第二种就内存耗尽(Memory Exhaustion) 即程序中对象必须存在,内存不足了。
关于内存泄漏处理策略是:通过Eclipse Memory Analyzer这个工具中的 Dominator Tree 表示谁控制了最多内存” 的树状结构
Shallow Heap:该对象自身占用的内存(如对象头 + 字段值,不含引用对象)
Retained Heap(单位byte):该对象及其所有可达对象(且不被其他路径引用)的总内存 → 这才是真正“它独占”的内存
Percentage:占整个堆的百分比(按 Retained Heap 计算)
重点看 Retained Heap 和 Percentage —— 它告诉你:哪个对象“吃掉”了最多内存,是泄漏嫌疑最大的目标!
下一步:如何深挖这个 main Thread?
– 步骤 1:右键 java.lang.Thread @… main Thread → “Show in Dominator Tree”
(点击左侧小三角 , 就可以看出是哪个点击Percentage最大的一个,左侧标签栏有一个Attributes,可以看具体是什么对象)
– 步骤 2:右键该线程 → “Merge Shortest Paths to GC Roots”
这是关键操作!它会显示:
从 GC Root 到该线程的最短引用路径(即:谁让这个线程“活”着?它又持有哪些对象?)
– 步骤 3:右键该线程 → “List Objects” → “with outgoing references”
查看它直接引用了哪些对象(比如是不是有个 HashMap 或 ArrayList 占了大部分 Retained Heap)
/**
VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
@author zzm
*/
public class HeapOOM {static class OOMObject {
}public static void main(String[] args) {
List list = new ArrayList();while (true) { list.add(new OOMObject()); }}
}
虚拟机栈和本地方法栈溢出的理解:
- 栈溢出,jvm不区分是虚拟机栈和还是本地方法栈。
- jvm栈的大小通过-Xss参数 来设置,出现如下两种情况会:就会抛出StackOverflowError异常
a. 如果超过栈的深度。如在不断递归达到指定深度时(默认Linux 64位通常为 1MB, 可以达到1000~2000层递归)
/**
- jvm栈的大小通过-Xss参数 来设置,出现如下两种情况会:就会抛出StackOverflowError异常
- VM Args:-Xss128k
*/
public class JavaVMStackSOF_1 {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF_1 oom = new JavaVMStackSOF_1();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println(“stack length:” + oom.stackLength);
throw e;
}
}
}
b. 在方法中定义异常大量“基本类型”(不是引用对象的变量,对象的大小在堆中),从而增大了本地变量表的长度。
-3 -Xss参数 默认在liunx下面不能小于228k. 不然,jvm启动会有一个提示:The Java thread stack size specified is too small. Specify at least 228k
-4. 线程创建过多会出现OutOfMemoryError,如果线程栈的很大,这种情况更容易发生,这与栈空间是否足够没有关系。(这种情况在我公司的项目就遇到过)
– 这种oom的异常现象:从JDK 7起, 提示信息中“unable to create native thread”后面, 虚拟机会特别注明原因可能是“possiblyout of memory or process/resource limits reached”
– 它的原因是如果单个进程是 2G 减去 (堆+方法区(-XX:MaxMetaspaceSize)+直接内存(-XX:MaxDirectMemorySize))=》 剩下给虚拟机栈和本地方法栈分配。如果线程量大,而且每个线程-Xss又很大,就会出现“unable to create native thread”
公式:最大线程数 ≈ (物理内存 - 堆(-Xmx) - Metaspace(-XX:MaxMetaspaceSize) - 直接内存(-XX:MaxDirectMemorySize)) / (-Xss)
– 解决方法:如果无法减少线程数时,无法加大进程的内存时,可以减小堆的大小及线程栈-Xss的大小。
方法区和运行时常量池溢出的理解:
- 在jdk8中方法区都在元空间中,存放的内容如类名、 访问修饰符、 常量池、 字段描述、 方法描述等。 所以jdk8之后,就没有java.lang.OutOfMemoryError: PermGen space
而是java.lang.OutOfMemoryError: Metaspace
- 在jdk8中方法区都在元空间中,存放的内容如类名、 访问修饰符、 常量池、 字段描述、 方法描述等。 所以jdk8之后,就没有java.lang.OutOfMemoryError: PermGen space
1.1 触发场景:A. 动态生成大量类(如 Spring CGLib 代理、Groovy 脚本、OSGi 模块) B. 类加载器泄漏(ClassLoader 未卸载 → 其加载的类无法回收)
- 字符串常量池已经放到了堆中了,以前jdk8以前是放在方法区。
– String::intern()的行行:
A. 会直接在堆中字符串常量池找是否存在,如果存在直接返回引用。
B.如果不存在直接从堆中找到该string对象放到常量池中(intern()不会创建对象),并返回引用。
- 字符串常量池已经放到了堆中了,以前jdk8以前是放在方法区。
public static void main(String[] args) {
String str1 = new StringBuilder(“计算机”).append(“软件”).toString();
System.out.println(str1.intern() == str1); // 这个在jdk8时,是true, 因为intern()在常量池中找"计算机软件",发生是第一次,所以从堆中找该对象StringBuilder(“计算机软件”)放到常量池中。所以它们是一样的。
String str2 = new StringBuilder(“ja”).append(“va”).toString();
System.out.println(str2.intern() == str2); // 这个在jdk8时,是false,因为因为“java”这个字符串在(如 rt.jar 中的类名)就已经出现过一次, 字符串常量池中已经有了和堆中新生的new StringBuilder(“java”)不一样。
}
-3. XX: MetaspaceSize: 指定元空间的初始空间大小(默认约 20.8MB,平台相关),
当元空间使用量 > MetaspaceSize 时,下次 Full GC 会尝试卸载无用类型卸载。 -XX: MinMetaspaceFreeRatio: 作用是在垃圾收集之后控制最小的元空间剩余容量的百分比, 可减少因为元空间不足导致的垃圾收集的频率。
- 疑问:在项目中,需要创建超极大量的对象,从jstat -gcutil 时,Metaspace一直在90%以上。
答: 这和创建超大量的对象与Metaspace 高占用无关。 和工程用到spring CGLIB、 AOP、ORM 等功能有关,会在运行时动态生成大量新的类
jstat -class
- 输出示例:
- Loaded Bytes Unloaded Bytes Time
- 25432 52345.6 0 0.0 12.34
如果 ‘Loaded’ 数量巨大(几万甚至更多),并且 ‘Unloaded’ 为0或很少,说明类在持续加载但没有被卸载。
本机直接内存溢出的理解:
- 直接内存可以通过-XX:MaxDirectMemorySize参数来指定, 如果不去指定, 则默认与Java堆最大值(由-Xmx指定) 一致,但最高不超过4G.
- 异常信息:java.lang.OutOfMemoryError: Direct buffer memory。通过jstat -gcutil,可以发现堆的使用率很低。
AI的langchain,langgraph , java,spring,hadoop, flink流式计算, 心理学,哲学相关知识探讨.
本人邮箱:luyllyl@163.com