JVM(二)——浅析内存模型及内存分配机制

目录

JVM整体结构及内存模型

JVM内存参数设置

堆内存参数设置

元空间区参数设置

栈内存参数设置

对象内存分配

对象栈上分配

逃逸分析

标量替换

标量与聚合量

对象Eden分配

大对象直接进入老年代

长期存活进入老年代

对象动态年龄判断

老年代分配担保机制

内存分配整体流程图


JVM整体结构及内存模型

JVM内存参数设置

堆内存参数设置

  • -Xms:堆内存
  • -Xmx:最大堆内存
  • -Xmn:年轻代大小

元空间区参数设置

  • -XX:MaxMetaspaceSize:元空间最大值
  • -XX:MetaspaceSize:元空间触发FullGC阈值(元空间初始大小),以字节为单位,默认是21M,达到该值就会触发FullGC进行类型卸载,同时收集会对该值进行调整,若释放了大量空间会适当调小该值,若释放少量空间那么会在不超过MaxMetaspaceSize(元空间最大值)范围内适当增大该值。
    Tips:调整元空间大小需要FullGC,这是非常昂贵的操作,如果在启动时发现大量FullGC,通常都是元空间大小进行了调整,所以建议在JVM启动参数中,设置MetaspaceSize和MaxMetaspaceSize为一样的值

栈内存参数设置

  • -Xss:栈大小,该值越小,每个线程栈能分配的栈针就越少,但是能开启的线程数越多,反之能分配的栈针增多,但开启的线程数减少

对象内存分配

对象栈上分配

逃逸分析

JVM会通过逃逸分析确定该类会不会被外部访问,如果不会逃逸,则会在栈上分配内存,该对象所占内存会随栈针的出栈而销毁,减轻了垃圾回收的压力

标量替换

通过逃逸分析确定该对象不会被外部对象访问,并且对象会被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈针或寄存器上分配空间,这样就不会因为没有一大块连续空间导致内存不够分配,开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启

标量与聚合量

标量即不可分解的量,Java的基本类型就是标量(如:int、long等基本类型),标量的对立就是可以被分解的量,这种的量被称为聚合量,Java中对象一般都是可以被分解的聚合量

对象Eden分配

对象进行分配一般都是在Eden区分配,如果Eden没有足够空间,则会触发一次YoungGC,Eden区与Survivor区的比例是8:1:1,这个是JVM默认的比例,让Eden尽量大,Survivor足够用即可,示例:

public class GCTest {
    public static void main(String[] args) {
        //-Xms512M -XX:+PrintGCDetails
        byte[] a = new byte[120000 * 1024];
    }
}

 打印内容如下:

Heap
 PSYoungGen      total 153088K, used 130527K [0x0000000715580000, 0x0000000720000000, 0x00000007c0000000)
  eden space 131584K, 99% used [0x0000000715580000,0x000000071d4f7d70,0x000000071d600000)
  from space 21504K, 0% used [0x000000071eb00000,0x000000071eb00000,0x0000000720000000)
  to   space 21504K, 0% used [0x000000071d600000,0x000000071d600000,0x000000071eb00000)
 ParOldGen       total 349696K, used 0K [0x00000005c0000000, 0x00000005d5580000, 0x0000000715580000)
  object space 349696K, 0% used [0x00000005c0000000,0x00000005c0000000,0x00000005d5580000)
 Metaspace       used 3216K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

此时Eden区已经被分配满了,此时在往Eden区分配内存,示例如下:

public class GCTest {
    public static void main(String[] args) {
        //-Xms512M -XX:+PrintGCDetails
        byte[] a = new byte[120000 * 1024];
        byte[] b = new byte[21504 * 1024];
    }
}

 打印内容如下:

[GC (Allocation Failure) [PSYoungGen: 130895K->1099K(153088K)] 130895K->124107K(502784K), 0.0852640 secs] [Times: user=0.35 sys=0.05, real=0.09 secs] 
Heap
 PSYoungGen      total 153088K, used 10415K [0x0000000715580000, 0x0000000728080000, 0x00000007c0000000)
  eden space 131584K, 7% used [0x0000000715580000,0x0000000715e990e0,0x000000071d600000)
  from space 21504K, 5% used [0x000000071d600000,0x000000071d712c50,0x000000071eb00000)
  to   space 21504K, 0% used [0x0000000726b80000,0x0000000726b80000,0x0000000728080000)
 ParOldGen       total 349696K, used 123008K [0x00000005c0000000, 0x00000005d5580000, 0x0000000715580000)
  object space 349696K, 35% used [0x00000005c0000000,0x00000005c7820010,0x00000005d5580000)
 Metaspace       used 3218K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

此时看到a对象直接被移入到老年代,因为给b分配的时候eden区内存几乎用完了,Eden区在没有足够内存进行分配的时候会触发一次YoungGC,GC期间虚拟机发现a对象无法进入survivor区,所以提前把新生代对象转移到老年代中,老年代中有足够区间存放a,所以不会触发FullGC,后面如果分配对象能够在Eden区,还是会在Eden区进行分配,示例如下:

