JVM---GC
概述
/** * 【GC---概述】 * <什么是垃圾> * 在运行程序中 没有任何指针指向的 对象; * * <为什么要进行GC> * a, 内存消耗完 * b, 碎片整理 * 除了回收垃圾对象外,GC也可以清理内存的记录碎片,碎片整理将整理出的内存分配给新的对象; * c, 影响程序正常运行 * 随着应用程序业务越复杂,用户越来越多,没有GC就不能保证应用程序的正常运行; * 经常造成的STW的GC跟不上实际的需求,需要对GC进行优化; * * <早期的GC> * 在早期的C/C++时代,GC基本是是手工进行; * 开发人员可以使用new申请内存,delete释放内存; * * 特点: * 1、灵活控制内存 * 2、给开发人员带来 频繁申请和释放内存的管理负担 * 若有一处内存区由于开发人员忘记,就会产生内存泄漏,垃圾对象永远无法清除; * * <Java垃圾回收机制> * 自动内存管理,无需开发人员手动参与内存的分配与回收; * * 特点: * 1、降低内存泄漏、内存溢出的风险; * 2、开发人员从繁重的内存管理释放出来,专注于业务开发 * 3、弱化开发人员在程序出现异常时定位、解决问题的能力; * * 垃圾回收器 对堆(回收重点)、方法区进行回收; */
标记阶段
/** * 【GC---垃圾回收-标记阶段】 * * <标记阶段做什么?> * 堆中存放几乎所有的Java对象实例,在GC之前,首先需要判断内存中哪些是存活对象,哪些是死亡对象; * (只有被标记为死亡对象,GC在执行垃圾回收时,才会释放其占用的内存空间) * * <如何判断一个对象已经死亡?> * 当一个对象 不被任何其他对象引用; * * <标记阶段算法> * 1、引用计数算法 * 对每个对象保存一个整型的 引用计数器属性; * * 引用计数器: * 作用: * 用于记录对象被引用的情况; * * 对于一个对象A,只有任何一个对象引用A,则A的计数器+1; * 当引用失效时,A的计数器-1; * 当A的计数器值为0,表示A不再被使用; * * 优点: * 实现简单,垃圾对象易于标识; * * 缺点: * 需要单独的字段存储计数器,增加内存空间的开销; * 每次赋值都要更新计数器,增加了时间开销; * 无法处理循环引用的问题(导致Java的垃圾回收没有选择引用计数算法); * eg: * p -> A <-> B * rc=2 rc=1 * * 当P引用移除后,导致内存泄漏 * A <-> B * rc=1 rc=1 * * 2、可达性分析(根搜索、追踪性垃圾收集)算法 * * GC Roots: * 一组必须活跃的引用; * * Java中,GC Roots包括哪些元素: * 虚拟机栈内 * 参数,局部变量等; * 本地方法栈内 * static变量 * ... * * 引用链: * 可达性分析算法,内存中存活对象 会被 GC Roots直接或间接连接着,搜索所走过的路径称为 引用链; * * 基本思路: * 以GC Roots为起始点,按照从上至下的方式搜索 被GC Roots所连接的目标对象是否可达; * 如果目标对象没有任何 引用链 相连,则不可达,意味着目标对象死亡,可标记为垃圾; * * 注意: * 当使用 可达性分析算法 判断内存是否可回收时,分析工作必须在 一个能保障一致性的快照中 进行; * GC时 必须Stop The World就是这个原因; */
对象的finalization机制
/** * 【GC---对象的finalization机制】 * Java语言 提供了 对象终止(finalization)机制 允许开发人员 提供 对象被销毁之前的自定义处理逻辑; * * finalize(): * java.lang.Object#finalize() * * Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. 当某个对象为垃圾时,垃圾收集器会调用该对象的finalize(); * A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. 子类可以重写finalize() ,用来处理系统资源或执行其他清理; * * ***只会被 垃圾收集器 调用一次; * * 注意: * 永远不要显式调用某个对象的finalize(),应该交给 垃圾收集器去调用,理由: * 1、可能导致对象复活 * 2、执行时间没有保障,完全由GC线程决定; * 3、 * * 由于finalize()的存在,虚拟机中的对象一般处于3种可能状态: * 可触及: * 从根节点开始,可以到达这个对象; * 可复活: * 从根节点开始,所有引用都被释放,但是有可能在finalize()中复活; * 不可触及: * 对象的finalize()被调用 且 没有复活; * * 判断一个对象是否可回收,至少经历2次标记过程: * 1、如果对象A到GC Roots不可达,标记第一次; * 2、判断对象A是否有必要执行finalize(): * a, 如果对象A没有重写finalize() 或 finalize()已被调用,对象A 为不可触及; * b, 如果对象A重写finalize() 且 未被调用,对象A会被插入到 队列中,由低优先级的Finalizer线程 调用对象A的finalize(); * 如果调用finalize()后,对象A可达,则被移出 被回收 集合; */
public class FinalizeTest { static Object o; @Override protected void finalize() throws Throwable { System.out.println("sub finalize..."); o = this; } public static void main(String[] args) throws InterruptedException { o = new FinalizeTest(); o = null; System.gc(); Thread.sleep(10000); if (null == o){ System.out.println("o is dead"); } else { System.out.println("o is still alive"); } } }
使用jprofiler查看GC Roots、分析OOM
/** * 【GC---使用jprofiler查看GC Roots、分析OOM】 * jprofiler * Idea插件 * * dump文件 * what * 进程的内存镜像; */
清除阶段
/** * 【GC---垃圾回收-清除阶段】 * 当成功标记出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放死亡对象所占用的内存空间,以便有足够的可用空间为新对象分配内存; * * <清除阶段算法> * 1、标记-清除算法 Mark-Sweep * 实现思路: * 当堆中有效内存空间被耗尽时,会STW,然后进行2个操作: * 标记: * 垃圾回收器 从引用根节点开始遍历,标记 所有被引用的对象(在对象的Header中记录为可达对象); * 清除: * 垃圾回收器 对堆内存 从头到尾 进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则进行回收; * * 缺点: * 效率不高; * GC的时候,需要STW,导致用户体验很差; * 这种方式清理出来的内存是不连续的,容易产生碎片,需要维护一个空闲列表; * * 注意: * 何为清除? * 清除并不是真的置空,而是把需要清除的对象地址保存在空闲列表中; * 当有新对象需要加载时,判断垃圾位置空间是否够,如果够,就覆盖垃圾空间内容; * * 2、复制算法 Copying * 实现思路: * 将内存空间分为2块,每次只使用其中一块; * 在GC时,将 可达对象 复制到 另一个块内存中,然后清除正在使用的内存; * * 优点: * 没有标记、清除过程,实现简单,运行高效; * 复制后的内存空间连续,不会出现碎片问题; * * 缺点: * 需要2倍的内存空间; * 复制算法需要维护引用关系,时间开销也不小; * 适用于 可达对象比较少的场景; * * 场景: * 新生代的 Survivor区 * * * 3、标记-压缩(整理)算法 Mark-Compact * 实现思路: * 标记: * 垃圾回收器 从引用根节点开始遍历,标记 所有被引用的对象(在对象的Header中记录为可达对象); * 压缩: * 移动所有可达对象 且 按内存地址依次排列,然后,将末端内存地址以后的内存全部回收,无需使用空闲列表; * * 优点: * 解决 标记-清除算法 的碎片化问题; * 解决 复制算法 的内存减半的问题; * * 缺点: * 移动对象同时,需要维护引用关系; * 移动过程中,需要STW; * * 本质: * 标记-清除算法执行后,再进行一次内存碎片整理; * * 标记-清除 与 标记-压缩 的区别: * 标记-清除 是非移动式的回收算法; * 标记-压缩 是移动式的回收算法; */
标记-清除算法
复制算法
标记-压缩算法