深入学习JVM底层(三):垃圾回收器与内存分配策略

垃圾回收器与内存分配策略

垃圾回收器

概念
  1. 上文讲到的:程序计数器、Java虚拟机栈、本地方法栈这3个区域都是随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每个栈帧分配多少内存基本上是在类结构确定下来的时候就已知的,因此这几个区域的内存分配和回收都具备确定性。在这几个区域内就不需要过多考虑回收的问题,方法结束或者线程结束时,内存自然就跟随着回收了
  2. Java堆区和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存

对象已死吗

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收

引用计数法

Java 堆 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1

  • 优点:

    引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。

  • 缺点:

    难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

可达性分析算法

可达性分析算法又叫根搜索算法,该算法的基本思想就是通过一系列称为「GC Roots」的对象作为起始点,从这些起始点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 对象之间没有任何引用链的时候(不可达),证明该对象是不可用的,于是就会被判定为可回收对象

在这里插入图片描述

在 Java 中可作为 GC Roots 的对象包含以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中 JNI(Native 方法)引用的对象。
引用

无论是通过引用计数器还是通过可达性分析来判断对象是否可以被回收都设计到「引用」的概念。在 Java 中,根据引用关系的强弱不一样,将引用类型划为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)

  • 强引用:Object obj = new Object()这种方式就是强引用,只要这种强引用存在,垃圾收集器就永远不会回收被引用的对象。

  • 软引用:用来描述一些有用但非必须的对象。在 OOM 之前垃圾收集器会把这些被软引用的对象列入回收范围进行二次回收。如果本次回收之后还是内存不足才会触发 OOM。在 Java 中使用 SoftReference 类来实现软引用。

  • 弱引用:同软引用一样也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 Java 中使用 WeakReference 类来实现。

  • 虚引用:是最弱的一种引用关系,一个对象是否有虚引用的存在完全不影响对象的生存时间,也无法通过虚引用来获取一个对象的实例。一个对象使用虚引用的唯一目的是为了在被垃圾收集器回收时收到一个系统通知。在 Java 中使用 PhantomReference 类来实现。

生存还是死亡

一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程

  • 第一次标记:如果对象在进行可达性分析后被判定为不可达对象,那么它将被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法。对象没有覆盖 finalize() 方法或者该对象的 finalize() 方法曾经被虚拟机调用过,则判定为没必要执行。

  • 第二次标记:如果被判定为有必要执行 finalize() 方法,那么这个对象会被放置到一个 F-Queue 队列中,并在稍后由虚拟机自动创建的、低优先级的 Finalizer 线程去执行该对象的 finalize() 方法。但是虚拟机并不承诺会等待该方法结束,这样做是因为,如果一个对象的 finalize() 方法比较耗时或者发生了死循环,就可能导致 F-Queue 队列中的其他对象永远处于等待状态,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,如果对象要在 finalize() 中挽救自己,只要重新与 GC Roots 引用链关联上就可以了。这样在第二次标记时它将被移除「即将回收」的集合,如果对象在这个时候还没有逃脱,那么它基本上就真的被回收了。

回收方法区

前面介绍过,方法区在 HotSpot 虚拟机中被划分为永久代。在 Java 虚拟机规范中没有要求方法区实现垃圾收集,而且方法区垃圾收集的性价比也很低。

方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类(废弃常量的回收和 Java 堆中对象的回收非常类似,这里就不做过多的解释了。)

无用的类的回收条件就比较苛刻了。要判定一个类是否可以被回收,要满足以下三个条件:

  1. 该类的所有实例已经被回收;
  2. 加载该类的 ClassLoader 已经被回收;
  3. 该类的 Class 对象没有被引用,无法再任何地方通过反射访问该类的方法。

垃圾回收算法

  • 标记-清除算法(Mark-Sweep)
  • 复制算法(Copying)
  • 标记-整理算法(Mark-Compact)
标记-清除算法

这是是最基础的收集算法,算法分为“标记”和“清除”两个阶段,首先标记处所有需要回收的对象,在标记完成后统一回收所被标记的对象,它的标记过程就是上边讲的对象的回收中的标记。

在这里插入图片描述

  • 标记和清除效率都不高
  • 标记清除后会产生大量内存碎片
复制算法

为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将其存活着的对象复制到另外一块上面,然后再把已使用过的内存一次清理掉。

在这里插入图片描述

  • 不会产生碎片
  • 运行效率高
  • 内存缩小了一半
标记-整理算法

上面的算法结合

标记-整理算法是介于【标记-清除】和【复制】之间的收集算法,标记过程任然与【标记-清除】算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

在这里插入图片描述

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现

下图展示了7种作用于不同分代的收集器:

  • 用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge
  • 回收老年代的收集器包括Serial Old、Parallel Old、CMS
  • 用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用

在这里插入图片描述

各种收集器概述:

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

内存分配策略

讲解

Java内存分配和回收的机制概括的说,就是分代分配,分代回收。对象根据存活的时间被分为:年轻代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation,也就是方法区,java8废弃)。

在这里插入图片描述

年轻代 GC (Minor GC)

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

年老代 GC (Major GC / Full GC)

对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。

分配策略
对象优先在 Eden 区分配

多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。

  • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
  • Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配

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

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代

空间分配担保

在新生代触发 Minor GC 后,如果 Survivor 中任然有大量的对象存活就需要老年队来进行分配担保,让 Survivor 区中无法容纳的对象直接进入到老年代

特点
  1. 对象优先在 Eden 分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值