java环境
$ java -version
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)
构造OOM条件
1. 设置JVM堆大小,固定40M
-Xmx40m
-Xms40m
2. 设置打印GC信息
- 配置JVM调试参数
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails // 在GC时打印GClog,且在进程退出后输出内存各区域的内存分布情况
-Xloggc:/tmp/gc.log //设置gclog输出的path
- 测试参数效果
public static void main(String[] args) {
// 在新生代分配对象
Runtime r = Runtime.getRuntime();
System.out.println("xms: " + r.totalMemory() / 1024 + "KB");// Xms
System.out.println("xmx: " + r.maxMemory() / 1024 + "KB");// Xmx
System.out.println("free:" + r.freeMemory() / 1024 + "KB");
// 直接在老年代分配内存。老年代空间 50/3*2 ~= 33.3M
byte[] b = new byte[3 * 1024 * 1024];// 创建一个3M左右大小的数组
}
查看GC输出log:
Java HotSpot(TM) 64-Bit Server VM (25.161-b12) for bsd-amd64 JRE (1.8.0_161-b12), built on Dec 19 2017 16:22:20 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(865508k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=41943040 -XX:MaxHeapSize=41943040 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
Heap // 进程退出后的内存情况
PSYoungGen total 11776K, used 4120K [0x00000007bf300000, 0x00000007c0000000, 0x00000007c0000000)
eden space 10240K, 40% used [0x00000007bf300000,0x00000007bf706268,0x00000007bfd00000)
from space 1536K, 0% used [0x00000007bfe80000,0x00000007bfe80000,0x00000007c0000000)
to space 1536K, 0% used [0x00000007bfd00000,0x00000007bfd00000,0x00000007bfe80000)
ParOldGen total 27648K, used 0K [0x00000007bd800000, 0x00000007bf300000, 0x00000007bf300000)
object space 27648K, 0% used [0x00000007bd800000,0x00000007bd800000,0x00000007bf300000)
Metaspace used 2901K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
从GClog显然可以看出来,没有发生GC,且数组是分配在Eden中的,从ParOldGen used 0K可以明显看出来。说明数组的大小没有达到在老年代直接分配的阈值。
当修改数组大小为10M时,得到GC结果如下:
Heap
PSYoungGen total 11776K, used 1048K [0x00000007bf300000, 0x00000007c0000000, 0x00000007c0000000)
eden space 10240K, 10% used [0x00000007bf300000,0x00000007bf406258,0x00000007bfd00000)
from space 1536K, 0% used [0x00000007bfe80000,0x00000007bfe80000,0x00000007c0000000)
to space 1536K, 0% used [0x00000007bfd00000,0x00000007bfd00000,0x00000007bfe80000)
ParOldGen total 27648K, used 10240K [0x00000007bd800000, 0x00000007bf300000, 0x00000007bf300000)
object space 27648K, 37% used [0x00000007bd800000,0x00000007be200010,0x00000007bf300000)
Metaspace used 2906K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
没有发生Minor GC,数组在老年代分配了空间。
3. OOM发生条件
- 对象一般在新生代的Eden区分配空间,大对象(超过阈值)和(大)数组会直接在老年代分配;
- JVM默认老年代和年轻代空间比例3:1,新生代默认Eden和survivor区比例8:1(两个survivor,所以是8:1:1);
- 如果新生代空间不够(触发阈值),会触发minor GC,回收年轻代的垃圾,使用复制算法,超过阈值年龄的存活对象会进入老年代;
- 如果分配空间时老年代空间不够(达到阈值),会触发major GC(就是FUll GC),算法根据垃圾收集器的不同而有所差别,Full GC 之后还是不够,就会抛出OOM错误。
- Java 8没有永久代,MetaSpace是堆外内存
查看JVM参数及其默认值的命令
-XX:+PrintFlagsInitial // 查看初始化的参数和默认值
-XX:+PrintFlagsFinal // 查看最终(配置后的)
-XX:+PrintCommandLineFlags // 查看命令行配置的JVM参数
jinfo工具查看
构造OOM思路,当创建大对象(或者数组)时的所需空间申请不到的时候,会先进行GC,如果GC之后还是不够,就会爆出OOM错误。
4. code
public static void main(String args[]) {
// 在新生代分配对象
Runtime r = Runtime.getRuntime();
System.out.println("xms: " + r.totalMemory() / 1024 + "KB");// Xms
System.out.println("xmx: " + r.maxMemory() / 1024 + "KB");// Xmx
System.out.println("free:" + r.freeMemory() / 1024 + "KB");
// 直接在老年代分配内存。老年代空间 40/3*2 ~= 26.67M
byte[] b = new byte[30 * 1024 * 1024];// 30M的数组,抛出OOM error
}
查看GClog:
Java HotSpot(TM) 64-Bit Server VM (25.161-b12) for bsd-amd64 JRE (1.8.0_161-b12), built on Dec 19 2017 16:22:20 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(828700k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=41943040 -XX:MaxHeapSize=41943040 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
0.161: [GC (Allocation Failure) [PSYoungGen: 843K->432K(11776K)] 843K->440K(39424K), 0.0011782 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.162: [GC (Allocation Failure) [PSYoungGen: 432K->400K(11776K)] 440K->408K(39424K), 0.0007663 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.163: [Full GC (Allocation Failure) [PSYoungGen: 400K->0K(11776K)] [ParOldGen: 8K->336K(27648K)] 408K->336K(39424K), [Metaspace: 2895K->2895K(1056768K)], 0.0043752 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.168: [GC (Allocation Failure) [PSYoungGen: 0K->0K(11776K)] 336K->336K(39424K), 0.0003055 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.168: [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(11776K)] [ParOldGen: 336K->324K(27648K)] 336K->324K(39424K), [Metaspace: 2895K->2895K(1056768K)], 0.0038658 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 11776K, used 307K [0x00000007bf300000, 0x00000007c0000000, 0x00000007c0000000)
eden space 10240K, 3% used [0x00000007bf300000,0x00000007bf34ce58,0x00000007bfd00000)
from space 1536K, 0% used [0x00000007bfd00000,0x00000007bfd00000,0x00000007bfe80000)
to space 1536K, 0% used [0x00000007bfe80000,0x00000007bfe80000,0x00000007c0000000)
ParOldGen total 27648K, used 324K [0x00000007bd800000, 0x00000007bf300000, 0x00000007bf300000)
object space 27648K, 1% used [0x00000007bd800000,0x00000007bd8511d8,0x00000007bf300000)
Metaspace used 2929K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 291K, capacity 386K, committed 512K, reserved 1048576K
由GC log可以发现,年轻代发生了三次GC ,老年代发生了两次Major GC,依然无法申请到需要的空间,JVM只好抛出了OOM。
JVM内存分配
从上述的heap分布来看,堆内存主要分为:
- 年轻代
- 老年代
java8中已经没有永久代的概念,变成了元空间,它不属于heap内存,它是堆外内存。
在jvm中,内存主要分为两大块:
- 线程私有的区域: 栈(JVM栈和本地方法栈)、PC(程序计数器)
- 线程共享区域:堆、元空间(方法区)
栈、PC和堆在笔记《5.JVM内存区域》已经记录了,这里看看元空间[2]。
MetaSpace
元空间是方法区的在HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
常用配置参数
- MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数
- MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
- MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
- MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。
5.MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
6.MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。
参考资料
[1] 周志明.深入理解Java虚拟机
[2] JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)