垃圾回收与内存分配
一些基础
对象的四种引用类型
- 强引用,内存不足时报错oom,但不会该类对象
- 弱引用,当内存不足时才会回收
- 软引用,不管内存是否充足,在gc都会回收
- 虚引用,任何时候都可以被回收
怎么判断对象是否仍在使用?
- 引用计数法
每个对象有一个引用计数属性,当被引用时计数+1,当引用释放时-1。两个对象互相循环使用时会导致对象无法回收 - 可达性分析——Java默认
若从GC Roots向下搜索时,走过的路径称为引用链。当对象没有任何引用链时代表已不可用。
- 引用计数法
可作为GC Roots的对象有哪几类?
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法中引用的对象
方法区回收的必要条件
- 该类所有实例都已被回收
- 该类对应的类加载器已被回收(很难)
- 无法通过反射访问该类(该类对应的java.lang,Class没有被引用)
安全点和安全区域
安全点特定位置
- 循环的末尾
- 方法返回前
- 调用方法的Call之后
- 抛出异常的位置
安全区域
- 线程中断的方式
- 主动式:不对线程操作,简单的设置一个标记位,线程不断主动轮询这个标志位状态,为真时,线程在最近的安全点挂起
- 被动式:系统直接把所有线程中断,如发现有的线程不在安全点时,就恢复执行到最近的安全点(不采用)
并发情况下的可达性变动解决算法(G1和cms)
- 增量空间算法——CMS
当出现新的引用对象时,则记录下该对象,待扫描结束后再以此为根重新扫描一次 - 原始快照算法——G1
当出现删除引用时,将要删除的引用记录下来,待扫描结束后再将该对象为根重新扫描
- 增量空间算法——CMS
垃圾回收算法
标记-清除算法
标记需要回收的对象或不需要回收的对象,再统一清除。
缺点:- 效率不稳定,无论标记还是清除,效率都会随着对象增多而降低
- 内存空间碎片化
标记-复制算法
把内存空间分为两块,每次只使用其中一块,当该块内存不足后,把存活的对象复制到另一块,再把原空间一次性清除。
优点:没有内存碎片。
缺点:内存空间利用率低;当存活对象较多时,效率变低标记-整理算法
标记完成后,将存活的对象向一端移动,清除边界以外的内容
优点:内存规整,对象赋值/创建速度快
缺点:标记、清理效率不高分代收集
- 新生代,每次垃圾收集都有大量对象死去,选用标记-复制算法
- 老年代,对象存活率较高,没有其余区域可以进行分配担保,使用标记-清除、标记-整理算法
垃圾回收器
新生代
- Serial
串行,标记复制算法。客户端默认新生代收集器 - ParNew
并行,标记复制算法;只有ParNew和serial可以配合CMS使用 - Parallel Scavenge
并行,标记复制算法;吞吐量优先收集器,可控制吞吐量(用户代码运行时间/总时间)- 最大垃圾收集停顿时间参数[绝对值]
- 垃圾回收时间占总时间比率参数[相对值]
- 自适应调节空间参数[布尔值],开启后可自动调节Eden、Survivor区域大小
- Serial
老年代
- Serial Old
标记整理算法,主要供客户端模式,若用于服务端:与Parallel Scavenge搭配使用;或者作为CMS发生失败后的与预案 - Parallel Old
标记整理算法,Parallel Scavenge的老年版 - CMS
- Serial Old
CMS
- 四个步骤:
- 初始标记:STW,只标记GC Roots直接关联到的对象
- 并发标记:与用户线程一起遍历整个对象图
- 重新标记:STW,修整并发标记期间,因用户线程导致引用变化的部分(增量更新算法实现)
- 并发清除:与用户线程并行,清除已死亡的对象
- 三个缺点:
- 对处理器资源敏感,默认启动线程数(处理器核心数+3)/4
- 产生浮动垃圾。并发标记和清除阶段会与用户线程一起运行,所以需要预留一部分空间供用户线程使用,当空间不足时,会临时启用预案:使用serial old进行标记整理
- 标记清除算法导致内存碎片。大对象不够分配时,触发full gc耗时。可设置参数,多次清楚后,在下一次full gc前进行一次整理
- 四个步骤:
G1
- 四个步骤:
- 初始标记:在minor gc阶段只标记gc Roots引用的对象,所以不存在停顿
- 并发标记:与用户线程并发执行,标记完成后处理SATB(原始快照算法)记录在并发时有变动的对象
- 最终标记:STW(短暂),处理并发结束后仍遗留的少量SATB记录
- 筛选回收:STW,对各Region的回收价值与成本进行排序,根据期望停顿时间指定回收计划。将存活对象复制到空闲Region后,清除原Region
常见问题
- finalize() 方法什么时候被调用?它的目的是什么?
对象被释放前被垃圾回收器调用,目的是释放该对象所持有的资源(对外内存、长连接等),且该方法只会被调用一次。(不建议使用) - 为什么要有不同的引用类型?
由于gc回收时机不可控,且有时候需要适当的控制对象被回收的时机。比如新建 Person类,每次查询信息都需要重新构建实例,对象生命周期过短,引起巨大的消耗。听过软引用和HashMap的结合可以构建高速缓存,提升性能。