Note of «深入理解Java虚拟机»

走进JAVA

Java内存管理

各种溢出

垃圾回收&内存分配

判断对象是否存活的方法

  1. 引用计数法,给每个对象添加引用计数器,实现简单,效率高,但无法很难处理循环引用,所以java没有采用 TODO 有对象的列表?
  2. 根搜索法,从一系列名为“GC Roots”的对象作为起始点,遍历引用链,判断对象是否可达 TODO 起始点如何确定?

回收方法区,或者HotSpot虚拟机中的永久代,回收效率较新生代低,其主要回收:

  1. 废弃常量
  2. 无用的类

垃圾收集算法

  1. 标记-清除算法,先标记所有要回收的对象,之后统一清除,效率低且产生大量内存碎片

  2. 复制算法,最原始的做法是将内存分为相等的两部分,只使用其中的一部分,当用完后,将存活的对象拷贝的另一块,然后清理使用过的内存。 研究发现,新生代中的对象98%是短命的,所以不用对等分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一个Survivor,当回收时,将存活的对象拷贝到另外一个survivor上即可。HotSpot上默认Eden:survivor=8:1,也就是每次新生代中可用内存为90%(80%+10%),当存活对象多余10%的空间时,需要依赖其他内存,如老年代,进行分配担保。

  3. 标记-整理算法,和‘标记-清除’相似,只是后续步骤不是清除死亡对象,而是,将存活对象向一端移动,compact空间

  4. 分代收集,根据对象的存活周期不同,将内存分为几块,一般,将Java堆分为新生代(Young Generation)和老年代(Tenured Generation),分别使用不同的收集算法,如新生代对象存活率低,使用复制算法,老年代中对象存活率高且没有额外空间,使用标记-清理或标记-整理算法

垃圾收集器,垃圾收集算法的具体实现。

  1. Serial收集器,采用复制算法,‘单线程’,只有单个回收线程,且需要暂定所有工作线程,是虚拟机在Client模式下的默认新生代收集器。
  2. ParNew收集器,采用复制算法,Serial收集器的多线程版本,仅仅是多个回收线程,是很多虚拟机在Server模式下的首选新生代收集器。
  3. Parallel Scavenge收集器
  4. Serial Old收集器,Serial收集器的老年代版本,使用标记-整理算法,也是主要用于client模式
  5. Parallel Old收集器,Parallel Scavenge收集器的老年代版本,使用标记-整理算法
  6. CMS收集器(Concurrent Mark Sweep),以获取最短回收停顿时间为目标,使用与Server模式,基于标记-清除算法实现
  7. G1收集器,最牛逼的,基于‘标记-整理’算法,且能精确控制停顿。这样是G1能够在基本不牺牲吞吐量的前提下,完成低停顿内存回收。其主要思路是极力避免全区域垃圾回收,而是将Java堆,包括新生代和老年代,划分为多个大小固定的独立区域,并跟踪这些区域中的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的垃圾回收时间,优先回收垃圾最多的区域。区域划分优先级回收保证了G1在有限时间内获取最高的回收效率。

内存分配和回收策略

  1. 对象优先在Eden上分配
  2. 大对象(需要大量连续内存空间,如数组)直接进入老年代。如果大量大对象放在eden上,会导致内存频繁回收,Eden和两个survivor间大量拷贝,浪费时间,所以尽量避免大量短命大对象
  3. 长期存活的对象进入老年代,虚拟机给每个对象定义一个对象年龄计数器,每进入一次survivor年龄加1,一般到15岁左右转到老年代
  4. 动态对象年龄判定,即不一定要高于某个阈值才升级为老年代,而是相同年龄的所有对象空间之和大于survivor空间一半,则大于等于该年龄的所有对象
  5. 空间分配担保,一般使用老年代进行担保,但是可能会是失败,因为老年代空间也肯能不足,失败后就进行一次Full GC,当然担保前可以通过以往每次晋升到老年代的对象数量预估本次的,如果大于老年代现有的空余空间,则进行依稀FullGC,如果小于,根据是否允许失败,进行拷贝或进行Full GC

Misc

  1. 老年代除了方法区是,还有那些?还是可以主动划分区域为老年代?
TOP