垃圾收集器与内存分配

垃圾收集(Garbage Collection,GC),它需要完成的三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

其中,垃圾收集器关注的是Java堆和方法区的内存如和管理。

1.对象的存活和死亡

垃圾收集器在对堆进行回收前,第一件事就是确定这些对象之中哪些存活,哪些死去(死去就代表对象不可能再被任何途径使用)

1.1 判断对象是否可用的算法

引用计数算法

  • 描述:
    • 对象中添加一个引用计数器
    • 当有一个地方引用这个对象时,计数器+1
    • 当引用失效时,计数器-1
    • 计数器的值归零时就代表此对象不可能再被使用,即死亡
  • 弊端:
    • 存在很对例外情况,需要大量的额外处理次啊能保证正确工作
      • 例如,很难解决循环引用的问题:即 objA.instance = objB; objB.instance = objA;,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。

可达性分析法(主流)

  • 描述:
    • 有一系列称为"GC Root"的根对象
    • 从GC Root开始,根据引用关系向下搜索,走过的路径称为"引用链"(Reference Chain)
    • 从GC Root到某个对象不可达时,此对象不可能再被使用,即死亡。
  • Java中可作GC Root的对象:
    • 虚拟机栈中
      • 栈帧中的本地变量表引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
    • 本地方法栈中
      • JNI 引用的对象(native方法
    • 方法区中:
      • 类的静态属性引用的对象,如Java类的引用类型静态变量
      • 常量引用的对象,如字符串常量池中的引用
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器。
    • 所有被同步锁持有的对象
    • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存
    • 还可以有其他对象临时性的加入

1.2 可达性分析后,对象就被回收了吗?

即使在可达性分析算法中判定为不可达的对象,不会立即死去,至少要经两次标记过程

  1. 当发现对象不可达后,该对象被第一次标记
  2. 第一次标记之后进行判断:是否有必要执行finalize()方法
    • 不执行:对象没有覆盖finalize()或finalize() 方法已被执行过(finalize() 只被执行一次)
    • 否则就执行该方法
  3. 执行finalize()方法:
    • 过程:该对象被放置在F-Queue队列中,稍后由一个虚拟机自动创建的、低调度优先级的Finalizer线程去执行这个队列中每个对象的finalize()方法。
    • 执行finalize()方法时,只会触发开始,并不保证结束。
    • 特别需要注意的是,finalize() 只被执行一次,如果对象已经通过finalize() 方法逃脱过,下一次就不会执行finalize()方法,而是直接第二次标记。
    • finalize() 方法能做的,try-finally 都能做,而且异常机制更加优秀,所以忘了finalize()方法吧。
  4. 在执行(不执行)finalize()方法之后,收集器会对队列中的对象进行第二次标记,标记过后就会回收对象。
    • 如果对象要在逃脱死亡命运,就需要在finalize()方法中重新与引用链上任何一个对象建立关联。

1.3 回收方法区

方法区主要回收废弃的常量和不再使用的类型。回收的条件如下:

  • 废弃常量:和回收堆中的对象很相似。例如一个字符串 “abc”,已经没有任何一个字符串对象的值是"abc",也就是说当没有任何引用指向 “abc” 时,它就是废弃常量了。
  • 无用的类:
    • 该类的所有实例已被回收,Java 堆中不存在该类的任何实例
    • 加载该类的类加载器( Classloader )已被回收
    • 该类的 Class 对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法

特别注意的是,当回收类时,满足以上条件,仅仅是“被允许”回收。关于是否要回收,HotSpot虚拟机提供了-Xnoclassgc参数控制。

频繁自定义类的场景中(大量使用反射、动态代理、CGLib等字节码框架…),通常需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力

1.4四种引用类型

JDK1.2之后,才有了后三种引用的实现

  • 强引用:像 Object obj = new Object() 这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象
  • 软引用:用来描述一些还有用,但非必须的对象。在 OOM 前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就 OOM。SoftReference类实现软引用。
  • 弱引用:被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:WeakReference。
  • 虚引用:也被成为“幽灵引用”或者“幻影引用”,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:PhantomReference。

2.垃圾的收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类。也称为“直接垃圾收集”和“间接垃圾收集”。主要介绍主流的“追踪式垃圾收集”。

2.1 分代收集理论

分代收集理论建立在两个分代假说上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡

而在解决跨代引用问题时,就滋生出了第三条假说

  1. 跨代引用假说:跨代引用相对于同代引用来说占极少数

依据前两个假说,就奠定了多数收集器一致的设计原则:收集器应当将Java堆划分出不同的区域,然后将回收对象依据年龄(熬过垃圾收集过程的次数)分配到不同的区域。

老年代和新生代

  • 新生代:存放的大多数对象都是朝生夕灭的,难以熬过垃圾收集过程。每次回收时只关注如何保留少量存活。
  • 老年代:存放难以消亡的对象,虚拟机使用较低的频率来回收这个区域

新生代中,每次垃圾收集时就会又大批对象死去,而每次回收后存活少量对象,并且会逐步晋升到老年代中存放。

跨代引用和记忆集

  • 跨代引用:新生代的对象很有可能被老年代所引用,为了找出这种情况,不得不遍历整个老年代种的对象进行可达性分析,为解决这个问题,就产生了“记忆集”。

  • 记忆集:依据第三条假说,在解决跨代引用时,在新生代创建一个全局数据结构(称为“记忆集”),这个结构划分了老年代,标识出老年代中哪一部分存在跨代引用。当发生Minor GC收集时,只有包含跨代引用的部分被加入到GC Roots进行扫描。

不同分代的收集

  • 部分收集(Partial GC):指目标不是收集整个Java堆,其中分为
    • 新生代收集(Minor GC / Young GC):目标只是新生代
    • 老年代收集(Major GC / Old GC):目标只是老年代(注意:Major GC的说法有些混淆,注意不同资料上的不同描述)
    • 混合收集(Mixed GC):目标是整个新生代和部分老年代。目前只有G1收集器会有。
  • 整堆收集(Full GC):收集整个Java堆和方法区

2.2 标记-清除算法–基础算法

  • 描述:
    • 首先标记出所有需要回收的对象
    • 随后统一回收所有被标记对象
    • 也可以标记留存对象,回收非标记对象
  • 缺点:
    • 执行效率不稳定:标记和清理两个过程的效率都随对象而变化
    • 碎片化问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC
      2021221

2.3 标记-复制算法–解决效率问题

  • 描述:

    • 半区复制:将可用内存分为大小相等的两块,每次只使用其中一般内存
    • 当使用完一块内存时,把存活对象复制到另一块内存,并且对整个半区进行收集
    • 常用在新生代
  • 不足: 可用内存缩小为原来的一半,空间浪费太大

  • 现在大多都优先采用这种算法回收新生代
    2021222

  • 对此种回收算法Andrew Appel提出了更优化的半区复制策略–“Appel式回收

    • IBM公司对新生代做过量化诠释–新生代中的对象 98% 都熬不过第一轮收集
    • “Appel式回收”:
      • 把内存划分为1 块比较大的 Eden 区,2 块较小的 Survivor 区
      • 每次使用 Eden 区和 1 块 Survivor 区
      • 其中默认Ende和Survivor的大小比例是8:1,即每次新生代种使用的空间为90%
      • 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空
    • 罕见情况:
      • 当一个Survivor空间不足以容纳一次Minor GC之后存活的对象,就需要依赖其他内存区域(大多为老年代)进行分配担保

2.4 标记-整理算法–解决碎片化问题

  • 描述:
    • 标记方法与 “标记 - 清除算法” 一样
    • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存
    • 常用在老年代
  • 缺点:
    • 效率问题,在移动时,必须全程暂停用户应用
      2021223

3. HotSpot 中 GC 算法的实现

通过之前的分析,GC 算法的实现流程简单的来说分为以下两步:

  1. 找到死掉的对象
  2. 清除死掉的对象
根节点枚举

在进行枚举根节点的这个操作时,为了保证准确性,需要在一段时间内 “冻结” 整个应用,即 Stop The World(传说中的 GC 停顿)。

进行可达性分析的第一步,就是枚举 GC Roots。为了提高 GC 的效率,HotSpot 使用了一种 OopMap 的数据结构,OopMap 记录了栈上本地变量到堆上对象的引用关系,GC 的时候就不用遍历整个栈只遍历每个栈的 OopMap 就行了。

安全点

在OopMap的协助下,HotSpot可以快速准确的完成GC Roots枚举,但同时会有一个问题:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,这在内存空间方面GC的代价太高。

为解决这一问题,HotSpot采用了“安全点”策略。这就决定了用户程序执行时强制要求必须执行到达安全点后才能够暂停。安全点的选取,既不能太少以至于让垃圾收集器等待的时间过长,也不能太过频繁增加运行负担。安全点的选取是以“是否具有让程序长时间执行的特征”为标准进行选定的。也就是说,既要让程序运行一段时间,又不能让这个时间太长。而真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等。所有只有具有这些功能的指令才会产生安全点,虚拟机一般会将这些地方设置为安全点更新 OopMap 并判断是否需要进行 GC 操作。

这其中就涉及一个问题,如何在发生垃圾收集时让所有线程都跑到最近的安全点,然后停顿下来?
主要有两种方案:

  • 抢占式中断
    • 先中断线程全部
    • 如果发现有线程不在安全点,就恢复此线程
  • 主动式中断
    • 当垃圾收集需要中断线程的时候,仅仅简单的设置一个标志位
    • 各个线程执行时不断区主动轮询这个标志,发现标志为真时,在最近的安全点主动中断挂起。

除此安全点之外,还有一个叫做 “安全区域” 的东西,一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于 Sleep 或者 Blocked 状态的线程是没办法自己到达安全点中断自己的,我们总不能让 GC 操作一直等着这些个“不执行” 的线程重新被分配资源吧。对于这种情况,我们要依靠安全区域来解决。

安全区域

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

当线程执行到安全区域时,它会把自己标识为 Safe Region,这样 JVM 发起 GC 时是不会理会这个线程的。当这个线程要离开安全区域时,它会检查系统是否在 GC 中,如果不在,它就继续执行,如果在,它就等 GC 结束再继续执行。

记忆集与卡表

为了解决对象跨代引用问题,垃圾收集器在新生代建立了名为记忆集的数据结构。其实所有涉及部分区域收集行为的垃圾收集器都会产生跨代引用问题。

  • 记忆集是一种用于记录非收集区域指向收集区域的指针集合的抽象数据结构。
  • 记忆集并不需要使用跨代对象数组来实现,而有如下一些选择:
    • 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
    • 对象精度:每个记录精确到一个对象,该对象里有字段包含跨代指针
    • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
      • 卡精度是用一种“卡表”的方式实现,是目前最常用的记忆集实现形式
      • 卡表最简单的形式可以只是一个字节数组CARD_TABLE,字节数组中每个元素都对应一块特定大小的内存块,这个内存块被称作“卡页”
      • 只要卡页内存在对象的字段跨代引用,则对应卡表的数组元素值标识为1,称这个元素变脏。GC时只需要刷选变脏元素,就能获得垮代指针并加入到GC Roots。
写屏障

在HotSpot虚拟机里,通过写屏障技术维护卡表状态。它可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知。

并发条件下的可达性分析

三色标记:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析的开始阶段,所有对象都是白色的,若在结束阶段还有白色的对象,那他就是不可达的。
  • 黑色:表示对象已经被垃圾收集器访问过,这个对象到其他对象的所有引用都被扫描过。
  • 灰色:表示对象已经被垃圾收集器访问过,但是这个对象到其他对象的引用未被2完全扫描。

在并发条件下,如果同时满足以下两个条件:

  • 有可能在分析完一个A对象后(A对象变黑色),A对象又添加一条到B对象(白色,尚未开始访问)的引用
  • 同时删除所有灰色对象到B对象的直接或间接引用。

则此时A对象已经分析结束不会回去再扫描一次,而所有没有可达引用指向B对象。这就会导致添加的引用无法被记录,B对象丢失。解决这个并发问题时,针对两个条件可有两个解决方案:增量更新(记录添加信息,如果添加,结束后就再次扫描)和原始快照(类似记录原始状态,对刚开始扫描时的那一刻的状态进行扫描)

4 经典的垃圾收集器

收集算法是内存回收的方法论,垃圾收集器是内存回收的实践者。

2021271
图中展示不同分代的垃圾收集器,存在连线说明可以搭配使用(除了万能的 G1)。

4.1 Serial收集器

Serial收集器,最基础、历史最悠久。

搭配老年代收集器Serial Old的运行过程:

2021272

虽然Serial收集器很老,而且还是单线程处理,,但是它依然是HotSpot虚拟机运行在 Client 模式下的默认新生代收集器,他的优势在于简单高效,对于内存受限的环境,它是所有收集器里额外内存消耗最小的。对于单核或者少核的环境来说它的效率的最高的。

4.2 ParNew收集器

ParNew收集器本质上就是Serial收集器的多线程并行版本,虽然除此之外没什么创新之处,但是它却是许多运行在 Server 模式下的虚拟机中的首选新生代收集器,因为除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。ParNew收集器是激活CMS后的默认新生代收集器。

搭配老年代收集器Serial Old的运行过程:

2021273

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是新生代收集器,同样也是标记复制算法实现的,也能够并行收集。可见诸多特性与ParNew收集器相似。

arallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

其吞吐量就是:运行用户代码时间 / ( 运行用户代码时间 + 运行垃圾收集时间 )

由此可见:

  • 停顿时间越短就越适合需要与几乎或者需要保证服务响应质量的程序,良好的响应速度能更好的提升用户体验。
  • 高吞吐量则可以最高效率的利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多的交互分析任务。

可调节的虚拟机参数:

  • XX:MaxGCPauseMillis:最大 GC 停顿的毫秒数;
  • XX:GCTimeRatio:吞吐量大小,一个 0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1);
  • XX:+UseAdaptiveSizePolicy:一个开关参数,打开后就无需手工指定 Xmn,-XX:SurvivorRatio 等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。

