侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

JVM 垃圾回收

林贤钦
2020-05-20 / 0 评论 / 15 点赞 / 634 阅读 / 0 字
温馨提示:
本文最后更新于 2020-05-25,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

JVM 垃圾回收

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

java 垃圾回收机制

  • 在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。

  • 在JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚

    拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将

    它们添加到要回收的集合中,进行回收。

垃圾回收三个问题(最后会解答)

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

1、回收死亡对象

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了

在判断对象已经可以回收有两种算法,1、引用计数法 , 2、可达性算法

在主流的jvm中,并没有采用引用计数法,而是采用可达性分析算法

1.1、引用计数法

给对象添加一个引用计数器,当有其他对象引用该对象时,计数器+1;当引用失效时,计数器-1。任何时刻计数器为零的对象就是不可能再被使用的。客观地说,引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。

但是,在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单 的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

也就是说对象A引用了对象B,而对象B同时也引用了对象A,此时就形成了循环引用。这样两个对象就永远都不会被回收。

1.2、可达性分析算法

这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

  • 简单来说

    找一些对象作为遍历的起始点(成为GC Roots),从这些起始点开始搜索,当某个对象并没有和任何GC Root产生关联,则认为这个对象已经不被使用了,可以清除

这里就有两个问题了,1. 什么对象能作为GC Roots? 2. 怎么引用?

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

  • 可作为GC Root的对象有以下几种:

    1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象

      譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。

    2. 在方法区中类静态属性引用的对象

      譬如Java类的引用类型静态变量。

    3. 在方法区中常量引用的对象

      譬如字符串常量池(String Table)里的引用。·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

    4. Java虚拟机内部的引用

      如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

    5. 所有被同步锁(synchronized关键字)持有的对象

    6. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

  • 关于引用

    在JDK1.2之前,“引用”的定义是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义非常的纯粹,非黑即白。在内存回收的时候也是,有用就留着,没用立刻删。但有一些场景,例如,当内存充足的时候,某块内存留着备用;内存不充足的时候,回收这块内存。这时纯粹的“引用”就没办法了。在JDK1.2之后,Java对引用进行了扩展,将引用分为强引用、软引用、弱引用、虚引用。

    无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。

    1. 强引用

      指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

    2. 软引用

      表示对象还有用,但不是必须的。内存不够用的时候,才会回收这些内存。软引用可用来实现内存敏感的高速缓存。

      在JDK 1.2版之后提供了SoftReference类来实现软引用。

    3. 弱引用

      表示对象还有用,但不是必须的,且不如软引用“硬”。只要发生垃圾回收,这些对象就会被回收。

      在JDK 1.2版之后提供了WeakReference类来实现弱引用。

    4. 虚引用

      最弱的引用,一个对象是否存在虚引用,并不影响其生存。为一个对象设置虚引用的唯一目的是在该对象被回收时,能够收到一个系统通知。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。

      在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。

在可达性分析算法中判定为不可达的对象,宣告一个对象死亡,至少要经历两次标记过程:

  1. 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记

  2. 此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法

    简单来说,对象如果重写了Object中的finalize() 方法,将有可能逃逸垃圾回收,因为它的优先级,所 以这个方法强烈不建议使用,官方明确声明为 不推荐使用的语法

2、回收方法区内内存

JDK1.8之前方法区是放到永久代中实现的(HotSpot虚拟机)。对于堆中的内存回收,尤其是新生代,回收率能够达到70%~95%;而永久代中的回收率则非常低。

也就是说,回收方法区内的内存性价比很低。但这块内存又不能没有垃圾回收机制,SUN公司就曾公布过关于方法区内存泄漏的严重BUG。

  • 方法区中垃圾回收主要关注两部分:废弃的常量和不再使用的类型

    1. 判定一个常量是否“废弃

      回收废弃的常量与回收java堆中的对象非常相似

      比如有个字符串曾经进入常量池中,但是当前系统没有引用这个字符串,且虚拟机中也没有其他地方引用这个字面量。这时发生内存回收,而且 垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。

    2. 判定一个类型是否属于“不再被使用的类”的条件

      • 该类所有的实例都已经被回收

        也就是Java堆中不存在该类及其任何派生子类的实例。

      • 加载该类的类加载器已经被回收

        这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。

      • 该类对应的java.lang.Class对象没有在任何地方被引用

        无法在任何地方通过反射访问该类的方 法

      Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是 和对象一样,没有引用了就必然会回收

