菜鸟笔记
提升您的技术认知

java JVM内存结构之堆篇

java jvm内存结构之堆篇

JVM内存结构图

一,堆核心概念叙述

  1. 一个JVM实例只存在一个堆内存,堆也是java内存的核心管理区域

  2. Java堆区在JVM启动的时候即被创建,其空间大小也就确认了。堆内存的大小是可调节的,参数-Xms 设置初始 大小,-Xmx 设置堆空间最大内存

  3. 堆在物理内存中可以处于不连续的,但逻辑上被 视为是连续的

  4. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB:thread local allocation buffer)

  5. 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)

    但存在逃逸分析 , 可以在栈里为对象分配内存 但目前逃逸分析技术还不成熟,在HotSpot虚拟机还没有实践(具体下面 有专门的的讲解)

  6. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收的时候才会被移除。

    堆内存的细分

    以下称呼 新生代 =新生区=年轻代 老年代=老年区=养老区 永久代==永久区

    jdk1.8之前: 堆分为: 新生代,老年代和永久代, 新生代有分为 伊甸园区(Eden),S0区(Survivor)和s1区(Survivor)

    如图所示:

    jdk1.8之: 堆分为: 新生代,老年代和元空间, 新生代有分为 伊甸园区(Eden),S0区(Survivor)和s1区(Survivor)
    如图所示:

《Java虚拟机规范》虚拟机中规范是这样的,但在虚拟机落地实现上 堆真正空间 只包括 新生代和老年代,而永久代/元空间 不在堆内存空间计算内,使用 -XX:+PrintGCDetails 可开启打印查看方法区实现

二,堆空间的大小设置与堆的OOM

堆空间的大小设置

  1. -Xms9m :堆空间的起始内存。X执行 memory start

  2. -Xmx9m:堆空间的最大内存。X执行 memory max 超过最大内存将抛出OOM

  3. 开发中通常将-Xms和-Xmx两个参数配置相同的值。 目的:在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而

    提高性能。

  4. 默认情况下: 初始内存大小=物理电脑运行内存大小/64 最大内存大小=物理电脑运行内存/4

  5. jps命令 查看当前程序运行的进程 jstat 查看JVM在gc时的统计信息 jstat -gc 进程号

堆的OOM举例

设置参数 (怎么设置参数就不介绍了) -Xms200m -Xmx200m

三,年轻代与老年代

为什么要有新生代和老年代?

分代的目的:优化GC的性能(不分代完全可以),对象的生命周期不同,GC回收时间不同,甚至有对象一直不会被GC,而有的对象"朝生夕死"

若不分代–>GC需要扫描整个堆空间,分代之后–>对具体某一区域进行适合的GC

不同代根据其特点进行不同的垃圾回收算法–>提高回收效率(分代收集算法)

新生代与老年代空间默认比例1:2 默认的-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

jinfo -flag NewRatio 进程号,查看参数设定值

为什么新生代被划分为Eden和survivor?

如果没有survivor区,Eden区进行一次MinorGC(YGC),存活对象–>老年代–满-->MajorGC (下面会对GC做初步的解释,跟深层次的GC会持续更新)

MajorGC消耗时间更长,影响程序执行和响应速度。

survivor存在意义:增加进入老年代的筛选条件,减少送到老年代的对象,减少FullGC的次数。也就减少stop the world 用户线程暂停时间,提高用户线程效率

在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例默认是:8:1:1
-XX:SurvivorRatio调整这个空间比例 Eden与Survivor区的比例(调优篇会详细介绍参数怎么设置,待更)

为什么要设置两个survivor区

只有一个survivor区,在第一次Eden区满进行MinorGC,存活对象放到survivor区;第二次Eden区满MinorGC–>survivor区,会产生不连续的内存,无法存放更多的对象。

设置三个四个survivor区,则每个被分配的survivor空间相对较小,很快被填满。

设置两个survivor区,在MinorGC时可以将Eden区和S0存活的对象以连续存储的方式存入S1区。减少碎片化。(清除阶段的复制算法)