4.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,同样是单线程版本,使用标记整理算法。此收集器的主要意义是:供Client模式下的HotSpot虚拟机使用。如果在服务端,也可能有两种用途:一种实在JDK5以前版本中与Parallel Scavenge 收集器搭配使用,另一种是作为,CMS收集器失败时的后备预案。

4.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记整理算法。JDK6时提供。在此之前,Parallel Scavenge收集器一直只能与Serial Old收集器搭配,而由于多种原因,这种组合不一定比ParNew 和CMS的组合优秀。

直到Parallel Old收集器出现,“吞吐量优先”收集器才正经搭配起来,Parallel Scavenge搭配Parallel Old收集器的组合如图:

2021275

4.6 CMS收集器

  • 以获取最短回收停顿时间为目标
  • -基于标记-清除算法
  • 运行过程分为:
    • 初始标记:仅仅标记GC Roots能直接关联的对象
    • 并发标记:从GC Roots直接关联对象开始遍历整个对象图。此过程时间较长,但是不需要停顿用户线程,可与用户线程一起并发运行。
    • 重新标记:为了修正并发标记期间,因用户程序运行时导致标记变动的标记记录。(增量更新)
    • 并发清除:清理删除掉标记阶段判断的已死亡对象。可与用户线程一起并发运行。
  • 缺点:
    • 对CPU资源敏感:并发时虽然不会导致用户线程停顿,但是会占用一部分算力,导致应用程序变慢。
    • 无法处理“浮动垃圾”:在并发清理阶段,用户线程产生的新垃圾,无法在本次垃圾回收中回收。同样,并发时给用用户留存的空间不足(这个由CMS收集器启动阈值控制)以分配新对象,就会出现一次“并发失败”,启动备用预案:冻结用户线程,启用Serial Old收集器重新进行一次老年代的垃圾收集。
    • 产生大量空间碎片:JDK9之前,提供了两个参数,在不得不进行Full GC的时候启动碎片整理,或在CMS执行若干次之后启动碎片整理。
    • 在JDK9中被标记为废弃(Deprecate)

