一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也被确定下来.是JVM管理的最大一块内存空间
堆的大小是可以进行调节的:Java虚拟机规范规定,堆可以处于物理不连续的内存空间,但是逻辑上它应该视为连续的.
所有的线程共享Java堆,在这里还可以划分出线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
“几乎”所有实例对象都会在堆上分配内存;
数组和对象可能永远不会存储在栈上,仅仅是在垃圾收集的时候才会被移除;
堆是GC执行垃圾回收的重点区域;
现代垃圾收集器大部分基于分代收集理论设计,堆空间在不同的JDK版本的细分如下图所示:
设置堆内存大小和OOM
指令 | 内容 |
---|---|
-Xms | 表示设置堆区的起始内存 |
-Xmx | 表示设置堆区的最大内存 |
一旦堆中的内存超过了”-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常;
通常会将-Xms和-Xmx两个参数配置相同的值,其目的在于:为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
默认情况下:
初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存 / 4
年轻代和老年代
存储在JVM中的java对象可以分为两类:
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另一类对象的生命周期非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
Java堆区进一步细分,可以分为年轻代(YoungGen)和老年代(OldGen),其中年轻代又可以划分为Eden空间,Survivor0空间和Survivor1空间(有时也被称作from区和to区),如下图所示:
配置新生代与老年代在堆结构中的占比:
默认情况下:-XX:NewRatio = 2,表示新生代占1,老年代占2,新生代占整个堆的1/3;
可以修改:利用-XX:NewRatio = n:表示新生代占1,老年代占n,新生代占老年代的1/n
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例为8:1:1
可以利用-XX:SurvivorRatio = n来调整这个空间比例
几乎所有的Java对象都是在Eden区被new出来的
绝大部分的Java对象的销毁是在新生代中进行的,数据表明,新生代中80%的对象都是”朝生夕死”的
可以使用”-Xmn”来设置新生代最大内存大小
图解对象的分配过程
对象分配过程步骤:
1.new的对象先放伊甸园区,此区有大小限制;
2.当伊甸园区的空间被填满以后,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(GC),将伊甸园区的不再被其他对象所引用的对象进行销毁.再加载新的对象到伊甸园区
3.然后将伊甸园中剩下的存活的对象移动到0区
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的如果还没有被回收,此时全部移动到幸存者1区
5.如果再次经历垃圾回收,此时重新放回幸存者0区,接着再去幸存者1区,如此反复
6.当幸存者区中的对象的年龄到达阈值(默认15),会在垃圾回收时进入老年代区
7.在老年代区中,当老年代区被填满时,会触发GC:MajorGC,进行老年代的垃圾回收
8.若老年代区执行;垃圾回收还是发现无法进行对象的保存,就会产生OOM异常
总结:
1.针对幸存者0区,1区来说:复制之后有交换,谁空谁是to
2.关于垃圾回收:频繁在年轻代收集,很少在老年代收集,机会不再永久代/元空间收集
Minor GC,Major GC与Full GC
JVM在进行GC的时候,并非每次都是对上面三个内存(新生代,老年代,方法区)一起进行回收的,大部分时候回收都是指的新生代(年轻代)
针对HotSpot VM的实现,它里面的GC按照回收区域又分为了两个大类型:一个是部分收集(Partial GC).一个是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为
年轻代收集(Minor GC/Young GC):只是对年轻代进行垃圾收集
老年代收集(Major GC/Old GC): 只是对老年代进行垃圾收集
混合收集(Mixed GC):收集真个新生代以及部分老年代的垃圾收集
PS:目前只有G1 GC会有此种功法!
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
年轻代GC触发机制
1.当年轻代空间不足时,就会触发Minor GC,这里的年轻代指的是Eden区满,Survivor区满不会触发GC
2.因为Java对象大多都是朝生夕死的特性,所以Minor GC会比较地频繁,一般回收速度比较快.
3.Minor GC会触发STW(Stop The World,即让整个用户线程停止运行的状态),暂停其他用户的线程,等到垃圾回收结束,用户线程才会恢复运行.
老年代GC触发机制
1.指的是在老年代的GC,对象从老年代中消失
2.出现Major GC,经常会伴随至少一次的Minor GC(但也不是绝对的)
也就是说,在老年代空间不足的时候,会首先考虑触发Minor GC,如果之后空间还是不足,再触发Major GC
3.Major GC的速度一般比Minor GC慢十倍以上,STW的时间也会更长
4.如果Minor GC之后,内存空间还是不足,则报OOM
Full GC触发机制
1.调用System.gc()时,系统建议执行Full GC,但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden区,Survivor0区向Survivor1区复制时,对象大小大于To Space可用大小,则将该对象转存老年代,且老年代的可用内存小于该对象大小
PS:Full GC是开发中尽量避免的,这样暂停的时间会短很多
堆空间的分代思想
为什么需要将Java堆进行分代处理?不分代就无法正常工作了?
经研究表明:不同对象的生命周期是不同的,其中70%~99%的对象时临时对象;
其中对堆进行划分:
年轻代:由Eden区,两块大小相同的Survivor区(to总为空)构成
老年代:存放年轻代中经历多次GC仍然存活的对象
其实,不分代程序能够正常运行的,分代的原因就是为了优化GC的性能,从而提高程序的运行性能.如果不进行分代操作,那么进行GC是会对堆区的所有区域进扫描,而很多对象其实都是“朝生夕死”的,当每次GC都对所有堆区的对象进行扫描回收,将提高GC的时间和降低其效率。但是我们如果分为新生代和老年代,其中新生代存放生命周期较短的对象,即“朝生夕死”或者刚新创建的一些对象,老年代存放生命周期比较长的对象,这样一来进行GC的时候,会先把生命周期较短的临时变量进行回收,不用扫描整个堆区,而是有针对性的进行扫描回收,这样可以节省很多时间和提高回收的效率,大大地提高了jvm的性能。
内存分配策略
如果对象在Eden区出生到经历第一次MInor GC后仍然存活,并且能被Survivor所容纳的话,将会被移动到Survivor空间中,并且设置age = 1,对象在Survivor区每熬过一次Minor GC,age就会自动加1,当它增加到一定的程度(default = 15)时,就会晋升老年代中;
老年代的年龄阈值设置:-XX:MaxTenuringThreshold来设置
分配策略:
1.优先分配到Eden
2.大对象直接分配到老年代中:尽量避免程序中出现过多的大对象(还”朝生夕死”)
3.长期存活的对象分配到老年代
4.动态对象年龄判断
如果Survivor区中相同年龄的对象 > Survivor空间的一半,年龄大于或者等于该对象可以直接进入老年代,无需等到达到阈值以后
为对象分配内存:TLAB
为什么会存在TLAB(Thread Local Allocation Buffer)?
1.堆区是所有线程的共享区域,任何线程都可以访问到堆区中的共享数据
2.由于对象实例的创建在JVM中非常地频繁,因此在并发环境下从堆区中划分出内存空间是线程不安全的
3.为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响了分配的速度
从而引出了TLAB:线程私有的缓冲区
什么是TLAB?
1.从内存模型而不是垃圾收集器的角度来说,对Eden区继续进行划分,JVM为每个线分配了一个私有缓存区域,它包含在Eden空间内
2.多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存的分配的吞吐量,因此我们可以将这种内存分配方式叫做快速分配策略
基于TLAB的对象分配过程图如下所示:
堆空间的参数设置
测试堆空间常用的jvm参数:
-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps:查看当前运行中的进程
jinfo -flag SurvivorRatio 进程id
javap -v -p Xxx.class
-Xms:初始堆空间内存 (默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX: NewRatio:配置新生代与老年代在堆结构的占比
-XX: SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX: MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX: +PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX: HandlePromotionFailure:是否设置空间分配担保
空间分配担保:
堆是分配对象存储的唯一选择吗?
逃逸分析概论:
注意:
1.判断是否逃逸分析:new的对象是否有可能在方法外被调用
2.开发中能够使用局部变量就不要轻易使用在方法外进行定义
逃逸分析之代码优化
其中:
1.栈上分配的场景:分别给成员变量赋值,方法返回值.,实例引用传递等等
2.同步消除——锁消除(如果一个对象被发现只能从一个线程中被访问到,那么对于这个对象来说,可以不考虑同步)
3.分离对象或标量替换(HotSpot使用)
逃逸分析小结: