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 areaGarbage Collection执行垃圾回收的重点区域

总结

  • YoungGenerationSpace是对象诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命
  • TenureGenerationSpace放置长生命周期的对象,通常都是从Survivor区筛选拷贝过来的Java实例对象
    • 特殊情况
      • 普通的对象优先尝试分配在TLABThread local allocation buffer
      • 对象较大JVM试图直接分配在Eden
      • 对象太大无法在Yong区找到足够长的连续空闲空间,JVM直接分配到Old区
  • MinorGC只发生在Young区,发生频率比MajorGC高很多,效率也比MajorGC10倍以上
  • GC发生在Old区则称为MajorGCFullGC

分代思想

  • 经研究,不同对象的生命周期不同,70%~99%的对象是临时对象
    • 新生代: 由Eden大小相同Survivors0区s1区from区to区.to区总是空的构成
    • 老年代: 存放新生代中经历多次GC仍存活的对象
  • 不分代完全可以

分代的唯一理由就是优化GC性能。如果没有分代,将所有的对象实例都放在一块,GC时寻找哪些对象实例没有被使用,此时需要对所有区域进行扫描,STWstop the world时间相对分代策略会变长。
很对对象实例都是朝生夕死
如果分代,把新创建的对象实例放到某一地方,GC时先把这块存储新创建对象实例的区域进行回收,STW时间相对不分代的扫描全部区域少很多。

JVM中的java实例对象可以被划分为两类

  • 生命周期较短的瞬时对象,这类对象创建和消亡都非常迅速
  • 生命周期非常长,在某些极端的情况下还能够与JVM的生命周期保持一致

现代垃圾收集器大部分都基于分代收集理论设计

  • Young == 新生区 == 新生代 == 年轻代
  • Old == 养老区 == 老年区 == 老年代
  • method area == 永久区 == 永久代
heap area 逻辑上分为三部分java7及之前java8及之后
YoungYong Generation Space 新生区 Young/New同左
Eden区同左
Survivor区只有一个存放数据,当jvm计算容量时只会考虑一个,所以Runtime.getRuntime().totalMemory()Runtime.getRuntime().maxMemory()的值会少一个survivor区的大小同左
Survivor from区同左
Survivor to区同左
OldTenure Generation Space 养老区 Old/Tenure同左
Method AreaPermanent Space 永久区 PermMeta Space 元空间 Meta

新生代

HotspotEden空间和另外两个Survivor空间默认占比是8:1:1 ;通过-XX:SurvivorRatio=8调整比例默认为8,但是实际比例是6:1:1 -XX:+UseAdaptiveSizePolicy默认开启了自适应内存分配策略,显式设置为8才会生效
几乎所有Java实例对象都是在Eden区被new出来
绝大部分Java实例对象的销毁都在新生代进行
IBM公司的专门研究表明,新生代80%的对象都是朝生夕死

可以使用选项-Xmn设置新生代最大内存大小。这个参数一般使用默认值就可以了

老年代

[todo]

对象分配内存过程

image

  • new的对象优先尝试放Eden区,Eden区可能已有对象
  • 如果Eden区剩余空间放得下,则直接在Eden区为对象分配内存
  • 如果Eden区剩余空间放不下,则触发Minor GCYGCYGC会将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 0Survivor 1区有数据时触发YGC。此时JVMEden区和Survivor 0此时Survivor 0区会称为From区内不再被其他对象所引用的对象进行销毁。此时的From区要根据实际情况来定,此处只是用Survivor 0区作为例子说明
      • 尝试将幸存的对象移动到Survivor 1此时也叫To
        • From区幸存对象阈值等于设置的值
          • 则执行Promotion晋升到老年代Old区,移动次数1
        • 当From区幸存对象阈值小于设置的值
          • To区放得下则放到To区,移动次数1
          • To区放不下直接放Old区,移动次数1此时Old区正常设置参数肯定放的下,因为Young空间一般都比Old空间小
    • 之后就是重复3步

注意:
Eden区满时才会触发YGCSurvivor 0Survivor 1区满不会触发YGC
YGC回收Eden区From区YGC后会清空Eden区和From
Survivor 0区和Survivor 1区大小1:1,肯定会有一个为空。为了使用复制算法,目的是解决碎片化问题
Survivor 0区和Survivor 1区:复制之后有交换,谁空谁时To
Garbage Collection频繁在Young区收集,很少在Old区收集,几乎不在PermMeta收集
对象可能直接分配在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区对象总大小进行MinorGC
  • Old区的连续空间大于历次晋升的平均大小进行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