概念参考图:

2021278
运行示意图:
2021276

4.7 Garbage First收集器(G1)–区域分代化

G1收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1收集器是在Java7 update 4之后引入的一个新的垃圾回收器。

JDK9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,称为服务端模式下的默认垃圾收集器。而CMS沦落至被声明为不推荐使用的收集器。

  • 并行与并发
    • 并行:在G1回收期间,可以有多个GC线程同时工作。此时用户线程STW。
    • 并发:G1拥有与应用交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用的情况。
  • 整体上采用标记-整理算法,但局部Region又采用标记-复制算法。
  • 主要针对配备多核CPU以及大容量内存的机器
  • 收集的目标范围
    • G1不再进行Minor GC 或Major GC或Full GC,而是进行面向堆内存任何部分来组成回收集(CSet)进行回收。
    • Mixed GC 模式:垃圾回收的衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
  • G1基于Region的堆内存布局
    • G1遵守分代收集理论
    • 堆内存布局不在坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分成多个大小相等且独立的区域(Region),每一个Region根据需要扮演新生代的Eden、Survivor空间或老年代。
    • Region中存在一个Humongous区域,专门存储大对象,大对象的大小超过1/2Region。而超过整个Region的超大对象存储在连续的Humongous Region中。G1的大多数行为把Humongous Region作为老年代的一部分。
  • 收集器对扮演不同角色的Region采用不同收集策略。
  • G1虽然保留新生代和老年代,但是新生代和老年代都不在固定,而是一系列区域的动态集合。此时新生代和老年代就是在逻辑上,内存物理层面不是一整块区域。
  • 可预测的停顿时间模型(Pause Prediction Model)
    • 能明确指定在一个长度为M ms时间片段内,消耗在垃圾收集上的时间大概率不超过N ms。
    • 让G1收集器跟踪各个Region中的垃圾堆积的“价值”大小(其中价值就是回收空间和回收所需时间),维护一个优先级列表,每次在允许的收集时间内优先回收价值最大的Region。
  • 收集器的运作过程
    • 初始标记:仅仅标记GC Roots能直接关联的对象,此时非并发。
    • 并发标记:进行可达性分析,此时和用户线程并发执行。
    • 最终标记:对用户做短暂停顿,此时垃圾回收线程并行,处理并发阶段产生的引用变动。
    • 筛选回收:更新Region数据,对回收价值排序,制定回收计划,把决定回收的Region中的不回收对象复制到空的Region中,清理旧Region,这里停掉用户线程移动对象,多条收集器线程并行执行。
      2021281

