JVM之堆
核心概述
一个JVM实例值存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候就被创建了,其空间大小也就确定了,是JVM管理的最大一块内存空间。《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB,Thread Local Allocation Buffer)。《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上(但并不是全部)。数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是GC(垃圾收集器)执行垃圾回收的重点区域。
注意:
- Java 7之前和图上一模一样,Java 8把永久区换成了元空间(方法区)
- 堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成
- 当执行
new Person();
时,其实是new在新生区的伊甸园区,然后往下走,走到养老区,但是并未到元空间。 - 虽然说,逻辑上是将堆空间划分为新生代、老年代和永久代三部分,实际上是不考虑永久代(也就是Java8中提到的元空间)的,可以将其看做是方法区的落地实现。
注意: - GC发生在伊甸园区,当对象快占满新生代时,就会发生YGC(Young GC,轻量级GC)操作,伊甸园区基本全部清空
- 幸存者0区(S0),别名“from区”。伊甸园区没有被YGC清空的对象将移至幸存者0区,幸存者1区别名“to 区”
- 每次进行YGC操作,幸存的对象就会从伊甸园区移到幸存者0区,如果幸存者0区满了,就会继续往下移,如果经历数次YGC操作对象还没有消亡,最终会来到养老区
- 如果到最后,养老区也满了,那么就对养老区进行FGC(Full GC,重GC),对养老区进行清洗
- 如果进行了多次FGC之后,还是无法腾出养老区的空间,就会报**OOM(out of Memory)**异常
- from区和to区位置和名分不是固定的,每次GC过后都会交换,GC交换后,谁空谁是to区
- 大对象直接进入养老区,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
- 注意:
- 整个堆分为新生区和养老区,新生区占整个堆的1/3,养老区占2/3。新生区又分为3份:伊甸园区:幸存者0区(from区):幸存者1区(to区) = 8:1:1
- 每次从伊甸园区经过GC幸存的对象,年龄(代数)会+1
-
-XX:MaxTenuringThreshold=15
调整多少代进入老年区 - 关于默认的晋升年龄是15,这个说法的来源大部分都是《深入理解Java虚拟机》这本书。 如果你去Oracle的官网阅读相关的虚拟机参数,你会发现
-XX:MaxTenuringThreshold=threshold
这里有个说明
Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.
默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.
堆空间的分代思想
为什么需要把Java堆分代?不分代就不能正常工作了吗?
经研究,不同对象的生命周期不同,70%-90%的对象是临时对象。
TLAB(Thread Local Allocation Buffer)
我们知道,堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度,TLAB就是为了解决这一问题。
什么是TLAB?
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选。
- 在程序中,开发人员通过"-XX:UseTLAB"设置是否开启TLAB空间(可以通过
jinfo -flag UseTLAB 线程号
查询是否开启)。 - 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,可以通过选项
-XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。 - 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
注意:
- 临时对象就是说明,其在伊甸园区生,也在伊甸园区死。
- 堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成,元空间也叫方法区
- 永久代(方法区)几乎没有垃圾回收,里面存放的都是加载的rt.jar等,让你随时可用
注意
- 上面的图展示的是物理上的堆,分为两块,新生区和养老区。
- 堆的参数主要有两个:
-Xms
,Xmx
:
-
-Xms
堆的初始化的内存大小 -
-Xmx
堆的最大内存
- Young Gen(新生代)有一个参数
-Xmn
,这个参数可以调新生区和养老区的比例。但是,这个参数一般不调。 - 永久代也有两个参数:
-XX:PermSize
,-XX:MaxPermSize
,可以分别调永久代的初始值和最大值。Java 8 后没有这两个参数啦,因为Java 8后元空间不在虚拟机内啦,而是在本机物理内存中 - 一个JVM只有一个堆内存
- 保存我们所有引用类型的真实对象
- 3个区域
- 新生区(伊甸园): 所有的对象都new在了伊甸园去,从中出生
- 养老区-- 老年区 --从新生区(幸存0幸存1)中顺下来的,干不掉,杀不死
- 永久区
- 此区域不存在垃圾回收,但是也会崩( 启动了大量第三方jar) – 永久区是一个常驻内存区域,用于存放JDK自身携带的Class Interface的元数据
- jdk1.6之前永久代,常量池是在
方法区
- jdk1.7永久代,慢慢退化,去永久代,常量池在
堆
中
- jdk1.8无永久代,常量池在
元空间
- 关闭虚拟机就会释放这块内存
gc垃圾回收主要针对新生区(伊甸园区)和养老区
//查看自己机器上的默认堆内存和最大堆内存
public class Test{
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
//返回 Java虚拟机试图使用的最大内存量。物理内存的1/4(-Xmx)
long maxMemory = Runtime.getRuntime().maxMemory() ;
//返回 Java虚拟机中的内存总量(初始值)。物理内存的1/64(-Xms)
long totalMemory = Runtime.getRuntime().totalMemory() ;
System.out.println("MAX_MEMORY =" + maxMemory +"(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("DEFALUT_MEMORY = " + totalMemory + " (字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
}
}
注意:JVM参数调优,平时可以随便挑初始大小和最大大小,但是实际工作中,初始大小和最大大小应该是一致的,原因是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。避免内存忽高忽低产生停顿
idea 的JVM内存配置
- 点击Run列表下的Edit Configuration
- 在VM Options中输入以下参数:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- 把堆内存调成10M后,再一直new对象,导致Full GC也无法处理,直至撑爆堆内存,查看堆溢出错误(OOM),程序及结果如下:
GC收集日志信息详解
- 第一次进行YGC相关参数:
[PSYoungGen: 2008K->482K(2560K)] 2008K->782K(9728K), 0.0011440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最后一次进行FGC相关参数:
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4025K->4005K(7168K)] 4025K->4005K(9216K), [Metaspace: 3289K->3289K(1056768K)], 0.0082055 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
官网 - JProfiler工具分析OOM的原因
-Xms 设置初始化内存大小
-Xmx 设置最大分配内存
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintGCDetails
堆内存调优
- 报OOM时,首次按尝试扩大堆内存空间查看结果,分析内存,查看一下哪个地方出现问题(JProfiler)
- JProfiler作用:分析DumpN内存文件,快速定位内存泄漏,获得堆中的数据,获取大的对象。
- 虚拟机基本配置参数
- -Xms 设置Java程序启动时的初始堆大小-- 初始化内存大小
- -Xmx 设置java程序能获得的最大堆大小-- 最大内存大小
- -XX:+HeapDumpOnOutOfMemoryError 使用改参数可以在内存溢出时导出这个堆信息
- -XX:+HeapDumpPath, 可以设置导出堆的存放路径
- -XX:+PrintGCDetails 打印GC垃圾回收信息
- 举例 ----- -Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test3.dump