JVM heap
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated分配.
The heap is created on virtual machine start-up. Heap storage存储 for objects is reclaimed回收的 by an automatic storage management system (known as a garbage collector); objects are never explicitly显示地 deallocated取消分配. The Java Virtual Machine assumes假定 no particular特别的 type of automatic storage management system, and the storage management technique技术 may be chosen according相应的 to the implementor’s实现者 system requirements系统需求. The heap may be of a fixed固定的 size or may be expanded扩展 as required by the computation计算 and may be contracted收缩 if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous连续的.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.
The following exceptional异常的 condition情况 is associated相关的 with the heap:
- If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an
OutOfMemoryError.
概述
- 一个
JVM进程只存在一个heap area,是java内存管理的核心区域 heap area在JVM启动时即被创建,其空间大小也随即确定heap area size是可以调节的,是JVM管理的最大一块内存空间jvm规范规定heap area可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的- 所有线程共享
heap area. 在这里还可以划分线程私有的缓冲区TLABThread local allocation buffer - 数组和对象实例可能永远不会存储在
jvm stack上,因为stack frame中保存指向对象或者数组在堆中的位置的引用 - 在方法结束后,
heap area中对象实例不会马上被移除,仅仅在垃圾收集的时候才会被移除 heap area是Garbage Collection执行垃圾回收的重点区域
总结
YoungGenerationSpace是对象诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命TenureGenerationSpace放置长生命周期的对象,通常都是从Survivor区筛选拷贝过来的Java实例对象- 特殊情况
- 普通的对象优先尝试分配在TLABThread local allocation buffer上
- 对象较大
JVM试图直接分配在Eden区 - 对象太大无法在
Yong区找到足够长的连续空闲空间,JVM直接分配到Old区
- 特殊情况
MinorGC只发生在Young区,发生频率比MajorGC高很多,效率也比MajorGC高10倍以上- 当
GC发生在Old区则称为MajorGC或FullGC
分代思想
- 经研究,不同对象的生命周期不同,70%~99%的对象是临时对象
- 新生代: 由
Eden、两块大小相同的Survivor区s0区、s1区或from区、to区.to区总是空的构成 - 老年代: 存放新生代中经历多次
GC仍存活的对象
- 新生代: 由
- 不分代完全可以
分代的唯一理由就是优化GC性能。如果没有分代,将所有的对象实例都放在一块,GC时寻找哪些对象实例没有被使用,此时需要对所有区域进行扫描,STWstop the world时间相对分代策略会变长。
很对对象实例都是朝生夕死。
如果分代,把新创建的对象实例放到某一地方,GC时先把这块存储新创建对象实例的区域进行回收,STW时间相对不分代的扫描全部区域少很多。
JVM中的java实例对象可以被划分为两类
- 生命周期较短的瞬时对象,这类对象创建和消亡都非常迅速
- 生命周期非常长,在某些极端的情况下还能够与
JVM的生命周期保持一致
现代垃圾收集器大部分都基于分代收集理论设计
Young== 新生区 == 新生代 == 年轻代Old== 养老区 == 老年区 == 老年代method area== 永久区 == 永久代
| heap area 逻辑上分为三部分 | java7及之前 | java8及之后 |
|---|---|---|
Young | Yong Generation Space 新生区 Young/New | 同左 |
Eden区 | 同左 | |
Survivor区只有一个存放数据,当jvm计算容量时只会考虑一个,所以Runtime.getRuntime().totalMemory()和Runtime.getRuntime().maxMemory()的值会少一个survivor区的大小 | 同左 | |
Survivor from区 | 同左 | |
Survivor to区 | 同左 | |
Old | Tenure Generation Space 养老区 Old/Tenure | 同左 |
Method Area | Permanent Space 永久区 Perm | Meta Space 元空间 Meta |
新生代
在Hotspot中Eden空间和另外两个Survivor空间默认占比是8:1:1 ;通过-XX:SurvivorRatio=8调整比例默认为8,但是实际比例是6:1:1 -XX:+UseAdaptiveSizePolicy默认开启了自适应内存分配策略,显式设置为8才会生效
几乎所有的Java实例对象都是在Eden区被new出来
绝大部分的Java实例对象的销毁都在新生代进行IBM公司的专门研究表明,新生代中80%的对象都是朝生夕死
可以使用选项-Xmn设置新生代最大内存大小。这个参数一般使用默认值就可以了
老年代
[todo]
对象分配内存过程
new的对象优先尝试放Eden区,Eden区可能已有对象- 如果
Eden区剩余空间放得下,则直接在Eden区为对象分配内存 - 如果
Eden区剩余空间放不下,则触发Minor GCYGC,YGC会将Eden区和From区清空- 将
Eden区和From区内的不再被其他对象所引用的对象进行销毁。 - 将
Eden区和From区内幸存的对象,移动到To区,并且标识移动次数加1次 - 此时
Eden区为空,再次判断Eden区是否放得下- 放得下则放
Eden区 - 放不下则尝试放
Old区一般是超大对象- 放得下,直接将对象放置到
Old区 - 放不下,则触发
Major GC,回收一次Old区,再进行判断Old区是否放的下- 放得下则直接将对象放置到
Old区 - 放不下则
OOM
- 放得下则直接将对象放置到
- 放得下,直接将对象放置到
- 放得下则放
- 将
Survivor 0区和Survivor 1区- 当
JVM进程第一次触发YGC。将Eden区内的不再被其他对象所引用的对象进行销毁。 - 将
Eden区所有幸存对象,尝试移到Survivor 0区会将Eden区清空- 当
Survivor 0区空间放得下则放在Survivor 0区,移动次数加1 - 幸存对象太大放不下,则直接晋升老年代,放到
Old区,移动次数加1
- 当
- 当
Eden区和Survivor 0或Survivor 1区有数据时触发YGC。此时JVM将Eden区和Survivor 0区此时Survivor 0区会称为From区内不再被其他对象所引用的对象进行销毁。此时的From区要根据实际情况来定,此处只是用Survivor 0区作为例子说明- 尝试将幸存的对象移动到
Survivor 1区此时也叫To区。- 当
From区幸存对象阈值等于设置的值- 则执行
Promotion晋升到老年代Old区,移动次数加1
- 则执行
- 当From区幸存对象阈值小于设置的值
To区放得下则放到To区,移动次数加1To区放不下直接放Old区,移动次数加1此时Old区正常设置参数肯定放的下,因为Young空间一般都比Old空间小
- 当
- 尝试将幸存的对象移动到
- 之后就是重复3步
- 当
注意:
当Eden区满时才会触发YGC,Survivor 0或Survivor 1区满不会触发YGCYGC回收Eden区和From区YGC后会清空Eden区和From区Survivor 0区和Survivor 1区大小1:1,肯定会有一个为空。为了使用复制算法,目的是解决碎片化问题Survivor 0区和Survivor 1区:复制之后有交换,谁空谁时To区Garbage Collection频繁在Young区收集,很少在Old区收集,几乎不在Perm和Meta收集
对象可能直接分配在Old区Eden区和To区满了,对象即使没达到阈值Promotion,也可能直接晋升到Old区
内存分配策略
对象晋升规则Promotion:
- 如果对象在
Eden出生并经过第一次MinorGC后仍存活,并且能被Survivor容纳,将被移动到Survivor空间,并将对象年龄设为1 - 对象在
Survivor区中每熬过一次MinorGC年龄就+1岁,当它的年龄增加到一定程度时默认15岁,每个JVM每个GC都有所不同,就会晋升Promotion到老年代
对象晋升PromotionOld的年龄阈值,可以通过-XX:MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则:
- 优先分配到
Eden - 大对象需要连续的内存空间,更高概率触发
GC直接分配到Old.尽量避免程序中出现过多的大对象 - 长期存活的对象分配到
Old经过多次YGC仍存活,达到阈值则晋升到老年代 - 动态对象年龄判断
- 如果
Survivor区中相同年龄的所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入Old无须达到MaxTenuringThreshold设置的年龄
- 如果
- 空间分配担保
-XX:HandlePromotionFailure
空间分配担保
在发生MinorGC之前,jvm会检查Old区最大可用的连续空间是否大于Young区所有对象的总空间
- 如果大于则此次
MinorGC是安全的 - 如果小于则
jvm会查看-XX:HandlePromotionFailure设置值是否运行担保失败- 如果
-xx:HandlePromotionFailure=true会继续检查Old区最大可用连续空间是否大于历次晋升Old区的对象的平均大小- 大于,则尝试进行一次
MinorGC,但这次MinorGC是有风险的 - 小于,则改为进行一次
MajorGC
- 大于,则尝试进行一次
- 如果
-xx:HandlePromotionFailure=false则改为进行一次MajorGC
- 如果
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到JVM的空间分配担保策略,虽然OpenJDK源码中仍定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它. 规则变更为
Old区的连续空间大于Young区对象总大小进行MinorGCOld区的连续空间大于历次晋升的平均大小进行MinorGC- 否则进行
MajorGC
heap area是分配对象存储位置的唯一选择
随着JIT编译器的发展与逃逸分析Escape Analysis技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙变化,所有对象都分配到heap area上也渐渐变得不那么绝对
在JVM中,对象是在heap area中分配内存,但是如果经过逃逸分析Escape Analysis后发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配, 这样无需在heap area上分配内存,也不需要GC,这也是常见的堆外存储技术。
例如:
基于OpenJDK深度定制的TaoBaoVM其中创新的GCIHGC invisible看不见的 heap技术实现off-heap.
将生命周期较长的实例对象从heap area中迁移到heap area外,并且GC不能管理GCIH内部的实例对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
Escape Analysis
逃逸分析技术不成熟历史
- 关于逃逸分析的论文在
1999年就已经发表,但直到JDK1.6才有实现,而且这项技术至今也不是十分成熟 - 原因是:无法保证逃逸分析的性能一定高于逃逸分析的消耗
- 虽然不成熟,但是他是
JIT编译器优化技术中一个十分重要的手段 - 有些观点认为,通过逃逸分析
JVM会在stack area上分配不会逃逸的对象,这在理论上是可行的,但是实际上取决于JVM设计者的选择。Oracle Hotspot JVM并未这么做,逃逸分析的相关官方文档已经说明,所以可以明确所有的对象实例都是创建在heap area上
- 目前很多书籍还是基于
JDK7以前的版本,JDK已经发生很大变化,intern字符串缓存和静态变量曾经都被分配在Permanent Space永久代上- 而
Permanent Space永久代已经被Meta Space元空间取代 - 但是
intern字符串缓存和静态变量并不是转移到Meta Space元空间,而是直接在heap area上分配,所以这一点同样符合:所有对象实例都是分配在heap area
概述
- 使用逃逸分析手段将本该分配到
heap area上的对象分配到stack - 有效减少Java程序中同步负载和内存
heap area分配压力的跨函数全局数据流分析算法 - 通过逃逸分析,
hotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到heap area上 - 逃逸分析的基本行为就是分析对象动态作用域
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
- 没有发生逃逸的对象,则可以分配到
stack上,随着方法执行的结束,该对象随stack frame一起被移除,不需要GC就释放内存 JDK6u23后,HotSpot中默认就已经开启了逃逸分析- 如果使用的是较早的版本
-XX:+DoEscapeAnalysis开启逃逸分析-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
结论: 开发中能使用局部变量的,就不要在方法外定义
代码优化:
- 栈上分配:将
heap area分配转化为stack分配,如果一个对象在子程序中被分配,使指向该对象的指针永远不会逃逸,对象可能是stack分配的候选,而不是heap分配 - 锁消除同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
- 标量替换:有的对象如果不需要作为一个连续的内存结构保存也可以被访问,那么对象的一部分或全部可以不存储在内存,而是存储在
CPU寄存器中
栈上分配
JIT编译器在编译期根据逃逸分析的结果发现如果一个对象并没有逃逸出方法,就可能被优化成栈上分配- 分配完成后,继续在该线程的
jvm stack内执行,最后线程结束,jvm stack被回收,局部变量对象也被回收,这样就不需要GC
锁消除
锁消除同步省略
线程同步的代价相当高,同步的后果是降低并发性和性能
动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程
如果没有被发布到其他线程,JIT编译器在编译这个同步块的时候就会取消这部分代码的同步,从而提高并发性和性能。这个过程就叫同步省略,也称为锁消除
标量替换
标量Scalar :一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量Scalar
聚合量Aggregate :可以分解的数据。java中的普通类对象就是聚合量Aggregate,因为可以分解成其他聚合量Aggregate和标量Scalar
JIT编译阶段,如果经过逃逸分析,一个对象不会被外界访问的话未发生逃逸,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
- 标量替换可以大大减少
heap area内存的占用,因为一旦不需要创建对象,那么就不再需要分配heap area内存 - 标量替换为栈上分配提供了很好的基础
-XX:+EliminateAllocations开启标量替换默认开启,允许将对象打散分配在jvm stack上