07-堆
堆的核心概述
一个进程对应一个JVM实例,一个JVM实例中有一个运行时数据区,一个运行时数据区中只有一个堆空间
进程中的多个线程各自拥有一套程序计数器,本地方法栈,虚拟机栈,但是多个线程共享同一个堆空间
- 一个JVM实例只存在一个堆内存,堆是Java内存管理的核心区域
- 堆区在JVM启动的时候就被创建,其空间大小也确定了(JVM管理的最大一块内存区域,大小可调节)
- 堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的
- 多个线程共享一个堆区,容易造成并发性能差的问题,所以堆划分出线程私有的缓冲区,每个线程占一份
- 几乎所有的对象实例都在这里分配内存
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,指向对象或数组在堆中的位置
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收的时候会被移除(只是移除了方法区指向堆空间的索引,如果方法结束就移除堆中的对象的话,当方法多次调用时,会因为多次 GC而影响系统性能)
1 | public class SimpleHeap { |
堆内存结构概述和OOM
堆空间细分为
JDK 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
JDK 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
新生区<=>新生代<=>年轻代
养老区<=>老年区<=>老年代
永久代<=>永久区
堆空间大小的设置 -Xms -Xmx
-Xms
(默认:物理内存的1/64):表示堆空间(新生代+老年代)的起始内存
-Xmx
(默认:物理内存的1/4):则用于表示堆空间的最大内存
通常会将-Xms和-Xmx两个参数配置相同的值,目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提升性能
新生代与老年代
堆区可细分为新生代和老年代,新生代又可划分为Eden空间,Survivor1空间和Survivor2空间(from区,to区)
- 配置新生代和老年代在堆结构中的占比(一般不进行调整)
默认情况下 -XX:NewRatio=2
,表示新生代占1,老年代占2,即新生代占整个堆的1/3
- 配置年轻代中的Eden和Survivor区的比例
-XX:SurvivorRatio=8
,表示调整这个空间比例(Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1)
- 配置新生代的空间的大小:
-Xmn
,一般不设置
几乎所有的Java对象都是在Eden区被new出来的,大部分的Java对象的销毁都在新生代进行的
对象分配
一般过程
首先对象先存储到Eden区,当Eden区存满的时候,会触发Young GC(Minor GC),此时会把用户工作进程停止,称为STW(stop the world),然后判断Eden区里面哪些对象需要回收。(如图所示,红色为回收的对象)
然后将不需要回收的对象放到from区,并加上年龄计数器1
当from区存满的时候,如果对象还不需要回收,那么就会转入到to区,然后在年龄计数器上加上1,此时Eden里面不需要回收的对象也会存储在to区
此时,form区为空,from和 to区就互相转换
当年龄计数器上的值为阈值时(默认为15)对象就会转存到老年区
采用-XX:MaxTenuringThreshold
,参数可以设置对象在经过多少次GC后会被放入老年代
实际上时对 Eden区到 Survivor区过度的一种策略,是为了保证 Eden区到 Survivor区不会频繁的进行复制一直存活的对象且对Survivor区也能保证不会具有太多的一直占据的内存
关于垃圾回收,频繁在新生区进行收集,很少在养老区收集,几乎不在永久区 / 元空间收集
特殊过程
当存在一个超大对象,导致Eden区放不下的时候,该对象则会直接放到老年区
如果老年区放不下:
- 如果老年区本来的空间够放得下该对象,但是一部分被占用了。则进行Full GC,之后如果空间还是放不下,则直接OOM
- 如果老年区本来的空间就放不下该对象,直接返回OOM
GC
关于HotSpot VM的实现,GC按照回收区域可以分为部分收集和整堆收集
一、部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集,其中分为
- 新生代收集(Minor GC / Young GC):只是新生代(Eden/S0/S1)的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
二、整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
注意:
1、目前只有CMS GC会有单独收集老年代的行为
2、很多时候Major GC会和Full GC一起混合使用,需要具体分辨是老年代回收还是整堆回收
新生代GC(Minor GC)触发机制
当新生代空间不足时,就会触发,这里的新生代空间不足指的是Eden区已满
注意:
- Survivor满不会引发GC(每次Minor GC都会清理新生代的内存)
- Minor GC非常频繁,回收速度较快
- 会引发STW
老年代GC(Major GC / Full GC)触发机制
当发生在老年代的GC,对象从老年代消失时,我们说老年代GC触发了
注意:
- 当老年代空间不足时,会尝试先触发Minor GC。如果之后空间还不足,则触发Major GC
- Major GC的速度比Minor GC满10倍以上,STW时间更长
- Major GC后,如果内存还是不足,直接返回OOM
Full GC触发机制(*)
1、调用 System.gc()
时,系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
…………
堆空间分代的思想
分代的唯一目的就是优化GC的性能
如果没有分代,那么所有的对象都在一块,当要进行GC的时候,判断哪些对象需要回收,哪些不需要的时候,就需要对整个空间进行扫描。如果进行分代处理的话,就可以把新创建的一些对象,放在同一块区域,GC的时候就可以针对性地进行搜索,而且还可以腾出一大块区域
内存分配策略
- 优先分配到Eden区
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 动态对象的年龄判断:如果Survivor区中相同年龄的所有对象的和大于其空间的一般,年龄大于或等于该年龄的对象直接进入老年代
- 空间分配担保(*)
TLAB为对象分配内存
为什么要有TLAB?
1、对象的创建在JVM中很频繁,所以在并发环境下从堆区中划分内存空间是线程不安全的
2、为了避免多个线程操作同一个地址,需要加锁,从而影响分配的速度
什么是TLAB?
1、在Eden区域进行划分,为每个线程分配了一个私有缓存的区域
2、多个线程共同操作时,可以避免线程安全问题,提升内存分配吞吐量
注意:
1、JVM将TLAB作为内存分配的首选
2、TLAB只占 Eden的1%,当对象在TLAB空间分配内存失败后,JVM在Eden直接分配内存,而且通过加锁来确保原子性
3、TLAB大小可以通过 -XX:TLABWasteTargetPercent
+ 参数进行设置
总结堆空间参数设置
1、-XX:+PrintFlagsInitial
: 查看所有的参数的默认初始值
2、-XX:+PrintFlagsFinal
: 查看所有的参数的最终值(可能会存在修改(:表示修改了),不再是初始值)
3、具体查看某个参数的指令:
- jps:查看当前运行中的进程
- jinfo -flag SurvivorRatio 进程id
4、-Xms:初始堆空间内存 (默认为物理内存的1/64)
5、-Xmx:最大堆空间内存(默认为物理内存的1/4)
6、-Xmn:设置新生代的大小。(初始值及最大值)
7、-XX:NewRatio
:配置新生代与老年代在堆结构的占比
- 默认:-
XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3 - 可以修改
-XX:NewRatio=4
,表示新生代占1,老年代占4,新生代占整个堆的1/5
8、-XX:SurvivorRatio
:设置新生代中Eden和S0/S1空间的比例
(Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1)
9、-XX:MaxTenuringThreshold
设置新生代垃圾的最大年龄
10、-XX:+PrintGCDetails:输出详细的GC处理日志
(如下这两种方式是简单的打印 gc 简要信息:
1) -XX:+PrintGC
*2) -verbose:gc*
11、-XX:HandlePromotionFailure
:是否设置空间分配担保
(JDK6之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC)
逃逸分析
概述
将堆上的对象分配到栈,需要使用逃逸分析手段
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中
其实就是看new出来的对象是否有可能在方法外被调用
1 | public EscapeAnalysis object; |
在jdk7及之后,可以通过
1、-XX:+DoEscapeAnalysis
显式开启逃逸分析
2、通过 -XX:+PrintEscapeAnalysis
查看逃逸分析的筛选结果
代码优化
栈上分配
- 成员变量赋值
- 方法返回值
- 实例引用传递
在开启逃逸分析后执行时间变,而且没有发生GC
1 | //栈上分配测试 |
同步省略
也叫做锁消除,指在动态编译同步块时,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程,如果没有,那么在编译这个同步块的时候,就会取消这部分代码的同步,大大提高并发性和性能。
代码中堆obj进行加锁,但是obj对象的生命周期只在test1方法中,并不会被其他线程访问到,所以在JIT编译阶段就会优化成test2的
1 | public void test1() { |
分离对象或标量替换
标量:一个无法再分解成更小的数据的数据,如基本数据类型
聚合量:还可以继续分解的数据,如对象
如果一个对象不会被外界访问的话,经过JIT的优化,就会把对象拆解成若干个成员变量来替代,这就是标量替换
标量替换可以大大减少堆内存的占用,因为不需要创建对象,也就不需要分配堆内存
参数:-XX:+EliminateAllocations
开启标量替换,允许将对象打散分配在栈上(默认打开)
1 | public static void main(String[] args) { alloc();} |
1 | private static void alloc() { |