JVM——堆空间(新生代老年代)、对象堆空间分配详解

概述

  • 一个JVM进程只存在一个堆空间
  • 在JVM启动的时候就被创建,空间大小也已经确定(可设置大小),是JVM管理的最大的内存空间,是JVM调优中最需要关注的地方。
  • 堆可以处于物理上不连续的内存空间中,但是逻辑上它是被视为连续的(虚拟内存可以映射不连续的物理内存)
  • 所有线程共享堆空间(其中例外的情况是线程私有缓冲区,Thread Local Allocation Buffer,TLAB,为了解决多线程访问的并发性问题
  • 所有对象的实例以及数组都应当在运行时分配到堆中(JVM规范中描述)
    • 创建对象实例或者数组的指令
      • new
      • newarray
      • anewarry
    • 从实际角度看,并不是所有的,只能说几乎所有的(因为存在栈上分配这一操作)
  • 在方法执行结束,或者说是某个线程执行结束后,其执行过程中创建的对象不会马上在堆中进行移除,仅在垃圾回收的时候才有可能被移除(使用过的对象实例,需要在GC的时候判断是否是个垃圾,才有可能被GC处理)

堆空间内存细分

现代垃圾收集器大部分都是基于分代收集理论设计的,所以相应的就存在分代垃圾回收算法:分代算法。当然这里不是要说垃圾回收的问题,既然有垃圾回收的分代算法,那么相应的堆空间是有分代情况的。

以下说的都是Hotspot VM

JDK7以及之前的版本逻辑上分为以下三个部分:

  • 新生区(Young Generation space)
    • 伊甸园区(Eden space)
    • 幸存者区(Survivor space):分为幸存者0区和幸存者1区
  • 养老区(Tenure Generation space)
  • 永久区(Permanent space)

JDK8以及之后的版本逻辑上分为一下三个部分:

  • 新生区
    • 伊甸园区
    • 幸存者区:分为幸存者0区和幸存者1区
  • 养老区
  • 元空间(Meta space)

其实叫什么的都有,什么老年代,新生代balabala的,只要能对应上就行了

堆空间大小

在JVM启动的时候就被创建,空间大小也已经确定

可以通过如下代码查看:

//查看当前堆的大小
Runtime.getRuntime().totalMemory();//单位b
//查看堆的最大内存占用
Runtime.getRuntime().maxMemory();//单位b

默认:

  • 起始内存大小为物理内存的1/64
  • 最大内存大小为物理内存的1/4

手动设置:

  • -Xms:堆的起始内存大小,等价于 -XX:InitialHeapSize
  • -Xmx:堆的最大内存,等价于-XX:MaxHeapSize

格式:-Xms[size],如果不指定单位就是b,可以使用k、m、g

需要注意的是:

  • 这里设置的小大指的是:新生代+老年代的大小,不包括元空间
  • 通常情况下,在设置-Xms和-Xmx的时候,会将两者设置成一样的大小。因为如果两者不一致,堆占用的空间可能会在使用过程中扩容,或者是GC回收了垃圾后堆内存又随之减少,在这样的情况下,不断地扩容或者收缩内存,也是对系统的一种负担。
  • 通常新生代的幸存者区,会有两个:Survivor 0和 Survivor 1,由于这两个幸存者区中,只会有一个使用到,通常计算新生代大小=伊甸园区+一个幸存者区的大小

查看堆区占用信息

  • 使用JDK中lib目录下的工具:jvisualvm,在其中安装一个GC插件,可视化的动态监控堆区的信息
  • 使用命令:
    • jps & jstat -gc [进程号]:jsp可以查看当前的Java进程,jstat -gc 可以查看某个Java进程的GC信息
    • -XX:PrintGCDetails:和上面的命令大致相同,在运行Java程序的时候使用该参数,在Java程序执行结束的时候会打印GC信息

年轻代和老年代

在JVM中的Java对象,从其生命周期长短来说,大致分为两类:

  • 生命周期很短的瞬时对象,其创建和消亡是很快的
  • 生命周期很长的对象,极端情况下可能和JVM的生命周期相当
  • 几乎所有的Java对象都是在Eden区被new出来的
  • 绝大分布Java对象的销毁也都是在Eden区进行的

比例设置

  • 默认情况下年轻代和老年代比例为1:2
  • 通过参数-XX:NewRatio=4,新生代和老年代的比例为1:4

新生代中各区域所占比例:

  • 默认情况下 Eden:Survivor0:Survivor1 = 8:1:1
  • 但是!通过jvisualvm工具或者是jps&jstat -gc [Java进程号]这两种方式查看其比例的时候,通常不是8:1:1
    • 原因是因为JVM自适应内存分配策略的影响,具体没有深入研究,可以通过-XX:-UseAdaptiveSizePolicy来关闭该策略,-XX:+UseAdaptiveSizePolicy开启该策略,但是某些情况下关闭了该策略,实际情况可能还不是预期值
    • 要想达到精确的控制,需要手动设置这三者的比例:-XX:SurvivorRatio=8

注意:新生代的大小可以使用-Xmn来设置其内存占用大小,如果-Xmn和-XX:NewRatio同时设置了,以-Xmn为主

对象分配过程

创建一个对象并对其进行内存分配是一个谨慎复杂的事情,需要考虑怎么分配、分配在哪,还需要考虑GC垃圾回收之后是否会产生内存碎片等问题。

  • 对象的创建首先是在新生代的Eden区存放
  • 当Eden区的内存空间随着对象的不断创建而放满之后,会发生GC,称之为YGC(Young garbage collection,也叫Minor GC),在经历过GC之后,没有被引用的对象,将会被销毁,仍在被引用的对象将会被放在Survivor0区(需要注意的是先放Survivor0和Survivor1,不确定,Survivor区又被称为from和to区,from和to这两个角色具体是谁不是一定的,此时Surviror0为from区,Survivor1为to区),并且为每个对象的年龄计数器赋值,此时为1

JVM——堆空间(新生代老年代)、对象堆空间分配详解_第1张图片

  • 随着程序的运行,Eden区若再次放满之后又会经历一次GC,没有被应用的对象被销毁,仍在被使用的对象被放在Surviror1中(这里其实准确的来说是放在Survivor区中目前为空的那个),如果此时Survivor0区中之前一次GC幸存下来的对象还在使用,则一并转移到Survivor1中,年龄计数器+1,此时Survivor0为空了,Survivor0变为to区,Survivor1变为from区(谁空谁是to区)

JVM——堆空间(新生代老年代)、对象堆空间分配详解_第2张图片

  • 之后随着程序的运行,进行如上的过程。当某些对象在Survivor区来回数次之后,年龄计数器达到阈值之后(默认是15,可通过-XX:MaxTenuringThreshold=[age]设置),将会被放入老年代

【需要特别注意的是:Survivor区满了是不会触发GC的,当然并不是说Survivor没有垃圾回收。当Eden区满了之后触发GC,GC处理除了Eden区之外,会对Survivor一并进行垃圾回收的。所以Survivor是被动的GC】

对象分配过程的特殊情况

顺便提一下,在手动明确设置了堆区大小的时候:

  • 如果在分配对象到Eden区的时候发现放不下,触发了GC,还是放不下,那么就会老年代尝试存放,如果老年代也放不下会触发FGC,如若还是放不下直接OOM。
  • 如果在Survivor被动GC之后,Survivor出现了放不下的情况,如果放不下,将不会判断对象的年龄计数器,直接将对象放在老年代

对象分配策略

  • 优先分配Eden区
  • 大对象Eden区放不下,直接放在老年代
    • 所以尽量的避免大对象的存在:Eden区放不下就会触发Minor GC,只要是GC,就一定程度上会出现STW
  • 长期存活的对象分配到老年代当中:年龄计数器到阈值的对象(默认15)
  • 动态对象年龄判断
    • 如果Survivor中相同年龄的所有对象的大小总和 > Survivor空间的一半,这些等于或者大于该年龄的对象直接进入老年代,就不用等到-XX:MaxTenuringThreshold设置的阈值再进入老年代

TLAB(Thread Local Allocation Buffer)

JVM会对Eden区进行划分,为每一条线程创建其对应的私有缓冲区

  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的效率(吞吐量),所以这种内存分配的方式也称为快速分配方式。
  • 由于线程对应的TLAB区较小,所以并不是所有对象都可以在其中分配,但是JVM会将TLAB区作为首选。
  • 使用-XX:UseTLAB开启关闭TLAB空间
  • 默认情况下TLAB空间很小(仅占整个Eden空间的1%),通过-XX:TLABWasteTargetPercent设置TLAB空间所占Eden区的大小

堆空间参数设置总结

  • -XX:+PrintFlagsInitial:查看所有JVM虚拟机的默认值(不受自行设定的参数值影响)
  • -XX:+PrintFlagsFinal:查看所有参数的实际值
    • 查询某个指定参数的值:jps(查询当前运行中的Java进程)+ jinfo -flag [设置项] [Java进程pid]
  • ​​​​​​​-XX:+PrintGCDetails:Java程序执行结束的时候会打印GC信息
    • jps & jstat -gc [进程号]:jsp可以查看当前的Java进程,jstat -gc 可以查看某个Java进程的GC信息
  • -Xms[size]:堆的起始内存大小,等价于 -XX:InitialHeapSize(起始内存大小默认为物理内存的1/64​​​​​​​)
  • -Xmx[size]:堆空间占用最大内存(最大内存大小默认为物理内存的1/4)
  • -Xmn[size]:设置新生代的大小
  • -XX:NewRatio=[num]:设置新生代和老年代在堆空间的比例
  • -XX:SurvivorRatio=[num]:设置新生代中Eden和Survivor0/Survivor1的比例(默认情况下 Eden:Survivor0:Survivor1 = 8:1:1)
  • -XX:MaxTenuringThreshold=[age]:设置新生代的最大年龄(被放入老年代前的年龄阈值,默认15)

你可能感兴趣的:(#,JVM,heap,堆分配,对象分配,JVM)