四,堆中的对象分配过程

  1. new的对象先放Eden区,放得下直接放入(此区有大小限制 参数-Xmn 一般默认)

  2. 当创建新对象,Eden空间填满,会触发一次Minor GC/YGC,将Eden不再被其他对象引用的对象进行销毁。将Eden中未销毁的对象移到survive0区。survive0区需要销毁的也会被销毁,每个对象都有一个年龄计数器年龄还存活的对象年龄计数加1

  3. 如果Eden有空间,加载的新对象放到Eden区(超大对象放不下直接入老年代)

  4. 再次eden区满,触发垃圾回收,回收eden+survive0(from),幸存下来的放在survive1(to)区

  5. 再垃圾回收,又会将幸存者重新放回survive0区,依次类推,当分代年龄等于15的 对象进老年区(默认值是15 , 可以通过参数 -XX:MaxTenuringThreshold=N进行设置)

  6. 老年代满或放不下,触发majorGC,再放不下,OOM


细节: 触发YGC,幸存者区就会进行回收,不会主动进行回收 .

​ 特别注意: 在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作

​ 如果Survivor区满了后,新对象可能直接晋升老年代

总结:针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。
新生代采用复制算法的目的:为了减少内存碎片,
在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集

五,分代收集思想 MinorGC,MajorGC,FullGC

​ 针对HotSpotJVM的实现,GC按照内存回收区域分为:

​ 部分收集,不是完整收集整个Java堆 : 新生代收集: MinorGC (YoungGC),新生代( Eden、S0/S1 )的垃圾收集

​ 老年代收集,MajorGC/oldGC :目前只有CMS(concurrent mark sweep并行标记扫描) GC会单独收集老年代的行为

​ 混合收集,收集整个新生代以及部分老年代的垃圾收集: 目前只有G1 GC会有这种行为,很多时候MajorGC与FullGC混淆使用,具体分辨是老年代回收还是整堆

​ 整堆收集收集(Full GC),整个Java堆和方法区的垃圾收集

MinorGC的触发条件

​ Eden区满,触发MinorGC,Survivor区满不触发GC。每次MinorGC会清理年轻代(eden+survivor)的内存,因为Java对象大多朝生夕死,所以MinorGC非常频繁,MinorGC会引发STW(stop the world 用户线程暂停)

老年代GC(MajorGC/Full GC)触发条件

​ 老年代空间不足,会触发MinorGC,空间还不足,触发MajorGC/Full GC。还不足,OOM;出现了MajorGC,经常会伴随至少一次MinorGC(也就是老年代空间不足,会尝试MinorGC 对新生代进行回收,还不足,就major GC),但非绝对,在Parallel Scavenge收集器的收集策略里就直接进行MajorGC的策略选择过程;MajorGC的速度比MinorGC慢10倍以上,STW的时间更长(所以调优要减少MajorGC/FullGC)

FullGC的触发机制

  1. 调用System.gc()时,系统建议执行FullGC,但是不必然执行

  2. 老年代空间不足

  3. 方法区空间不足

  4. 通过MinorGC后进入老年代的平均内存大小,大于老年代的可用内存

  5. 由Eden区,Survivor 0区向Survivor 1区复制时,对象的大小大于ToSpace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小,

总结:Minor GC 针对于新生区,Major GC 针对于老年区,Full GC 针对于整个堆空间和方法区

六:内存分配

​ 如果对象再Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor区容纳,则被移动到Survivor空间中,并将对象年龄设置为1,对象再Survivor区每熬过一次MinorGC,年龄就+1,当年龄增加到一定程度(默认为15,不同Jvm,GC都所有不同)时,就会被晋升到老年代中(上面已经 讲了的分配过程)

​ 内存分配总结是: 优先分配到Eden; 大对象直接分配到老年代(尽量避免程序中出现过多的大对象),长期存活的对象分配到老年代

​ 动态对象年龄分配: 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄

