目录
一、JVM内存区域
二、堆中对象
1. 对象的创建
2. 对象的内存布局
3. 对象的访问定位
三、OOM异常
1. 堆OOM异常测试
2. 栈SOF异常测试
1):栈容量过小
2):大量本地变量
3. 常量池OOM异常测试
4. 方法区测试
5. 堆外内存测试
四、参考资料
一、JVM内存区域
HotSpot虚拟机把内存分为不同的数据区域,有的区域随JVM启动而一直存在(线程共享区域),有的区域随用户线程启动和结束而建立和销毁(线程私有区域),如下图所示。
下图所示是线程共享的区域,其中堆区由参数-Xms和-Xmx决定堆大小,若是两参数一致,则堆不能动态扩展,否则可动态扩展;JDK7版本方法区内的常量池、静态变量移至堆中;JDK8使用元空间(Metaspace)代替永久代,主要存储类信息。
下图所示是线程私有的区域,其中程序计数器是唯一一个没有OOM异常的区域;虚拟机栈由参数-Xss决定栈容量,不能动态扩展。
下图所示是堆外内存,由参数-XX:MaxDirectMemorySize决定大小,没有配置该参数时,默认大小与-Xmx大小一致。需要注意:直接内存不能主动触发GC回收,只有执行Full GC时,顺带回收堆外内存。
二、堆中对象
1. 对象的创建
开发人员创建对象通常仅仅是个new关键字,而JVM中创建对象的过程如下图所示。
一般来说,new指令后面若是紧跟着invokespecial指令,则会执行该Class的<init>()方法,这样一个真正可用的对象才完整构造出来。如下图所示是IDEA插件jclasslib查看字节码,new HashSet<>();会同时生成new和invokespecial指令,invokespecial指令会执行HashSet类的<init>()方法完成对象构造。
2. 对象的内存布局
实例数据的存储顺序会受到虚拟机分配策略参数-XX:FieldsAllocationStyle参数和定义顺序的影响,其默认顺序是longs/doubles、ints、shorts/chars、bytes/booleans、oops。分配顺序原则:相同宽度的字段放在一起,父类字段放在子类字段前面。
3. 对象的访问定位
堆中对象是通过虚拟机栈中的reference数据来访问,其访问方式如下图所示。
两种访问方式:句柄、直接指针,如下图所示。
三、OOM异常
1. 堆OOM异常测试
JVM配置:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError,如下代码所示。
package com.cmmon.instance.oom;
import com.google.common.collect.Lists;
import java.util.List;
/**
* @author tcm
* @version 1.0.0
* @description 测试堆OOM
* @date 2023/5/9 15:48
**/
public class HeapOom {
static class OOMObject { }
/**
* 堆大小设置,报出:java.lang.OutOfMemoryError
* -Xms:JVM启动时堆初始化大小,默认是物理内存的1/64
* -Xmx:堆初最大占用内存大小,默认是物理内存的1/4
* -XX:+HeapDumpOnOutOfMemoryError:JVM异常退出时,生成Dump堆转储快照,进行分析
* VM参数:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public static void main(String[] args) {
List<HeapOom.OOMObject> list = Lists.newArrayList();
while (true) {
list.add(new HeapOom.OOMObject());
}
}
}
测试结果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid13308.hprof ...
Heap dump file created [30014384 bytes in 0.237 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.cmmon.instance.oom.HeapOom.main(HeapOom.java:28)
java_pid13308.hprof堆转储文件分析如下。文件详细参数,参考:https://www.cnblogs.com/duanxz/p/5230856.html
2. 栈SOF异常测试
HotSpot虚拟机栈,不支持动态扩展,所以导致栈java.lang.StackOverflowError异常,有两种情况:
- -Xss参数配置栈容量;
- 方法中定义大量的变量,导致栈帧中本地变量表大。
1):栈容量过小
JVM配置:-Xss108k,如下代码所示。
package com.cmmon.instance.oom;
import lombok.Data;
/**
* @author tcm
* @version 1.0.0
* @description 测试栈 - 设置栈容量大小
* @date 2023/5/9 16:24
**/
public class JVMStackSOF {
@Data
public static class JavaVMStackSOF {
public int stackLength = 1;
public void stackLeak() {
stackLength++;
// 递归调用
stackLeak();
}
}
/**
* 栈容量大小,报出:java.lang.StackOverflowError
* VM参数:-Xss108k
* step1:设置-Xss5k,会提示信息"The stack size specified is too small, Specify at least 108k"
* 说明:当前java的JVM版本与OS,栈容量最低是108k
* step2:设置-Xss大于等于108k
*/
public static void main(String[] args) {
JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
try {
javaVMStackSOF.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + javaVMStackSOF.getStackLength());
throw e;
}
}
}
测试结果如下:
stack length: 991
Exception in thread "main" java.lang.StackOverflowError
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:18)
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:20)
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:20)
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:20)
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:20)
at com.cmmon.instance.oom.JVMStackSOF$JavaVMStackSOF.stackLeak(JVMStackSOF.java:20)
......
2):大量本地变量
JVM配置:无,如下代码所示。
package com.cmmon.instance.oom;
import lombok.Data;
/**
* @author tcm
* @version 1.0.0
* @description 测试栈 - 大量本地变量
* @date 2023/5/9 16:48
**/
public class JVMStackSOF2 {
@Data
public static class JavaVMStackSOF2 {
public int stackLength = 1;
public void stackLeak() {
// 定义大量变量
long useData0, useData1, useData2, useData3, useData4, useData5, useData6, useData7, useData8, useData9,
useData10, useData11, useData12, useData13, useData14, useData15, useData16, useData17, useData18, useData19,
useData20, useData21, useData22, useData23, useData24, useData25, useData26, useData27, useData28, useData29,
useData30, useData31, useData32, useData33, useData34, useData35, useData36, useData37, useData38, useData39,
useData40, useData41, useData42, useData43, useData44, useData45, useData46, useData47, useData48, useData49,
useData50, useData51, useData52, useData53, useData54, useData55, useData56, useData57, useData58, useData59,
useData60, useData61, useData62, useData63, useData64, useData65, useData66, useData67, useData68, useData69,
useData70, useData71, useData72, useData73, useData74, useData75, useData76, useData77, useData78, useData79,
useData80, useData81, useData82, useData83, useData84, useData85, useData86, useData87, useData88, useData89,
useData90, useData91, useData92, useData93, useData94, useData95, useData96, useData97, useData98, useData99;
stackLength++;
// 递归调用
stackLeak();
useData0 = useData1 = useData2 = useData3 = useData4 = useData5 = useData6 = useData7 = useData8 = useData9
= useData10 = useData11 = useData12 = useData13 = useData14 = useData15 = useData16 = useData17 = useData18 = useData19
= useData20 = useData21 = useData22 = useData23 = useData24 = useData25 = useData26 = useData27 = useData28 = useData29
= useData30 = useData31 = useData32 = useData33 = useData34 = useData35 = useData36 = useData37 = useData38 = useData39
= useData40 = useData41 = useData42 = useData43 = useData44 = useData45 = useData46 = useData47 = useData48 = useData49
= useData50 = useData51 = useData52 = useData53 = useData54 = useData55 = useData56 = useData57 = useData58 = useData59
= useData60 = useData61 = useData62 = useData63 = useData64 = useData65 = useData66 = useData67 = useData68 = useData69
= useData70 = useData71 = useData72 = useData73 = useData74 = useData75 = useData76 = useData77 = useData78 = useData79
= useData80 = useData81 = useData82 = useData83 = useData84 = useData85 = useData86 = useData87 = useData88 = useData89
= useData90 = useData91 = useData92 = useData93 = useData94 = useData95 = useData96 = useData97 = useData98 = useData99 = 0;
}
}
/**
* 定义大量本地变量,方法栈中本地变量表大增,报出:java.lang.StackOverflowError
* VM参数:无
*/
public static void main(String[] args) {
JVMStackSOF2.JavaVMStackSOF2 javaVMStackSOF2 = new JVMStackSOF2.JavaVMStackSOF2();
try {
javaVMStackSOF2.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + javaVMStackSOF2.getStackLength());
throw e;
}
}
}
测试结果如下:
stack length: 596
Exception in thread "main" java.lang.StackOverflowError
at com.cmmon.instance.oom.JVMStackSOF2$JavaVMStackSOF2.stackLeak(JVMStackSOF2.java:30)
at com.cmmon.instance.oom.JVMStackSOF2$JavaVMStackSOF2.stackLeak(JVMStackSOF2.java:32)
at com.cmmon.instance.oom.JVMStackSOF2$JavaVMStackSOF2.stackLeak(JVMStackSOF2.java:32)
at com.cmmon.instance.oom.JVMStackSOF2$JavaVMStackSOF2.stackLeak(JVMStackSOF2.java:32)
at com.cmmon.instance.oom.JVMStackSOF2$JavaVMStackSOF2.stackLeak(JVMStackSOF2.java:32)
......
3. 常量池OOM异常测试
JVM:-Xmx6M,如下代码所示。需要注意:JDK8版本中,已经移除-XX:PermSize=6M和-XX:MaxPermSize=6M这两个参数,JDK6的常量池是放在方法区中,属永久代;而JDK8放在堆中,就一直运行下去。
package com.cmmon.instance.oom;
import java.util.HashSet;
import java.util.Set;
/**
* @author tcm
* @version 1.0.0
* @description 测试常量池OOM
* @date 2023/5/9 16:54
**/
public class ConstantPoolOOM {
/**
* 常量池容量限制,报出:java.lang.OutOfMemoryError
* VM参数:-XX:PermSize=6M -XX:MaxPermSize=6M
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=6M; support was removed in 8.0
* Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=6M; support was removed in 8.0
* 上述说明:JDK8版本中,已经移除这两个参数,JDK6的常量池是放在方法区中,属永久代;而JDK8放在堆中,就一直运行下去。
*
* VM参数:-Xmx6M
* Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
*/
public static void main(String[] args) {
Set<String> set = new HashSet<>();
int i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}
测试结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.HashMap.newNode(HashMap.java:1747)
at java.util.HashMap.putVal(HashMap.java:631)
at java.util.HashMap.put(HashMap.java:612)
at java.util.HashSet.add(HashSet.java:220)
at com.cmmon.instance.oom.ConstantPoolOOM.main(ConstantPoolOOM.java:29)
测试JDK8版本常量池在堆中,测试代码如下。
/**
* JDK8版本测试:常量池在堆中
* String::intern
* 字符串常量池已有该字符串:返回该字符串的引用
* 字符串常量池没有该字符串:该字符串添加到常量池中,并返回该字符串的引用
*/
public void constantPoolOOM2() {
String s1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(s1.intern()); // 计算机软件,属于第一次出现
System.out.println(s1.intern() == s1); // true
// sun.misc.Version该类加载时,已经出现该“java”字符串
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern()); // java,属于第二次出现
System.out.println(s2.intern() == s2); // false
}
4. 方法区测试
JVM:-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=128M,如下代码所示。注意:JDK8版本方法区改为元空间,存储类信息;而常量、静态变量放入堆中。
package com.cmmon.instance.oom;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author tcm
* @version 1.0.0
* @description 测试方法区(元空间)
* @date 2023/5/9 17:01
**/
public class MethodAreaOOM {
static class MethodAreaObject { }
/**
* 方法区内存溢出,报出:java.lang.OutOfMemoryError
* -XX:MetaspaceSize设置元空间初始化大小,到达该值触发GC类型卸载,同时会对该值进行调整
* -XX:MaxMetaspaceSize设置元空间最大值,默认-1,整个内存空间
* -XX:MaxMetaspaceFreeRatio设置GC回收后控制元空间剩余容量最大百分比
* -XX:MinMetaspaceFreeRatio设置GC回收后控制元空间剩余容量最小百分比
*
* VM参数:-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=128M
* Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
*/
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MethodAreaOOM.MethodAreaObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o, new String[]{});
}
});
enhancer.create();
}
}
}
测试结果如下:
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:387)
at com.cmmon.instance.oom.MethodAreaOOM.main(MethodAreaOOM.java:40)
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:535)
... 7 more
5. 堆外内存测试
JVM:-Xmx20M -XX:MaxDirectMemorySize=10M,如下代码所示。
package com.cmmon.instance.oom;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @author tcm
* @version 1.0.0
* @description 测试直接内存(堆外内存)OOM
* @date 2023/5/9 17:09
**/
public class DirectMemoryOOM {
/**
* 直接内存溢出,报出:java.lang.OutOfMemoryError
* -XX:MaxDirectMemorySize设置堆外内存最大值,若没有指定大小则与-Xmx大小一致
*
* VM参数:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
public static void main(String[] args) throws IllegalAccessException {
int _1MB = 1024 * 1024;
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
测试结果如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.cmmon.instance.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:30)
四、参考资料
JVM内存模型_小搬砖仔的博客-CSDN博客
https://www.cnblogs.com/duanxz/p/5230856.html
https://www.cnblogs.com/duanxz/p/8510623.html
为对象分配内存——TLAB_usetlab_chengqiuming的博客-CSDN博客
JVM调优常用参数_-xx:+printreferencegc_point-break的博客-CSDN博客