JVM 垃圾回收

Jvm 堆空间划分?

  • 新生代

    • Eden区
    • Survivor区
      • From Survivor
      • To Survivor
  • 老年代

堆空间划分是为了更好的回收内存和更快的分配内存。

jvm 堆空间是怎么工作的?

  • 新生成的对象首先进入 Eden区。当 Eden 区内存不够时,会进行一次 Minor GC。
  • 经过一次Minor GC,Eden 区和 From 区的存活对象全部被移动到 To 区。
  • 当某次 GC 时,对象的年龄大于某个阈值,这个对象会被移动到老年区。
  • 如果某次 GC 时,From 区不够用了,有一些打不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代。

堆内存的分配策略?

  • 对象优先分配在Eden区
  • 大对象直接进入老年代
  • 长期存活的对象将会进入老年代

为什么要大对象直接进入老年代?

大对象就是需要大量连续的内存空间的对象,例如字符串、数组。

为了避免大对象分配内存时,由于分配担保机制带来的复制而降低效率。

动态年龄对象年龄判定?

在经过每一次 GC 后,对象的年龄加1,Hotspot 会按照年龄计算每个年龄的累计大小,当某个年龄大小超过了 Survivor 区的 50% 时,取这个年龄和给定阈值年龄中较小的一个作为晋升老年代的年龄阈值。

按照区域收集划分的GC?

  • 部分收集:
    • 新生代(Minor GC / Young GC):只对新生代进行垃圾收集
    • 老年代(Major GC / Old GC):只对老年代进行垃圾收集。
    • 混合收集:(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  • 整堆收集:Full GC,收集整个堆和方法区。

空间分配担保?

空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。

担保规则,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

如何判断一个对象是否已经死亡?

  • 引用计数法:
    • 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
    • 引用计数发很难解决对象之间相互循环引用的问题,导致循环引用的对象无法被回收。
  • 可达性分析法:
    • 通过从 GC Roots 做为引用起点开始搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

可以作为 GC Roots 的对象?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

四种引用类型?

  • 强引用:以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机抛出 OutOfMemoryError 错误,使程序异常终止,也不会回收具有强引用的对象。
  • 软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
  • 弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
  • 虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。

不可达对象可能不会被立即回收掉,为什么?

对象被回收至少要经过两次标记过程,第一次标记是否需要执行对象的 finalize 方法。如果对象没有重写 finalize 方法,或者已经执行过了,这个对象会被直接回收。被判定为需要执行 finalize 方法的对象会放入一个队列中进行二次标记。

如何判断一个常量是废弃常量?

如果当前没有任何对象引用常量的话,就说明该常量就是废弃常量,如果这时发生Full GC 的话而且有必要的话,就会被系统清理出常量池了。

如何判断一个类是无用的类?

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的类加载器已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足上面三个条件的类,可以判定为无用的类。

垃圾收集算法有哪些?

  • 标记-清除算法

  • 标记-复制算法

  • 标记-整理算法

  • 分代收集算法

标记-清除算法?

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。

存在的问题:

  • 效率低
  • 空间碎片多

标记复制算法?

它将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法?

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法?

根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”“标记-整理”算法进行垃圾收集。

HotSpot 为什么要分为新生代和老年代?

为了方便使用分代收集算法。

常见的垃圾收集器?

  • Serial (串行)收集器:

    • 单线程,在垃圾收集时必须暂停其他所有线程工作。
    • 新生代采用标记-复制算法,老年代采用标记-整理算法。
  • ParNew 收集器:

    • 其实就是串行收集器的多线程版本,除了使用多线程回收垃圾外,其他与串行收集器一致。
    • 新生代采用标记-复制算法,老年代采用标记-整理算法。
  • Parallel Scavenge 收集器:

    • Parallel Scavenge 收集器也是使用的标记-复制算法的多线程收集器,看上去和ParNew 收集器收集器一样。
    • Parallel Scavenge 收集器更关注吞吐量,即用户代码运行的时间与CPU总消耗时间的比值。其他搜集器,例如CMS更关注用户线程的停顿时间。
    • 新生代采用标记-复制算法,老年代采用标记-整理算法。
    • JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能。
  • Serial Old 收集器:

    • 是串行收集器的老年代版本,也是单线程收集器。主要有两大用途:
      • JDK1.5之前与Parallel Scavenge收集器搭配使用。
      • 作为CMS收集器的后备方案。
  • Parallel Old 收集器:

    • Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法,注重吞吐量和CPU的场景,可以优先考虑Parallel Scavenge 和Parallel Old收集器。
  • CMS 收集器:

    • 并发标记清除收集器,是Hotspot上的第一款真正意义上的并发收集器,实现了垃圾收集线程和用户线程同时工作,它非常实在在注重用户体验的应用上使用。
    • 实现步骤:
    • 初始标记:stop the world,暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
    • 并发标记:与用户线程同时进行,耗时最长。从GC Root 出发,标记出所有可达对象。
    • 重新标记:stop the world,修正并发标记期间,因为用户线程更新引用域导致的产生变动的部分,这个阶段一般比初始标记耗时长,但是远远低于并发标记阶段。
    • 并发清除:与用户线程同时进行,同时GC线程对未标记的区域进行清除。
    • 有点:
      • 并发收集。
      • 停顿时间短。
    • 缺点:
      • 对CPU资源敏感,与用户线程同时进行,如果处理器核数少,GC线程占比会很高。
      • 无法处理浮动垃圾,GC线程并发清除的时候,会随之产生垃圾,这就是浮动垃圾。
      • 使用标记-清除算法,会有大量空间碎片产生。
  • G1 收集器:

    • 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间的要求,同时还具备高吞吐量。
    • G1 收集器的特点:
      • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
      • 空间整合:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
      • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
    • 实现步骤:
      • 初始标记
      • 并发标记
      • 最终标记
      • 筛选回收
    • G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容