• Java 栈(JVM栈),Java 堆,方法区的联系:

image.png
Java 栈存放局部变量的引用(实例对象的地址),堆存放引用对应的实例对象,方法区则存放实例对象的类和方法实现。

堆的细分内存结构

JDK 7以前: 逻辑上分为新生区+养老区+永久区(即Xms/Xmx分配的内存物理上没有涉及永久区)

  • Young Generation Space 新生区 Young/New
    • 又被分为Eden区和Survior区
  • Tenure generation Space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

Java 堆 - 图2
JDK 8以后: 逻辑上分为新生区+养老区+元空间(即Xms/Xmx分配的内存物理上没有涉及元空间)

  • Young Generation Space 新生区 Young/New
    • 又被分为Eden区和Survior区
  • Tenure generation Space 养老区 Old/Tenure
  • Meta Space 元空间 Meta(方法区的具体实现)

Java 堆 - 图3

在JDK 1.8及以后,永久代会被移除,换为metaspace(元空间)

约定:新生区=新生代=年轻代 养老区=老年区=老年代 永久区=永久代

年轻代与老年代

  1. 存储在JVM中的java对象可以被划分为两类:
  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
  • 另外一类对象时生命周期非常长,在某些情况下还能与JVM的生命周期保持一致
  1. Java堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)。其中年轻代可以分为Eden空间、Survivor0空间和Survivor1空间(有时也叫frmo区,to区)

Java 堆 - 图43. 配置新生代与老年代在堆结构的占比

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

Java 堆 - 图5

  1. 在hotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项-XX:SurvivorRatio调整空间比例,如-XX:SurvivorRatio=8
  2. 几乎所有的Java对象都是在Eden区被new出来的
  3. 绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是「朝生夕死」的)
  4. 可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)

    图解对象分配过程

    一般过程

    image.png
    image.png
    image.png

  5. new的对象先放伊甸园区。此区有大小限制;

  6. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区;
  7. 然后将伊甸园中的剩余的幸存对象移动到幸存者0区。
  8. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  9. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  10. 啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold进行设置。
  11. 在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
  12. 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。

总结:针对幸存者s0,s1区:复制之后有交换,谁空谁是to。
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。**

特殊情况

image.png

Minor GC、Full GC

JVM在进行GC时,并非每次都针对三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收都是指新生代。

针对hotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)。

  • Partial GC:不是完整收集整个Java堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC/Young GC):只对新生代进行垃圾收集
    • 老年代收集(Old GC):只对老年代进行垃圾收集
      • 只有CMS的concurrent collection是这个模式
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
      • 混合收集不涉及方法区回收,只是新生代,老年代的混合收集。
      • 目前,只有G1 GC会有这种行为。
  • 整堆收集(Full GC):收集整个java堆和方法区。


  • Major GC:通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说「major GC」的时候一定要问清楚他想要指的是上面的「full GC」还是「old GC」。

    最简单的分代式GC策略的触发条件

    年轻代GC(Minor GC)触发机制

  • 当年轻代中的Eden区满时,就会触发Minor GC(每次Minor GC会清理年轻代的内存,Survivor是被动GC,不会主动GC);注意Minor GC中有部分存活对象会晋升到old gen,所以Minor GC后老年代的占用量通常会有所升高。

  • 因为Java队形大多都具备「朝生夕灭」的特性,所以Minor GC 非常频繁,一般回收速度也比较快,这一定义既清晰又利于理解。
  • Minor GC 会引发STW(Stop the World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。

Java 堆 - 图10

Full GC触发机制

  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  • 当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);
  • 方法区空间不足
  • heap dump带GC

  • 说明:Full GC 是开发或调优中尽量要避免的,这样暂停时间会短一些;Full GC主要由 Minor GC 触发。

**
HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。

当然也总有例外。Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。这是HotSpot VM里的奇葩嗯。

并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

堆空间分代思想