public class GCTest {
    public static void main(String[] args) {
        //-Xms512M -XX:+PrintGCDetails
        byte[] a = new byte[123000 * 1024];
        byte[] b = new byte[8000 * 1024];
        byte[] c = new byte[8000 * 1024];
        byte[] d = new byte[8000 * 1024];
        byte[] e = new byte[8000 * 1024];
        byte[] f = new byte[8000 * 1024];
    }
}

 打印内容如下:

[GC (Allocation Failure) [PSYoungGen: 130895K->1099K(153088K)] 130895K->124107K(502784K), 0.0841600 secs] [Times: user=0.39 sys=0.04, real=0.09 secs] 
Heap
 PSYoungGen      total 153088K, used 44994K [0x0000000715580000, 0x0000000728080000, 0x00000007c0000000)
  eden space 131584K, 33% used [0x0000000715580000,0x000000071805dc30,0x000000071d600000)
  from space 21504K, 5% used [0x000000071d600000,0x000000071d712c50,0x000000071eb00000)
  to   space 21504K, 0% used [0x0000000726b80000,0x0000000726b80000,0x0000000728080000)
 ParOldGen       total 349696K, used 123008K [0x00000005c0000000, 0x00000005d5580000, 0x0000000715580000)
  object space 349696K, 35% used [0x00000005c0000000,0x00000005c7820010,0x00000005d5580000)
 Metaspace       used 3219K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

大对象直接进入老年代

怎么定义大对象 我们可以通过-XX:PretenureSizeThreshold参数来设置大对象大小,如果对象超过设置大小则会直接被分配在老年代,示例如下:

public class GCTest {
    public static void main(String[] args) {
        //-Xms512M -XX:+PrintGCDetails
        byte[] a = new byte[120000 * 1024];
    }
}

打印如下:

Heap
 par new generation   total 157248K, used 11182K [0x00000005c0000000, 0x00000005caaa0000, 0x000000066aaa0000)
  eden space 139776K,   8% used [0x00000005c0000000, 0x00000005c0aebb58, 0x00000005c8880000)
  from space 17472K,   0% used [0x00000005c8880000, 0x00000005c8880000, 0x00000005c9990000)
  to   space 17472K,   0% used [0x00000005c9990000, 0x00000005c9990000, 0x00000005caaa0000)
 tenured generation   total 349568K, used 120000K [0x000000066aaa0000, 0x0000000680000000, 0x00000007c0000000)
   the space 349568K,  34% used [0x000000066aaa0000, 0x0000000671fd0010, 0x0000000671fd0200, 0x0000000680000000)
 Metaspace       used 3216K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

 我们可以看到未开设置之前原本应该放在eden区的对象,现在分配在了老年代。

Tips:因为-XX:PretenureSizeThreshold只在Serial和ParNew下生效,所以需要指定垃圾收集器,上面我是制定了使用了ParNew收集器-XX:+UseParNewGC,G1收集器也有大对象的概念,它有专门存放大对象的区域叫Humongous区,G1判定的条件是超过region的大小的一半则会判断为巨型对象。

长期存活进入老年代

每次Eden区内存不足都会触发一下YoungGC,如果对象没有被回收掉且可以被Survivor区接纳的话,对象的年龄+1,对象再Survivor区经历过一次YoungGC年龄都会+1,直到年龄到达一定岁数(通过-XX:MaxTenuringThreshold来设置)就会被晋升到老年代。

对象动态年龄判断

当前放对象的Survivor区域如果有一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批年龄最大值之后的对象就可以直接进入老年代了,例如年龄1+年龄2+······+年龄n(内存大小)>=Survivor区域内存50%,那么年龄n及以上的对象都直接放入老年代,这个规则其实希望那些可能长期存活的对象提早进入老年代,减少Survivor区域的复制损耗,对象动态年龄判断机制一般是在YoungGC之后触发的。

老年代分配担保机制

年轻代每次进行YoungGC之前都会判断一下老年代剩余可用空间,如果老年代剩余可用空间小于年轻代所有对象(包括垃圾对象)内存总和,就会看-XX:HandlePromotionFailure参数有没有配置,若配置了则会判断历史每次YounGC之后进入老年代的对象大小的平均值,若可用内存小于平均值则会触发FullGC将老年代和新生代一起进行垃圾回收,若回收后还没有足够的空间进行分配就会发生OOM,如果可用内存大于平均值就会触发YoungGC,但是如果YoungGC之后进入老年代的对象大小还是大于老年代可用内存空间也会发生OOM,若未配置直接进行FullGC进行垃圾回收,回收后还无足够空间也会发生OOM。老年代分配担保机制流程图如下:

内存分配整体流程图

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值