5.低延迟垃圾收集器

5.1 Shenandoah收集器

第一款不由Oracle公司开发的HotSpot垃圾收集器。
旨在针对JVM上的内存回收实现低停顿的需求。

优势:低延迟。

缺点:高运行负担下的吞吐量下降。

工作过程分为9个阶段:

  • 初始标记
  • 并发标记
  • 最终标记
  • 并发清理
  • 并发回收
  • 初始引用更新
  • 并发引用更新
  • 最终应用更新
  • 并发清理

5.2 ZGC收集器

定义:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

  • 内存布局:Region具有动态性,–动态创建与销毁,以及动态的区域容量与大小。
    • 小型Region:容量2MB,存放小于256KB的对象。
    • 中型Region:容量32MB,存放大于等于256小于4MB的对象。
    • 大型Region:容量可动态变化,但必须是2MB的整数倍,用于放置4MB及以上的对象。并且每个大型Region只会放一个对象。且它不会被重分配。
  • 并发整理算法:
    • 读屏障
    • 染色指针技术
    • 多重映射技术
  • 工作阶段
    • 并发标记:可达性分析
    • 并发预备重分配:扫描所有的Region,需要清理的Region组成重分配集。
    • 并发重分配:把重分配集中存活的对象复制到新的Region,并维护转发表(记录转向关系,应对并发时访问存活复制的对象,称之为指针的“自愈”能力)。
    • 并发重映射:修正堆中指向重分配集中旧对象的所有引用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值