为什么要把Java堆分代?不分代就不能正常工作了么

  • 经研究,不同对象的生命周期不同。70%-99%的对象都是临时对象。
    • 新生代:有Eden、Survivor构成(s0,s1 又称为from to),to总为空
    • 老年代:存放新生代中经历多次依然存活的对象
  • 其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

    内存分配策略

    1. 对象优先在 Eden 分配

    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

    2. 大对象直接进入老年代

    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
    经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
    -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

    3. 长期存活的对象进入老年代

    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
    -XX:MaxTenuringThreshold 用来定义年龄的阈值。

    4. 动态对象年龄判定

    虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

    5. 空间分配担保

    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
    如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

    TLAB

    Thread Local Allocation Buffer,堆当中的线程私有缓存区域

    为什么有TLAB?

  • 众所周知堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。

  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
  • 一般为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
  • 为了解决这一问题,TLAB应运而生。

    什么是TLAB?

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内

  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

    TLAB说明

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,单JV明确是是将TLAB作为内存分配的首选

  • 在程序中,开发人员可以通过选项-XX:UseTLAB设置是够开启TLAB空间(默认开启)
  • 默认情况下,TLAB空间的内存非常小,仅占有整个EDen空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配了内存

    代码演示

  • 运行程序后,终端输入jps查看TLABArgsTest进程id

  • jinfo -flag UseTLAB 64566(进程id),输出-XX:+UseTLAB,证明TLAB默认是开启的

    1. /**
    2. * 测试-XX:UseTLAB参数是否开启的情况:默认情况是开启的
    3. */
    4. public class TLABArgsTest {
    5. public static void main(String[] args) {
    6. System.out.println("我只是来打个酱油~");
    7. try {
    8. Thread.sleep(1000000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. }

    TLAB对象分配过程

    Java 堆 - 图11

  • 创建实例对象时先进行TLAB分配,如果实例对象所需空间比较大,那么就直接在Eden中分配,如果比Eden可用空间还大,那就启动GC(也有可能直接放到老年代

    堆空间的参数设置

  • -XX:PrintFlagsInitial:查看所有参数的默认初始值

  • -XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
    • 具体查看某个参数的指令:
      • jps:查看当前运行中的进程
      • jinfo -flag SurvivorRatio 进程id: 查看新生代中Eden和S0/S1空间的比例
  • -Xms: 初始堆空间内存(默认为物理内存的1/64)
  • -Xmx: 最大堆空间内存(默认为物理内存的1/4)
  • -Xmn: 设置新生代大小(初始值及最大值)
  • -XX:NewRatio: 配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)
  • -XX:+PrintGCDetails:输出详细的GC处理日志
    • 打印gc简要信息:① -XX:+PrintGC-verbose:gc
  • -XX:HandlePromotionFailure:是否设置空间分配担保

    设置堆内存大小

  1. Java堆区用于存储java对象实例,堆的大小在jvm启动时就已经设定好了,可以通过-Xmx-Xms来进行设置
  • -Xms用来设置堆空间(年轻代+老年代)的初始内存大小,等价于-XX:InitialHeapSize
    • -X是jvm的运行参数
    • ms是memory start
  • -Xmx用于设置堆的最大内存,等价于-XX:MaxHeapSize
  1. 一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OOM(out of memory)异常。
  • 默认情况下,初始内存大小:
  • 初始堆空间内存:物理内存大小/64;最大堆空间内存:物理内存大小/4。
  • 手动设置:-Xms600m -Xmx600m
    • 通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
    • 比如说:默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
    • 因此服务器一般设置-Xms-Xmx相等以避免在每次GC 后调整堆的大小
  1. 查看设置的堆内存参数:
  • 方式一: 终端输入jps , 然后jstat -gc进程id;
    • HeapSpaceInitial启动时设置参数: -Xms500m -Xmx500m

image.png
计算下堆内存大小:(S0C+EC+OC)/1024=479.5M
我们可以发现少了20.5M而且计算式中我们并没有加上S1C。实际上,堆内存中新生代Survior区的S0和S1永远只会有一个区域存放数据,另一个是不存放任何数据的,所以我们只计算其中一块区域。

  • 并不是一开始就定死S0和S1其中一个不存放数据,而是随着程序运行不断切换
    • 这是一个程序运行中的堆内存空间图,可以看到S0,S1是在不断切换的

image.pngimage.png

  • 方式二:IDEA控制台打印)Edit Configurations->VM Options 添加-XX:+PrintGCDetails

image.png
新生代内存=eden+from=149504K(只加了一个Survior区)

参考