​ 空间分配担保:(类似你向银行贷款,对你总资产进行评估,做担保,检查你的总资产是否符合担保条件):

​ 在发生Minor GC之前,jvm会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间

​ 如果大于,则此次MinorGC是安全的

​ 如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败

​ true: 会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的 ,小于,则改为进行一次FullGC

​ false:则改为进行一次FullGC

​ jdk6update24之后,这个参数(-XX:HandlePromotionFailure)不会再影响到虚拟机的空间分配担保策略。

​ 规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC

​ 否则进行FullGC

七:堆中开辟TLAB(Thread Local Allocation Buffer)

​ 堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为避免多个线程操作(指针碰撞方式分配内存)同一地址,需要使用加锁等机制,进而影响分配速度,所以在堆中划分出每一个线程的私有缓存区域(TLAB)

什么是TLAB?

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

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

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

开发人员通过-XX:UseTLAB设置是否开启TLAB空间

默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小

一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用(CAS)加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

我的这篇文章中有讲到:

八:堆在内存中是唯一分配的选择吗?

​ 随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了。有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术

​ TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现了off-heap,实现了将生命周期较长的Java对象从heap中移动heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的

逃逸分析概述

​ 逃逸分析:是对象引用范围分析,减少程序同步负载、内存堆分配压力

​ 什么对象发生了逃逸:被外部方法引用,发生逃逸

​ 没有发生逃逸: 对象在方法中定义,对象只在方法内部使用,未逃逸

​ 没有发生逃逸的对象分配到栈内存上

逃逸分析对代码优化

  1. 栈上分配:调用栈内运行,线程结束,栈空间被回收,局部变量对象也被回收。无须进行垃圾回收。

  2. 同步省略(线程同步代价高,同步降低并发性和性能。又叫锁清除)

动态编译(解释运行)阶段,JIT编译器用逃逸分析,来判断同步块 所使用的锁对象,是否只能被一个线程访问 是,JIT编译器在编译阶段,会取消对这部分代码的同步。提高并发性和性能

  1. 标量替换(分离对象)

JIT编译器在编译阶段,经过逃逸分析,发现一个对象不会被外界访问,那么经过JIT优化,就会把这对象拆解成若干个成员变量来代替。

标量:无法再分解的更小的数据,如Java中原始数据类型,聚合量分解为标量

标量替换参数:-XX:EliminateAllocations,默认打开

逃逸分析小结:

目前逃逸分析技术还不是成熟,所以目前JVM中还没有真正用到栈里分配对象,所以对象还是 都还是堆中分配

不过即时编译器中优化的重要手段,标量替换确实有

九:堆中常用的参数设置

  1. -Xms2g :初始化堆大小为2g

  2. -Xmx2g :最大堆内存为2g

  3. -Xmn : 设置新生区内存大小

  4. -XX:NewRatio=2 : 设置新生代与老年代内存比例为1:2

  5. -XX:SurviveRatio=8 : 设置eden区与survivor区内存比例为8:1:1

  6. -XX:MaxTenuringThreshold : 设置分代年龄阈值

  7. -XX:+UseParNewGC : 指定使用 ParNew + Serial Old 垃圾回收器组合

  8. -XX:+UseParallelGC : 指定年轻代使用Parallel scavenge+Parallel Old并行收集器执行内存回收任务

  9. -XX:+UseParallelOldGC : 默认jdk8开启。默认开启一个,另一个也会被开启。(互相激活)

  10. -XX:+UseConcMarkSweepGC : 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。

  11. -XX:+PrintGCDetails:打印 gc 详细信息圾回收器组合

  12. -XX:+UseParallelGC : 指定年轻代使用Parallel scavenge+Parallel Old并行收集器执行内存回收任务

  13. -XX:+UseParallelOldGC : 默认jdk8开启。默认开启一个,另一个也会被开启。(互相激活)

  14. -XX:+UseConcMarkSweepGC : 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。

  15. -XX:+PrintGCDetails:打印 gc 详细信息