引用
- 狭义引用
- 地址
- 扩充引用
- 强引用 Strong Reference
- Object obj = new Object()
- 软引用 Soft Reference
- SoftReference,将要发生内存溢出才会回收
- 弱引用 Weak Reference
- WeakReference,不影响回收,可做回收通知
- 虚引用 Phantom Reference
- PhantomReference,不影响回收,可做回收通知
- 强引用 Strong Reference
对象判活
- 引用计数法
- 无法解决循环引用
- 可达性分析
- GC Roots
- 虚拟机栈中引用
- 类静态属性引用
- 方法区常量引用
- 本地方法栈中JNI引用
- 对象状态
- GC Roots可达
- GC Roots不可达但需执行finalize
- 不可达也不需执行finalize
- 执行finalize
- 进入F-Queue,由虚拟机建立的低优先级Finalizer线来执行,此执行表示触发,但不保证执行结束
- 若finalize方法卡死,会被咔嚓掉
- finalize方法中可进行一次自救,但只能救一次
- finalize方法据说是在初期对应C/C++的析构函数而做的一次折中,实际并不提倡使用,而用try-finally更好
- 枚举根节点
- 必须Stop The World,所以对效率很敏感,全局扫描是不现实的
- 得益于准确式GC,虚拟机知道某个地址存的是什么类型数据,以此建立OopMap(Ordinary Object Pointer)
- 虚拟机没有为每条指令都建立OopMap,因为成本太高
- 所以有了Safe Point的概念,“可长时间执行”的地方,会建立OopMap,即安全点
- 可长时间执行一般指指令序列复用,即方法调用,循环跳转,异常跳转
- 有安全点后,需要考虑怎么让线程都停在安全点
- 抢先式中断(Preemptive Suspension)
- 发生GC时虚拟机停止所有线程,恢复没有跑到安全点的线程,等其安全,几乎不用
- 主动式中断(Voluntary Suspension)
- 生成一个test轮询指令,在安全点处执行,看是否有中断标识存在,若存在就停下自己
- 具体方案是虚拟机将一个内存页设为不可读,test读取到这里时会产生自陷异常信号,并在异常处理器中进行线程中断
- 安全点的问题
- 线程处于sleep或者block时,等线程跑到安全点不现实
- 安全区域(Safe Region)
- 线程进入安全区域时,就在自己身上打个安全区域的标签,GC枚举根节点时,就不用管了
- 当线程将要离开安全区域时,需要检查系统是否已经完成根节点枚举或者GC
- 如果没有,就等待直到收到可以离开的信号为止
- GC Roots
方法区回收
- 虚拟机规范并未要求方法区回收
- 回收对象
- 废弃常量
- 不被引用,会被清出常量池
- 无用类
- 该类所有实例已回收
- 类加载器已被回收
- Class对象没有在任何地方被引用
- 仅仅是可回收,不一定就回收,HotSpot提供了参数可控制
- 废弃常量
收集算法
- 复制(对新生代分区后,涉及到分配担保Handle Promotion,需要老年代做担保)
- 标记-清除(弊端是空间碎片)
- 标记-整理
- 实际
- 分代收集
收集器
- 搭配关系
- 年轻代 - 年老代
- Serial(Serial Old,CMS)
- ParNew(Serial Old,CMS)
- Parallel Scavenge(Serial Old(实际应该是PS MarkSweep,只是它和Serial Old的实现太接近了,以至于很多资料中直接以Serial Old来代替),Parallel Old)
- 年老代 - 年轻代
- CMS(Serial,ParNew)
- Serial Old(Serial,ParNew,Parallel Scavenge)
- Parallel Old(Parallel Scavenge)
- G1
- 年轻代 - 年老代
- 说明
- Parallel Scavenge和G1采用全新代码框架,其余则共用架构
- 并行:GC多线程,用户线程需停止
- 并发:用户线程与GC线程同时执行,但可能是交替占用cpu,只是用户线程不需要暂停
- 新生代
- Serial(复制算法)
- 用户多线程-》GC单线程-》用户多线程
- 单Cpu下无敌,甚至2 Cpu下还能虐ParNew
- ParNew(复制算法)
- 用户多线程-》GC多线程-》用户多线程
- 除多线程外,与Serial基本一致,存在的一个主要原因是只有它能和CMS一起用,而Parallel Scavenge不行
- Parallel Scavenge(复制)
- 和ParNew有相似之处,不过更关注于可控的吞吐量
- MaxGCPauseMillis GC最大停顿时间
- GCTimeRatio 吞吐量
- UseAdapativeSizePolicy 自适应调节新生代大小,Eden比例,晋升老年代对象年等
- Serial(复制算法)
- 老年代
- Serial Old(标记-整理)
- 用户多线程-》GC单线程-》用户多线程
- 可作为CMS发生Concurrent Mode Failure时的备用方案
- Parallel Old(标记-整理)
- 用户多线程-》GC多线程-》用户多线程
- 在这个出来前,由于Parallel Scavenge只能和Serial Old一起使用,所以实际在吞吐量控制中,效果可能并不如ParNew + CMS
- CMS(Concurrent Mark Sweep,标记-清除)
- 用户多线程-》初始标记单线程-》用户多线程(并发标记)-》重新标记多线程-》用户多线程(并发清理)-》用户多线程(重置线程)
- 主打短停顿
- 初始标记(Initial Mark)
- 标记GC Roots能直接关联的对象;单线程
- 并发标记(Concurrent Mark)
- GC Roots Tracing
- 重新标记(Remark)
- 修正并发标记期间的改变,比初始标记稍长,远短于并发标记;多线程
- 并发清除(Concurrent Sweep)
- 初始标记 + 重新标记
- Stop The World
- CMS缺点
- 并发GC会消耗cpu,对用户线程造成影响
- 为此,曾经出现过Incremental Concurrent Mark Sweep,思想是让GC和用户线程交替运行,减少对cpu的占用
- 效果并不理想,已经被deprecate
- 无法处理浮动垃圾
- 浮动垃圾是指在并发清除阶段产生的垃圾,本次GC无法清除
- Concurrent Mode Failure
- 由于并发清除阶段需要运行用户线程,所以需要预留空间
- 预留空间太大容易导致GC频繁
- 通过-XX:CMSInitiatingOccupancyFraction调高的话,又可能导致Concurrent Mode Failure
- 发生Failure时,会启用Serial Old重新进行老年代收集
- 空间碎片
- 提供-XX:+UseCMSCompactAtFullCollection 默认开启
- -XX:CMSFullGCsBeforeCompaction 默认值0
- 并发GC会消耗cpu,对用户线程造成影响
- Serial Old(标记-整理)
- G1
- 1.7中出现,可处理整个堆
- 标记-整理
- 用户多线程-》初始标记单线程-》用户多线程(并发标记)-》最终标记多线程-》筛选回收多线程-》用户多线程
- 优势
- 可预测的停顿,用户可以控制M毫秒内,GC所用时间不能超过N毫秒
- 基于Region,即将整个Java堆划分为若干等大小的Region,新生代和老年代不再物理隔离,而都是一部分Region的集合
- G1跟踪各个Region垃圾堆积的价值大小(回收所获得空间大小和回收时间的经验值),在后台维护一个优先列表,每次根据允许的时间,收集价值最大的Region,这也是Garbage-First的名称由来
- 基于Region有个突出的问题(这个问题在新生代/老年代中也存在,只是没有这么突出),因为对象引用关系可能是跨Region的,如果没有对应措施的话,会导致全堆扫描,这是不能容忍的
- 虚拟机采用的方案是Remembered Set
- 每个Region都有一个对应的Remembered Set
- 虚拟机发现应用程序在对Reference的数据进行写操作时,会产生一个Write Barrier暂时中断写操作
- 检查Reference所引用的对象是否在不同的Region
- 若是,则通过Card Table在被引用对象所属Region的Remembered Set中记录该信息
- 枚举根节点时,扫描Remembered Set以确保扫描不遗漏
- 步骤(暂不考虑维护Remembered Set的工作)
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 将对象变化记录在Remembered Set Logs
- 最终标记(Final Marking)
- 将Remembered Set Logs合并到Remembered Set中
- 筛选回收(Live Data Counting and Evacuation)
- 先对Region的回收价值进行排序,然后根据用户的期望时间制定回收计划,这部分其实可以做到并发,只是不并发的效率更高,所以实际实现时并不是并发
G1和CMS对比
- 性能对比
- 软实时目标(M时间中最大允许GC为N)
- G1的失败概率小于CMS,并且失败情况下,G1的超时也小于CMS;此对比下,G1完胜
- 吞吐量对比
- CMS占优势
- 软实时目标(M时间中最大允许GC为N)
- 共同点:
- 都立足于低停顿(各种并发就看出来了)
- 选择
- 在1.7时,建议的还是CMS
- 不过呢,G1在低停顿已经有优势,只是吞吐量还不太好
内存分配策略
-
TLAB:Thread Local Allocation Buffer
- 在线程中划出缓冲区,用于对象新建
- 为什么要这个东西呢,减少分配竞争
对象优先在Eden中分配
大对象直接进入老年代,虚拟机提供参数-XX:PretenureSizeThreshold来控制,大于这个size的对象直接进入老年代,不过这参数只对Serial和ParNew有效
长期存活对象进入老年代,为每个对象赋予一个年龄值,达到一定年龄后进入老年代,可通过参数-XX:MaxTenuringThreshold来设置
动态对象年龄判定,Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等到-XX:MaxTenuringThreshold
空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代中连续空间是否大于新生代所有对象空间总和,如果条件成立,那么可以确保是安全的;如果不成立,则会查看HandlePromotionFailures设置值是否允许担保失败;如果允许,那么继续检查老年代可用连续空间是否大于历次晋升老年代的平均容量大小,如果大于,则尝试Minor GC;否则,或者HandlePromotionFailures设置不允许冒险,则进行Full GC
不过在JDK6 Update24之后,HandlePromotionFailures已经被抛弃了,新规则变为老年代连续空间大于新生代对象总大小,或者大于历次晋升平均大小,就进行Minor GC;否则,进行Full GC
担保失败时,会重新触发Full GC;虽然这比直接Full GC代价要大,但是从总体减少Full GC频率上,还是很有效的