5.Java 堆内存
5.1堆内存概述:
一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域.
Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理 的最大一块内存空间.
堆内存的大小是可以调节. 例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小) 一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重 新分配大小的次数,提高效率.
《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它 应该被视为连续的. 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分 配在堆上.
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
(总结:)
5.2 堆内存区域划分:
Java8 及之后堆内存分为:
新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园(新生成的对象存储))区和 Survivor(幸存者(幸存者0(from),幸存者1(to))区.
5.3 为什么分区(代)?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫 描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
(把不同的生命周期的对象存储在不同的区域中,这样不同的区域可以使用不同的垃圾回收算法,可以提高垃圾回收的效率)
5.4 对象创建内存分配过程:
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如 何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考 虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片.
1.new 的新对象先放到伊甸园区,此区大小有限制.
2.当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
3.然后将伊甸园区中的剩余对象移动到幸存者 0 区.
4.如果再次触发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
5.如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.
6.什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为
15-XX:MaxTenuringThreshold=<N>
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111, 即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。
7.在老年区,相对悠闲,当养老区内存不足时,再次触发垃圾回收(Major GC),进行养老区的内存清理.
8.若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space
对象在堆内存中的过程总结:
新创建的对象存放在伊甸园区,第一次垃圾回收时,垃圾对象直接被回收掉,存活下来的对象,会把他存放在幸存者0/1区,再次垃圾回收时,把在幸存者0区存活的对象移到幸存者1区,将幸存者0区清空,依次交替执行,每一次保证有一个幸存者区域是空的(内存是完整的).当对象经过15次垃圾回收后依然存活的,将被移动到老年区.
老年区垃圾回收的频率比较低.
5.5 新生区与老年区配置比例:
配置新生代与老年代在堆结构的占比(一般不会调) :
1.默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3 .
2.可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5 .
3.当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优.
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比 如-XX:SurvivorRatio=8 .
新生区的对象默认生命周期超过 15 ,就会去养老区养老.
为什么最大是15次?
因为在对象头中只有4个bit为空间,只能表示最大值为15.
(总结:堆各区域的占比:新生代占整堆的1/3; 新生代中的 伊甸园,幸存者0,幸存者1 它们的默认占比为8:1:1)
5.6 堆空间的参数设置:
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
-Xms:初始堆空间内存(默认为物理内存的 1/64)
-Xmx:最大堆空间内存(默认为物理内存的 1/4)
-Xmn:设置新生代的大小(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
一般所说JVM优化,就是调整JVM各区的参数
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
5.7 分代收集思想 Minor GC、Major GC、Full GC:
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的 都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大 类型:一种是部分收集,一种是整堆收集.
部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.
整堆收集出现的情况:
System.gc();时
老年区空间不足
方法区空间不足
开发期间尽量避免整堆收集.
总结:
回收新生代:会触发 minor GC/yong GC;
老年代回收,会触发 Major GC/Old GC;
整堆收集:Full GC
整堆收集的触发的条件:
System.gc();时
老年区空间不足
方法区空间不足
开发期间尽量避免整堆收集.(在垃圾回收时,会STW(stop the world) 回收时,会终止其他线程运行)
5.8TLAB 机制:
为什么有 TLAB(Thread Local Allocation Buffer):
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据. 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存 空间是线程不安全的,为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.
什么是 TLAB?:
TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区, 这是一个线程专用的内存分配区域。
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大 大提升分配效率。
JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。 TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以 通过选项-XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的 百分比大小。
(总结:TLAB 线程本地分配缓存区:
在多线程情况下,可以在堆空间中通过 -XX:UseTLAB 设置,在堆空间中为线程开辟一块空间,用来存储线程中产生的一些对象,避免空间竞争,提高分配效率)
5.9字符串常量池:
JDK7 及以后的版本中将字符串常量池放到了堆空间中。因为方法区的回收效率 很低,在 Full GC 的时候才会执行永久代(方法区)的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。
这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建, 回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
(jdk7 以前将字符串常量池位置为 方法区(永久代)中存储,jdk8之后方法区又称为元空间,jdk8之后将字符串常量池位置放到了堆空间中,因为方法区只有触发FULL GC才会回收,因为程序中大量使用字符串,所以将字符串常量池的位置改变到堆中,可以及时回收无效的字符串常量)