3、垃圾回收算法

  1. 标记-清除:在标记完成后统一回收所有被标记的对象。它是最基础的收集算法

    两个明显的问题;1:效率问题和2:空间问题(标记清除后会产生大量不连续的碎片)

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

  3. 标记-整理:根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接

3.1、标记-清除

  • 算法分为“标记”和“清除”两个阶段:

    1. 首先标记出所有需要回 收的对象

      通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象

    2. 在标记完成后,统一回收掉所有被标记的对象

      也可以反过来,标记存活的对象,统一回 收所有未被标记的对象

适用场合

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)

缺点

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
  • 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

3.2、标记-复制

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉

现在的商业虚拟机都采用这种收集算法来回收新生代。

适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

缺点:

  • 需要一块儿空的内存空间
  • 需要复制移动对象

3.3、标记-整理

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。

这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

适用场合:

  • 存活对象少、垃圾对象多
  • 适用于年老代(即旧生代)

缺点:

  • 必须全程暂停用户应用 程序才能进行

4、分代垃圾回收

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的

    对象年龄加 1并且交换 from to

  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时

    间更长

相关VM 参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC

5、HotSpot的枚举GC Roots

5.1、枚举根节点

  • 以可达性分析中从GC Roots 节点找引用链这个操作为例,可作为GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在的很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

  • 另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行—这里的一致性的意思是指在整个分析期间整个执行系统看起来像被冻结在某个时间点上,不可以出现在分析过程中对象引用关系还在不断的变化,该点不满足的话分析结果的准确性就无法得到保证。

    这点导致GC进行时必须停顿所有Java执行线程(Sun称这件事情为“Stop The World”)的其中一个重要的原因,即使在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

  • 目前主流的Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏的检查完所有的执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存在着对象引用。

    在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

  • OopMap : 普通对象指针(Ordinary Object Pointer,OOP)

    在HotSpot中,虚拟机把对象内的什么偏移量上是什么类型的数据的信息存在到一个叫做“OopMap”的数据结构中。这样在计算引用链时直接查OopMap即可,不用到整个内存中去挨个找了,由此提高了分析速度。

5.2、安全点(Safepoint)

程序中的引用关系时时刻刻都在变化,如果每次变化都要记录到OopMap中,也是一项很大的负担。所以,只有在程序执行到了特定的位置,才会去记录到OopMap中。这个“特定的位置”,就叫安全点

如何保证在GC发生时,让所有的线程正好到达安全点?

有两种方式:

  • 抢先式中断(已经没人用了)

    抢先式中断的思路是,先把所有线程中断,如果有线程没有跑到安全点上,就恢复该线程,让它跑到安全点。

  • 主动式中断

    主动式中断的做法是,设置一个中断标志,这个标志和安全点是重合的。让各个线程去轮询这个标志,发现需要中断时,线程就自己中断挂起。

5.3、安全区域(Safe Region)

虽然安全点已经完美解决了如何保证在GC发生时,让所有的线程正好到达安全点的问题。

但是有一些情况下,线程失去了行为能力,比如线程处于sleep或者blocked状态。这个时候线程无法去响应JVM的中断请求,而JVM显然也不肯能一直等待某几个线程。该怎么办呢?

这种情况就需要“安全区域”来解决。

安全区域是指在一段代码片段中,引用关系不会发生变化,这个区域中任意地方开始GC都是安全的。

6、垃圾回收器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

6.1、常见的垃圾回收器有那些?

  • Serial收集器

    单线程收集器,会造成"Stop The Word"

    新生代采用复制算法,老年代采用标记 - 整理算法。

  • ParNew收集器

    就是Serial的多线程版本

    新生代采用复制算法,老年代采用标记 - 整理算法。

  • Parallel Scavenge收集器

    新生代收集器,使用了复制算法,同时是并行的收集器

    收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

  • Serial Old收集器

    Serial收集器的老年代版本

    种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

  • Parallel Old收集器

    Parallel的老年代版本,使用多线程和标记整理算法进行回收

    在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

  • CMS收集器(重点)

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用

    收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

  • G1收集器(重点)

    G1收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

    Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

  • ZGC收集器

    由Oracle公司研发的,JDK 11中新加入的具有实验性质[1]的低延迟垃圾收集器,希望在尽可能对吞吐量影响不太大的前提下,实现 在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

