1.对象相关
1. 判断对象是否存活方法
1.引用计数法:给对象加一个引用计数器,每当一个地方引用它,引用计数器加1,引用失效则减1,任何时刻计数器为0则对象无法被引用
存在问题,不能解决对象之间循环引用问题
2.可达性分析算法:通过设置GC Roots为起始节点,从节点向下搜索,搜索路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,该对象不可用
可作为GC Roots的对象:虚拟机栈引用的对象、方法区类静态属性引用的对象、方法区常量引用的对象、本地方法中JNI栈引用的对象
2.引用分类
强引用:存在强引用的对象不会被垃圾收集器回收
软引用:描述有用但非必需的对象。系统内存溢出异常之前,将软引用加入回收范围进行第二次回收。
弱引用:描述非必需对象。弱引用对象只能存活到下一次垃圾收集之前,垃圾收集时,不论内存是否足够,都会回收弱引用对象
虚引用:存在的唯一作用是当对象被回收时收到一个系统通知。
3.对象真正死亡需要两次标记:
第一次标记:当发现对象没有和GC Roots的引用链相连时,该对象会被标记,进行筛选,筛选条件为是否有必要执行finalize()方法
若对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,那么认为没有必要执行finalize()方法,进行回收 finalize()方法至多由GC执行一次
第二次标记:判定为有必要执行finalize()方法,对象被放置在F-Queue队列中,由一个自动建立、低优先级的Finalizer线程执行它
在finalize()方法中,如果对象建立了和引用链的关联,则自救成功
4.方法区(HotSpot中的永久代)垃圾收集 永久代垃圾收集分为两部分
废弃常量:没有对象引用常量池中的常量,这个常量叫做废弃常量
无用类:1.无该类实例
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
2.垃圾收集算法
1.标记-清除算法
标记出需要回收的对象,标记完成后统一进行回收。
问题:1.效率不高,标记和清除两个过程效率都不高
2.标记清除后,产生大量不连续内存碎片,内存碎片太多导致在分配大对象时,找不到足够的连续内存空间,这使得不得不再次触发一次垃圾收集
2.复制算法
将内存分为相等的两块,每次用其中一块,使用完毕后将存活对象复制到另一块上
3.标记-整理算法
让所有存活对象都向一端移动,然后清除掉端边界以外的内存
4.分代收集算法
根据对象存活周期不同将内存划分为几块。一般把堆分为新生代和老年代
在新生代中,少量对象存活,使用复制算法
在老年代中,对象存活率搞,没有额外空间进行担保分配,使用标记-整理或者标记-清理算法
3.HotSpot的算法实现
1.枚举根节点
以可达性分析为例
1.可作为GC Roots的节点主要在全局性引用(常量或类静态属性)和执行上下文(栈帧中的本地变量表)中,方法区很大时检查里面的引用很耗时。
2.可达性分析对时间的敏感还体现在GC停顿
可达性分析必须在确保一致性的快照中进行,一致性指的是不可以出现分析过程中对象引用关系还在不断变化的情况,所以导致GC进行时必须停顿 所有Java的执行线程(Stop the world)
3.在HotSpot中,使用OopMap的数据结构来得知哪些地方存放着对象引用
2.安全点
在OopMap的协助下,HotSpot可以快速完成GC Roots的枚举,但是OopMap的变化指令很多,为每一条指令生产成本对应OopMap的话需要大量额外存储空间
1.HopSpot没有为每一条指令都生成OopMap信息,只在特定的位置生成,这些特定的位置称为安全点
程序执行时不是再所以地方都能停止进行GC,只有在到达安全点的时候才可以
2.安全点的选择是以是否可以让程序长时间执行的特征为标准进行选定的
指令执行时间都很短,不太可能因为指令流长度太长这个原因而长时间运行,长时间运行最明显的特征是指令复用(方法调用、循环跳转、异常跳转)
3.如何让GC发生时让所有线程都到最近的安全点停顿下来
1.抢占式中断(几乎没有用到的)
GC发生时,将所有线程中断,如果发现有的线程中断位置不在安全点上,那么恢复这些线程,让它们跑到安全点上
2.主动式中断
不直接中断线程,设置一个标志,各线程轮询该标志,中断标志为真就挂起
轮询标志位置和安全点位置重合
3.安全区域
程序不执行的时候,即线程处于Sleep状态或者Blocked状态时,无法响应JVm的中断请求,安全区域解决此类问题
安全区域指的是在一段代码片段中,引用关系不会发生变化,该区域中任何位置都是GC安全的
线程执行到Safe Region中的代码时,首先标志自己进入Safe Region,当JVM发起GC时,就不用管标识为Safe Region的线程了
线程要离开Safe Region时,首先要检查是否完成了根节点枚举或GC是否已完成,若已完成则继续执行,否则等待安全离开的信号。
4.垃圾收集器
新生代收集器
1.Serial收集器
单线程收集器,进行垃圾收集工作时,必须暂停其他工作线程,直到它收集结束
优点:简单而高效(没有线程交互开销)
2.ParNew收集器
Serial的多线程版本
只有Serial和ParNew收集器可以和CMS收集器配合工作
并行:垃圾收集线程并行工作,用户线程依然处于等待状态
并发:用户线程和收集线程同时执行
2.Parallel Scavenge收集器(吞吐量优先收集器)
使用复制算法,多线程
目的是达到一个可控的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))
GC自适应调节(GC Ergonomics)
老年代收集器
1.Serial Old收集器
单线程收集器,使用标记-整理算法
主要意义是给Client模式下的虚拟机使用
2.Parallel Old收集器
多线程,标记-整理算法
3.CMS收集器
以获取最短停顿时间为目标。基于标记-清除算法
初始标记、并发标记、重新标记、并发清除
初始标记和重新标记需要Stop The World
初始标记:标记GC Roots能直接关联到的对象
并发标记:GC Roots Tracing
重新标记:修正并发标记中因为并发导致的引用错乱标记记录,时间比并发标记短
缺点:
1.CMS收集器对CPU资源敏感
不导致线程停顿,但是会占用一部分的线程(CPU资源),导致总吞吐量降低
CMS默认启动的回收线程数量是(CPU数量+3)/4,CPU数量少的时候,CMS对用户程序影响较大
增量式并发收集器:并发标记、清理时让用户线程和收集线程交替运行,减少GC线程独占资源时间
2.无法处理浮动垃圾
浮动垃圾为并发执行时随着程序运行产生的垃圾
3.基于标记-清除算法,回收结束会产生大量空间碎片
G1收集器
面向服务端应用,并行与并发、分代收集、空间整合、可预测的停顿
将java堆分为若干相等的独立区域,新生代和老年代不是物理隔离了,是Region的集合
初始标记:标记GC Roots能直接关联的对象,修改TSMS的值,让用户程序并发运行时,可以在正确可用的Region中创建新对象,需停顿线程
并发标记:进行可达性分析,GC收集线程和用户线程并发执行
最终标记:修正并发标记阶段用户程序继续运作而标记产生变动的那一部分记录
筛选回收:对Region的回收成本和回收价值进行排序,按照GC停顿时间进行回收
5.内存分配与回收策略
1.对象优先在Eden上分配
大部分情况下,对象在Eden中进行分配,Eden中内存不够时,进行一次Minor GC。
2.大对象直接进入老年代
需要大量连续内存空间的java对象叫做大对象,经常出现大对象容易导致内存还有很多空间就要提前进行一次垃圾收集
-XX:PretenureSizeThreshold参数(只对Serial和ParNew两款收集器有效),让大于这个值的对象直接在老年代分配,这样避免了在Eden和Survivor区之间大量的内存复制
3.长期存活得对象进入老年代
给每个对象定义一个对象年龄计数器(Age),每熬过一次Minor GC,Age加1,加到一定程度,会被移入老年代
通过参数-XX:MaxTenuringThreshold设置
4.动态对象年龄判断
在Survivor中相同年龄所有对象大小综合超过Survivor的一半,那么大于等于该年龄的对象直接进入老年代,无需等到MaxTenuringThreshold要求的年龄
5.控件分配担保
Minor GC开始前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,那么Minor GC可以确保是安全的。
如果不成立,查看HandlePromotionFailure设置值是否允许担保失败。如果允许,检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
如果大于,会进行一次Minor GC,如果小于,那么进行一次FULL GC。
新生代使用复制收集算法,如果在Minor GC后大量对象仍然存活,那么就需要老年代进行担保分配,将Survivor无法容纳的内存直接进入老年代