深入理解Java虚拟机之垃圾收集器与内存分配策略

哪些内存需要回收?

    JVM的内存区域中,对于程序计数器、虚拟机栈、本地方法栈 线程私有的这 3 个区域,方法结束或者线程结束时,内存就随着被回收了,而方法区,运行时才知道创建哪些对象,这部分内存的创建和回收都是动态的。垃圾收集器 和 分配关注的是这部分内存。
    HotSpot虚拟机对于方法区的实现叫做 “永久代” ,方法区存放的是类相关的变量: 类信息、静态变量、常量;  永久代的垃圾收集主要回收的是 无用的类废弃常量,如果程序中没有引用常量的地方,自然就是 ”废弃常量“。
    而无用的类需要满足 3 个条件:
(1)该类的所有实例都已经被回收,即 Java 堆中不存在该类的实例。
(2)加载该类的 ClassLoader 已被回收
(3)该类对应的 Object.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    虚拟机可以对无用类进行回收,是 ”可以“ ,不是说必然会回收。
    在大量使用反射、动态代理、CGLIB 等频繁自定义 ClassLoader 的情景都需要虚拟机具备类卸载(类加载器了解一下,有提到卸载哦~) 的功能,以保证永久代不会溢出。
    上图是 虚拟机的内存区域规范,是个概念模型,下图是具体的虚拟机,比如 HotSpot 的实现,总体上分为 堆heap 和 非堆,为了提高 GC 效率,堆又分为新生代和老年代,根据不同区域对象生命周期的特点,选择适合的垃圾回收算法;方法区是由永久代实现的。
深入理解Java虚拟机之垃圾收集器与内存分配策略
深入理解Java虚拟机之垃圾收集器与内存分配策略

可达性算法

    判断对象是否存活
    通过 ”GC Roots“ 作为起始点,从这些节点开始向下搜素,搜索所走过的路径成为 引用链。当一个对象到 GC Root 没有任何引用链相连时,就是说,从 GC Roots 到这个对象是不可达的,会被 第一次标记,然后再看对象是否有必要执行 finalize() 方法,一个对象的 finalize() 最多只能被系统自动调用一次 ,如果该对象没有覆盖 finalize() 方法,或者已经被虚拟机调用过,就没有必要执行。如果判断为 有必要执行,就会把对象放在 F-Queue 队列中,会被第二次标记,稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程去触发这个方法。如果对象没有在这个方法中成功”拯救自己“,即重新与引用链上的对象建立关联,就要被回收。
    
    哪些可作为 GC Roots 对象:
(1)虚拟机中引用的对象。
(2)本地方法栈中引用的对象。
(3)方法区中类静态属性引用的对象。
(4)方法区中常量引用的对象。

内存分配与回收策略


    对象优先在 新生代的 Eden 上分配。
    大对象【需要大量 连续内存的 Java 对象,如字符串、数组】直接进入老年代。
    长期存活的对象将进入老年代。
    虚拟机给每个对象定义了一个对象年龄 (Age)计数器,如果对象在 Eden 出生,并且经过第一次 Minor GC后仍然存活,并且能被 Servivor 容纳,将被移动到 Servior 中,对象年龄置为1 ,对象在 Survivor 区中每 ”熬过“ 一次 Minor GC ,年龄就增加一岁,当年龄增加到一定程度,默认是 15岁, 就会晋升到老年代。再有,如果在 Survivor 空间中 相同年龄 所有对象的大小的总和 大于 Survivor 空间的一半,年龄大于 或者 等于该年龄的对象就会直接进入老年代。

    

空间分配担保

    在发生 Minor GC 之前,虚拟机会先检查 老年代 最大可用的连续空间 是否大于新生代 所有对象 空间,如果大于,那么 Minor GC 是安全的。如果不大于,会去看是否允许担保失败,允许的话,会检查 老年代 最大可用的连续空间 是否大于 历代晋升到 老年代对象的 容量的 平均大小,如果大于,尝试进行 Minor GC,尽管这是有风险的,不然是要 Full GC的。
    

垃圾回收算法有什么?

标记-清除算法:

    首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,会产生大量不连续的内存碎片。

标记-整理算法

    让所有存活的对象向一端移动,然后清除掉端边界以外的内存。

复制算法

    新生代分为 一块较大的 Eden 和 两块较小的 Survivor ,每次使用 Eden 和其中一个 Survivor ,回收时,将 Eden 和 Surivor (from)中还存活着的对象 一次性地复制到另一块 Survivor (to)中,然后清理掉 Eden 和 Surivor(from)空间。【 HotSpot 默认的大小比例是 8:1:1 ,这样只有 10% 的内存会被 ”浪费“ 】如果 Servivor(to)空间不够用,就需要依赖老年代进行分配担保。也就是把没地儿放的对象放到老年代中。

分代收集算法

    根据对象存活周期不同将内存划分为几块,一般把 Java 堆分为 新生代 和 老年代,根据不同年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时 都发现有大批对象需要被回收,只有少量存活,那就选用复制算法。而对于老年代,对象存活率高、没有额外空间对它进行分配担保,必须使用 标记-清除 或者 标记-整理算法。

有哪些垃圾收集器?

    在 新生代中有:
单线程版本(老年代也有)、
多线程版本、
可以控制吞吐量、自适应调节的(老年代也有,标记-整理算法)。

老年代中还有个 CMS 收集器:并发收集,停顿【枚举 GC Roots,分析对象的引用关系时,需要停顿所有的线程】时间短,标记-清除算法。

G1【Garbage-First】

    把整个 Java 堆划分成多个 大小相等的 独立区域(Region) ,新生代 和 老年代 就是 Region 的集合。G1 会跟踪每个区域中垃圾堆积的价值大小(回收后 能获得的空间大小 或者 回收所需时间的经验值 ),维护一个 优先列表,每次根据允许的回收时间,优先回收价值最大的区域。
    并发。
    可预测停顿时间。

什么时候Minor GC,什么时候Full GC?

     JDK 6之后,只要老年代的连续空间 大于新生代对象总大小 或者 历次从新生代晋升到老年代的对象容量平均大小 就会进行 Minor GC,否则将进行 Full GC。
    举例说 什么时候会触发 Full GC:当老年代使用了一个阈值时 空间时就会** CMS 收集器,它是并发的,GC时用户线程也在执行,可能会产生垃圾,如果 CMS 预留的内存无法满足程序的需要,就会触发 Full GC ,而且 CMS 是标记-清除算法,所以会有碎片,如果无法找到连续的 足够大的 内存分配对象,也要触发 Full GC 机制。
    

会看 GC日志吗?

    GC发生的时间,从虚拟机启动开始的秒数。
    垃圾收集的停顿类型。
    是否是 System.gc 触发的。
    哪个收集器,收集的区域是新生代?老年代?永久代?
    GC前的容量、GC后的容量。
    GC所用时间。
    

面试让讲一下GC?