6.2、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种 “标记 - 清除” 算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  1. 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  2. 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  4. 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

CMS收集器是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。

但是它有下面三个明显的缺点

  • 对 CPU 资源敏感
  • 无法处理浮动垃圾;
  • 它使用的回收算法 -“标记 - 清除” 算法会导致收集结束时会有大量空间碎片产生。

6.3、G1收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。

它具备以下特点

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。

    部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。

  • 空间整合:与 CMS 的 “标记 – 清理” 算法不同,G1 从整体来看是基于 “标记整理” 算法实现的收集器;从局部上来看是基于 “复制” 算法实现的。

  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1收集器的停顿预测模型

​ 以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。

G1 收集器的运作大致分为以下几个步骤

  • 初始标记

    仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。

  • 并发标记

    从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。

  • 最终标记

    对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。

  • 筛选回收

    负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

7、内存分配与回收策略

Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。

在经典分代的设计下,新生对象通常会分配在新生代中,少数 情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。对象分配的规则并不是固定的, 《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪一种垃圾收集器,以及虚拟机中与内存相关的参数的设定。

7.1、 对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

7.2、大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这些设置值的对象直接在老年代中分配。这样做的目的是在避免Eden区及两个Survivor区之间发生大量的内存拷贝

7.3、长期存活的对象将进入老年代

​ 虚拟机既然采用分代收集的思想来管理内存,那么内存回收时就必须能够识别哪些对象应当放在新生代,哪些对象应该放在老年代。

​ 为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1.对象Survivor区每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁)时,就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold来设置。

7.4、动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代

如果在Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

7.5、空间分配担保

在发生Minor GC时,虚拟机就会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则改为直接进行一次Full GC.如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC:如果不允许,则也要改为进行一次Full GC.

8、常见问题

8.1、GC的主要任务:

  1. 分配内存
  2. 确保被引用对象的内存不被错误的回收
  3. 回收不再被引用的对象的内存空间

8.2、垃圾回收机制的主要解决问题

  1. 哪些内存需要回收

    堆中“死”对象和方法区内内存

    通过可达性算法把一系列“GC Roots”作为起始点,从节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。

  2. 什么时候回收

    即使是被判断不可达的对象,也要再进行筛选,当对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过,则没有必要执行;如果有必要执行——放置在F-Queue的队列中——Finalizer线程执行。

    注意:对象可以在被GC时可以自我拯救(this),机会只有一次,因为任何一个对象的finalize() 方法都只会被系统自动调用一次。并不建议使用,应该避免。使用try_finaly或者其他方式。

  3. 如何回收

    1、新建的对象,大部分存储在Eden中
    2、当Eden内存不够,就进行Minor GC释放掉不活跃对象;然后将部分活跃对象复制到Survivor中(如Survivor1),同时清空Eden区
    3、当Eden区再次满了,将Survivor1中不能清空的对象存放到另一个Survivor中(如Survivor2),同时将Eden区中的不能清空的对象,复制到Survivor1,同时清空Eden区
    4、重复多次(默认15次):Survivor中没有被清理的对象就会复制到老年区(Old)
    5、当Old达到一定比例,则会触发Major GC释放老年代
    6、当Old区满了,则触发一个一次完整的垃圾回收(Full GC)
    7、如果内存还是不够,JVM会抛出内存不足,发生oom,内存泄漏。

8.3、在Java中可作为GCRoots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI引用的对象。

8.4、Minor GC和Full GC的区别

  • 新生代Minor GC:指发生在新生代的垃圾收集动作,因为Java对象大多数都具有朝生夕灭的特性,多以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常都伴随着至少一次的Minor GC(但并非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。MajorGC的速度一般会比Minor GC慢10倍以上。

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

因为对象的新生代和老年代的生存时间不同,所以使用分代来对不同的对象进行分类,比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的所以我们可以选择“标记-清理”或“标记-整理”算法进行垃圾收集

8.6、如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

不会。对象回收需要一个过程,这个过程中对象还能复活。而且垃圾回收具有不确定性,指不定什么时候开始回收。

15

评论区