垃圾收集器与内存分配策略
垃圾收集器主要回收的内存区域是堆和方法区
判断对象是否已死
- 引用计数算法
- 通过计算一个对象是否被其他对象所引用来判断该对象是否可以被回收,Java中不采用该方法,存在循环引用问题(a->b, b->a,此时a,b均不会被回收)
- 可达性分析算法
- 从一系列的
GC Root
出发,如果一个对象没有任何从引用链与GC Root相连接,则该对象可以被回收 - Java中的GC Root对象
- 虚拟机栈中本地变量表中引用的对象
- 本地方法栈中JNI(也就是Native方法)引用的对象
- 方法区中类静态变量引用的对象
- 方法区中常量引用的对象
- 从一系列的
- 引用类型
- 强引用,永远不会被回收
- 软引用,有用但不是必须的对象,在系统即将要发生内存溢出异常之前,会对其进行二次回收
- 弱引用,比软应用更弱,只能生存到下一次垃圾收集发生之前,垃圾收集器工作时,会回收
- 虚引用,最弱的一种引用,无法通过虚引用来取得一个对象
- 对象死亡过程
- 两次标记
- 如果对象在进行可达性分析之后,发现没有与GC Roots相连接的引用链,则会被第一次标记并且执行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,则将这两种情况都视为没有必要执行
- 如果对象有必要执行finalize方法,则对象会被放置在一个叫F-Queue的队列中,并且在稍后由一个虚拟机自行建立、低优先级的Finalizer线程去执行
- finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize方法中成功拯救自己,也就是重新与引用链上的任意对象建立关联,则在第二次标记时,它将被移出即将回收集合
- 无用类
- 该类的所有实例都被回收,也就是堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何地方呗引用,无法在任何地方通过反射访问该类
垃圾收集算法
- 标记-清除算法(Mark-Sweep)
- 分为两个阶段,标记、清除
- 缺点
- 标记以及清除过程的效率不高
- 标记清除之后会产生大量不连续的内存碎片,导致无法为大对象分配空间,从而导致触发另一次垃圾收集算法
- 复制算法(Copying)
- 将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块使用完之后,就将还存活着的对象复制到另一块内存,然后一次性清理已使用过的内存空间
- 缺点
- 导致每次可用内存大小缩小为原来的一半
- 在对象存活比较多时需要进行比较多的复制操作
- 现在商业虚拟机都采用这种收集算法来回收新生代(朝生夕死),将内存分为较大的Eden空间和两个较小的Survivor空间,每次使用Eden和其中一块Survicor空间,回收时,将存活对象复制到另一个Survicor空间,最后清理Eden和刚刚使用过的Survicor空间,HotSpot中默认Eden:Survivor = 8:1,也就是新生代中每次可用内存为90%,当Survivor空间不足时,需要老年代进行分配担保
- 当另一块Survivor空间不足以存放上一次新生代存活下来的对象时,通过分配担保机制直接进入老年代
- 标记-整理算法(Mark-Compact)
- 分为两个阶段,标记、整理,让存活的对象向一端移动,然后直接清除端边界以外的内存,主要用于老年代
- 分代收集算法(Generation Collection)
- 商业虚拟机主要采用方式,根据对象存活周期的不同,将内存划分为几块,一般把堆分为新生代和老年代,然后根据各个年代的特点,采用最适当的收集算法
- 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,就选用复制算法
- 老年代中,对象存活率高,没有额外的空间对它进行分配担保,一般采用标记清除或者标记整理算法
HotSpot的算法实现
- 程序在执行时,并非在所有的地方都能停顿下来开始GC,只有在到达安全点时才暂停
- 安全点的选定基本上是以程序”是否具有让程序长时间执行的特征”为标准进行选定的,长时间特征为,指令序列的复用,如方法调用,循环跳转,异常跳转等,只有具有这些功能的指令才会产生安全点
- 让线程在安全点上停顿的方法
- 抢先式中断
- GC发生时,把所有线程全部中断,如果发现线程中断的地方不在安全点上,就恢复线程,让其运行至安全点,几乎没有虚拟机采用这种方式
- 主动式中断
- 当GC需要中断线程时,仅仅简单地设置一个标志,各个线程在执行时主动地轮询该标志,发现中断标志为真时就自己中断挂起,轮询中断的地方和安全点是重合的
- 安全区域
- 在一段代码片段中,引用关系不会发生变化,这个区域中的任意地方开始GC都是安全的
- 线程执行到安全区域时,首先标志自己已经进入安全区域,此时,当JVM发起GC时,就不需要管将自己标志为安全区域的线程了,线程要离开安全区域时,先检查系统是否完成了根节点枚举(或者整个GC过程),如果完成,则继续离开,否则,等待到可以安全离开安全区域的信号为止
垃圾收集器
- Serial收集器
- 单线程收集器,在进行垃圾收集时,必须暂停所有的工作现场,直到收集结束
- 新生代采用复制算法,老年代采用标记-整理算法
- 虚拟机运行在Client模式下的默认新生代收集器
- 简单,高效
- ParNew收集器
- Serial收集器的多线程版本,使用多线程进行垃圾收集,其余基本同Serial
- 新生代采用复制算法,老年代采用标记-整理算法
- 运行在Server模式下首选的新生代收集器,除了Serial外,只有ParNew能与CMS收集器配合工作
- Parallel Scavenge收集器
- 并行的采用复制算法的新生代收集器
- 目标是达到一个可控制的吞吐量,吞吐量优先收集器
- Serial Old收集器
- Serial收集器的老年代版本,单线程,使用标记-整理算法
- 主要给Client模式下的虚拟机使用
- Parallel Old收集器
- 多线程,采用标记整理算法
- 注重吞吐量以及CPU资源敏感的场合
- CMS收集器
- CMS(Concurrent Mark Sweep)以获得最短回收停顿时间为目标
- 采用标记-清除算法
- 运行过程
- 初始标记,需要Stop The World,仅仅标记GC Roots能直接关联到的对象,速度快
- 并发标记,对GC Roots Trancing的过程
- 重新标记,需要Stop The World,修正并发标记期间因为用户程序停止运作而导致标记产生变动的那一部分标记记录,比初始化标记时间长,但比并发标记时间短
- 并发清除
- 并发标记以及并发清除过程可以与用户线程一起并发执行
- 缺点
- 对CPU资源非常敏感,一定数量的线程用户回收,从而使得用户线程数量比例降低
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生
- 浮动垃圾:在并发标记过程中,由于标记线程与用户线程共同运行,所以可能给还会产生新的垃圾,而这些垃圾在本次手机过程无法被回收
- 由于是基于标记-清除算法,会产生许多的内存碎片,当碎片过多时,会给大对象分配带来麻烦,从而触发一次Full GC,内存整理过程无法并发,所以会导致停顿时间变长
- G1收集器
- Garbage-First收集器,面向服务器端应用的垃圾收集器,主要同于替换CMS收集器
- 特点
- 并行与并发
- 充分利用多CPU,多核环境,使用多个CPU来缩短Stop-The-World停顿的时间
- 分代收集
- 空间整合
- 整体采用标记-整理算法实现,局部采用复制算法
- 可预测的停顿
- 除了降低停顿外,还能建立可预测的停顿时间模型
- 可以有计划地避免在整个Java堆中进行全区域的垃圾收集,跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间以及回收所需要的经验值),在后台维护一个优先队列,每次根据允许的收集时间,优先回收价值最大的Region,保证在有限时间内可以获得尽可能高的收集效率
- 内存布局
- 将整个Java堆划分为多个大小相等的独立区域Region,虽然保留新生代,老年代的概念,但是新生代,老年代不再是物理隔离,都是一部分Region的集合
内存分配
对象的内存分配,主要是在堆上进行分配,也有可能经过JIT编译之后被拆散为标量并间接在栈上分配,对象主要分配在新生代的Eden区,如果启动了本地线程分配缓存(LTAB),则按照线程优先在TLAB上分配,少数情况下也直接在老年代中分配
普遍的内存分配策略
- 对象优先在Eden分配,如果空间不够,将触发一次Minor GC
- 大对象直接进入老年区
- 长期存活的对象将直接进入老年代