从OOM到JVM内存分配

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中,内存主要分为两大块:

  1. 线程私有的区域: 栈(JVM栈和本地方法栈)、PC(程序计数器)
  2. 线程共享区域:堆、元空间(方法区)

栈、PC和堆在笔记《5.JVM内存区域》已经记录了,这里看看元空间[2]。

MetaSpace

元空间是方法区的在HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。

常用配置参数

  1. MetaspaceSize

初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数

  1. MaxMetaspaceSize

限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。

  1. MinMetaspaceFreeRatio

当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。

  1. MaxMetasaceFreeRatio

当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。

5.MaxMetaspaceExpansion

Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。

6.MinMetaspaceExpansion

Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。

参考资料

[1] 周志明.深入理解Java虚拟机

[2] JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容