跳到主要内容

08、JVM 实战 - 运行时内存篇,堆

1、核心概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
  • 堆内存的大小是可以调节的。
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
  • 堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

1、 对象都分配在堆上?

是的。特殊情况“栈上分配”,发生JVM性能优化时,将对象分配在栈上,因为栈不会被GC,并且栈执行效率快。

2、 所有的线程都共享堆?

所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

2、堆的内部结构

 

堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间

Young Generation Space 新生区,又细分为Eden区和Survivor区(S0和S1)

Tenure Generation Space 养老区

Meta Space 元空间

年轻代与老年代

1、 存储在JVM中的Java对象可以被划分为两类;

  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
  • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。

2、 Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen);
3、 其中年轻代又可以划分为Eden空间、Survivora空间和Survivor1空间(也叫做from区、to区);
4、 几乎所有的Java对象都是在Eden区被new出来的大对象直接在老年代中创建;
5、 绝大部分的Java对象的销毁都在新生代进行了;

3、如何设置堆内存大小

1、 Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项“-Xms”和”-Xmx”来进行设置;

  • -Xms用于表示堆区的起始内存,等价手-XX:InitialHeapsize
  • -Xmx用于表示堆区的最大内存,等价于-XX:MaxHeapsize

2、 一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常;

3、 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小(根据回收效率来判断是否需要重新计算堆区大小),从而提高性能;

  • heap默认最大值计算方式:如果物理内存少于192M,那么heap最大值为物理内存的一半。如果物理内存大于等于1G,那么heap的最大值为物理内存的1/4
  • heap默认最小值计算方式:最少不得少于8M,如果物理内存大于等于1G,heap默认最小值为物理内存的1/64,即1024/64=16M。最小堆内存在jvm启动的时候就会被初始化。

4、 堆空间大小在实际开发中一般设置为2GB并不是设置的越大越好,可能会影响其他应用,比如ES;

1、 如何设置新生代与老年代比例?

一般新生代占1/3,老年代占2/3。

1、 -XX:NewRatio配置新生代与老年代在堆结构的占比;
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5 2、 可以使用-Xmn设置新生代最大内存大小,这个参数一般使用默认值就可以了;

2、 如何设置Eden、幸存者区比例?

1、 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1;
2、 当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例比如-XX:SurvivorRatio=8;

3、 参数设置总结

4、 初始堆大小和最大堆大小一样,问这样有什么好处?

通常会将 -Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

5、 什么是空间分配担保策略

空间分配担保策略 -XX:HandlePromotionFailure
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
1、 如果大于,则此次MinorGC是安全的;
2、 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败;
3、 如果HandlepromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次MinorGc,但这次MinorGC依然是有风险的;如果小于,则改为进行一次FullGC;

4、对象分配金句

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

1、 过程剖析

如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每经过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。

 

 

1、 new的对象先放Eden区此区有大小限制;
2、 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(MinorGC/YGC),将Eden区中的不再被其他对象所引用的对象进行销毁再加载新的对象放到Eden区;
3、 然后将Eden区中的剩余对象移动到s0区;
4、 如果再次触发垃圾回收,Eden区和s0区的存活对象会放到s1区;
5、 如果再次经历垃圾回收,Eden区和s1区的存活对象会放到s0区如此反复;
6、 啥时候能去养老区呢?可以设置次数默认是15次可以设置参数:-XX:MaxTenuringThreshold=15设置对象普升老年代的年龄阀值;
7、 在养老区,相对悠闲当养老区内存不足时,再次触发GC:MajorGC,进行养老区的内存清理;
8、 若养老区执行了MajorGC之后发现依然无法进行对象的保存,就会产生OOM异常,java.lang.OutOfMemoryError:Javaheapspace;

2、 内存分配原则

针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden
  • 大对象直接分配到老年代。尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保 -XX:HandlePromotionFailure

5、解释MinorGC、MajorGC、FullGC

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

针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:

  • 一种是部分收集(Partial GC),不是完整收集整个]ava堆的垃圾收集。其中又分为:
    ⑴ 新生代收集(Minor GC / Young GC):只是新生代(Eden\s0,s1)的垃圾收集
    ⑵ 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候MajorGC会和Full GC混滑使用,需要具体分辨是老年代回收还是整堆回收。
    ⑶ 混合收集(MixedGc):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
  • 一种是整堆(新生代、老年代、方法区)收集(Full GC),收集整个java堆和方法区的垃圾收集。

1、 MinorGC触发机制

  • 当Eden区空间不足时,就会触发Minor Gc。 Survivor满不会触发GC。
  • 因为Java对象大多都具备朝生夕的特性,所以Minor Gc非常频繁,一般回收速度也比较快。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

2、 MajorGC触发机制

  • 指发生在老年代的GC,对象从老年代消失时,我们说发生了Major GC或Full GC。

  • 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。

  • 在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC

  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。

  • 如果Major GC后,内存还不足,就报OOM了。

3、 FullGC触发机制

触发Full GC执行的情况有如下五种:

1、 老年代空间不足;
2、 方法区空间不足;
3、 通过MinorGC后进入老年代的平均大小大于老年代的可用内存;
4、 由Eden区、s0(From区)区向s1(to区)复制时,对象大小大于To区可用内存,则把该对象转到老年代,且老年代的可用内存小于该对象大小;
5、 调用system.gc()时,系统建议执行FullGC,但是不必然执行;

说明:Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些。

6、OOM如何解决

1、 要解决OOM异常或heapspace的异常,一般的手段是首先通过内存映像分析工具(如EclipseMemoryAnalyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(MemoryOverflow);
2、 如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置;
3、 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗;

7、堆空间分代思想

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

不分代也可以工作。分代的唯一理由就是优化GC性能:

  • 如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
  • 会频繁触发STW,暂停用户线程。

8、快速分配策略:TLAB

为什么有TLAB(Thread Local Allocation Buffer)?什么是快速分配策略?

1、 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
2、 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
3、 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度所以,多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略;

默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABwasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。