java垃圾收集与内存分配策略
垃圾收集(Garbage Collection GC)
在Java中垃圾回收主要是针对堆(Heap)的垃圾收集(方法区也有垃圾收集)。垃圾收集主要分为两步:
1、找出需要被回收的对象;
2、将需要被回收的对象进行内存回收。
找出需要被回收的对象(即不可能再被任何途径使用的对象),主要有两种算法:
1、引用计数算法
2、可达性分析算法
引用计数算法
给对象添加一个引用计数器,被引用一次就加1,引用失效就减1,为0时就是不可能再被使用的。
引用计数算法简单高效,但是它解决不了对象间的相互循环引用问题。
可达性分析算法
通过GC Roots对象作为起始点,开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java中,可作为GC Roots的对象包括一下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
finalize
当一个对象不可达时,它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果此对象被判定为有必要执行finalize()方法,此对象将会被放入F-Queue队列中,并由虚拟机创建的低优先级的Finalizer线程执行。finalize()方法只会被系统自动调用一次。
方法区Class的垃圾收集
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。其中回收常量和回收Java堆中的对象非常相似。
而类的回收必须满足一下三点:
- 该类所有的实例已经被回收
- 加载该类的ClassLoder已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
标记-清除(Mark-Sweep)算法
效率低,容易产生大量的碎片
复制算法
内存区域缩小了一半,一般用在新生代
标记-压缩算法
解决了碎片和内存浪费问题
HotSpot的算法实现
枚举根节点
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。
HotSpot是通过一组称为OopMap的数据结构实现枚举根节点的。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算处理,在JIT编译过程中,也会在特定的位置(安全点)记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
HotSpot是通过一组称为OopMap的数据结构实现枚举根节点的。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算处理,在JIT编译过程中,也会在特定的位置(安全点)记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
安全点(Safepoint)
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”。例如方法调用、循环跳转、异常跳转等明显具有指令序列复用的特征的指令。
让程序运行到最近的安全点上停顿下来有两种方案:
让程序运行到最近的安全点上停顿下来有两种方案:
- 抢先式中断(几乎没有虚拟机实现采用)
- 主动式中断(设值一个轮询标志,轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方)。
安全区域(Safe Region)
安全区域是指在一段代码片段之中,引用关系不会发生变化。线程执行到安全区域时,首先标识自己已经进入了安全区域,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。这样的线程必须在收到可以安全离开Safe Region的信号才可以继续执行。
垃圾收集器
HotSpot虚拟机的垃圾收集器
如果两个收集器之间有连线,表示它们可以搭配使用
Serial收集器
串行收集器,必须暂停所有其他的工作线程。client模式下的新生代收集器
ParNew收集器
多线程并行收集器,因为除了Serial收集器外,只有它能与CMS收集器配合工作
所以它是许多运行在server模式下的虚拟机中首选的新生代收集器。在单CPU的环境中ParNew收集器绝对不会有比Serial收集器更好的效果。
所以它是许多运行在server模式下的虚拟机中首选的新生代收集器。在单CPU的环境中ParNew收集器绝对不会有比Serial收集器更好的效果。
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代,采用复制算法,并行的多线程收集器,吞吐量优先的收集器。使用-XX:MaxGCPauseMillis和-XX:GCTimeRatio惨精确控制吞吐量。-XX:UseAdaptiveSizePolicy自适应的调整以提供最合适的停顿时间或者最大的吞吐量。
Serial Old收集器
Serial收集器的老年代版本。单线程,采用标记-压缩算法,主要在client模式下使用,在server模式下:1,在jdk1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;2,作为CMS收集器的后备预案。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和‘标记-压缩’算法。在注重吞吐量已经CPU资源敏感的场合,优先考虑Parallel Scavenge加Parallel Old收集器。
CMS收集器
以获取最短回收停顿时间为目标的,采用“标记-清除”算法的收集器。
整个过程分为4个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记,重新标记仍然需要“Stop The World”。
初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。
并发标记就是进行GC Roots Tracing的过程。
重新标记是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般比初始标记稍长,但远比并发标记的时间短。
优点:
并发收集,地停顿。缺点:
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾
- 产生碎片,所以虚拟机提供了参数来进行压缩-XX:+UseCMSCompactAtFullCollection开关参数和-XX:CMSFullGCsBeforeCompaction
G1(Garbage-First)收集器
将整个Java堆划分为多个大小相等的对立区域(Region),新生代和老年代不再是物理隔离的啦,它们都是一部分Region(不需要连续)的集合。
不计算维护Remembered Set的操作,G1可分为一下几步:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象。需停顿线程,耗时很短。
并发标记从GC Roots开始对堆中对象进行可达性分析,找出存货的对象,耗时较长,并发执行。
最终标记修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,需停顿线程,可并行。
筛选回收首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来定制回收计划,可并发。
优点:
- 并行与并发
- 分代收集
- 空间整合:从整体来看是基于“标记-压缩”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。
- 可预测的停顿
缺点:
吞吐量并不突出
各收集器搭配运行示意图
Serial - Serial Old搭配
ParNew-Serial Old搭配
Parallel Scavenge-Parallel Old搭配
Concurrent Mark Sweep收集器
G1收集器
内存分配与回收策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判定
- 空间分配担保