首页 > 系统相关 >JVM学习-03-垃圾收集器与内存分配策略

JVM学习-03-垃圾收集器与内存分配策略

时间:2024-12-01 15:33:38浏览次数:6  
标签:03 收集器 对象 内存 GC 引用 JVM 垃圾

第三章、垃圾收集器与内存分配策略

3.1 概述

垃圾收集需要完成的三件事情:

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

3.2 对象已死?

GC相关博客:JVM GC?我比《深入理解Java虚拟机》再深入一点点_技术交流_牛客网 (nowcoder.com)

3.2.1 引用计数算法

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

原理:在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。

优点‌:

  • 简单高效‌:引用计数算法的实现相对简单,不需要遍历整个对象图,只需要维护计数器即可。
  • 实时性‌:引用计数算法可以实时地进行内存回收,不需要等待垃圾回收器的运行。

缺点‌:

  • 循环引用问题‌:当存在循环引用时,引用计数算法无法正确地回收内存。例如,对象A和对象B相互引用,它们的引用计数器都不会变为0,导致内存泄漏。

3.2.2 可达性分析算法

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

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

可达性分析算法:GC root GC Roots详解-CSDN博客

在Java语言中,"GC roots", 或者说tracing GC的"根集合", 是一组必须活跃的引用。可作为 GC Roots 的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 本地方法栈中 JNI(Native方法)引用的对象
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  8. 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,“临时性”地加入的其他对象。

3.2.3 再谈引用

简介(书中内容)
  • 在JDK 1.2版之前,Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表 某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在 这种定义下只有“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显 得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空 间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应 用场景。

  • 在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

详细说明

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。
前言

参考文章:

Java 四种引用类型完全解析_当一个对象不再有任何强引用指向它时-CSDN博客

面试官:说说强引用、软引用、弱引用、虚引用吧 (baidu.com)

FinalReference详解-CSDN博客

JVM源码分析之FinalReference完全解读 - 你假笨 (lovestblog.cn)

很早就听说有这些引用,从来没用过,最近看源码发现,内部用到的地方挺多,就看了一下相关的文档。

强引用,软引用,弱引用,虚弱引用四种引用类型。

Java 中引入四种引用的目的是让程序自己决定对象的生命周期,JVM 是通过垃圾回收器对这四种引用做不同的处理,来实现对象生命周期的改变。

简单比喻

1.保洁(GC),日用品(Strong Reference),一次性用品的(soft Reference),生活垃圾(weak Reference),灰尘(PhantomReference)

GC一次,并不会清扫日用品,如果空间较为拥挤,则会清扫一次性用品,每次GC,均会清扫生活垃圾,
如果判断是否要进行,GC的标准,是判断灰尘是否已经被清扫。

相关类
  • Reference
    • Reference 供描述所有引用对象通用的行为的抽象类
  • SoftReference,WeakReference,PhantomReference
  • FinalReference
    • FinalReference 类是包内可见,其他三种引用类型均为 public,可以在应用程序中直接使用。
  • ReferenceQueue
    • ReferenceQueue ,单链表结构,存储被持有回收对象的引用类型,可以和四种引用类型组合使用,而虚引用则必须和ReferenceQueue关联使用

不同的引用类型有着不同的特性,同时也对应着不同的使用场景。

JDK 8中的 UML关系图

强引用(Strongly Re-ference)

实际编码中最常见的一种引用类型。常见形式如:A a = new A();等。

A a = new A()
或
Object obj = new Object()

强引用本身存储在栈内存中,其存储指向堆内存中对象的地址。

一般情况下,当堆内存中的对象不再有任何强引用指向它时,垃圾回收机器开始考虑可能要对此内存进行的垃圾回收。如当进行编码:a = null,此时,刚刚在堆中分配地址并新建的a对象没有其他的任何引用,当系统进行垃圾回收时,堆内存将被垃圾回收。

SoftReference、WeakReference、PhantomReference都是类java.lang.ref.Reference的子类。Reference作为抽象基类,定义了其子类对象的基本操作。Reference子类都具有如下特点:

  1. Reference子类不能无参化直接创建,必须至少以强引用对象为构造参数,创建各自的子类对象;
  2. 因为1中以强引用对象为构造参数创建对象,因此,使得原本强引用所指向的堆内存中的对象将不再只与强引用本身直接关联,与Reference的子类对象的引用也有一定联系。且此种联系将可能影响到对象的垃圾回收。

根据不同的子类对象对其指示对象(强引用所指向的堆内存中的对象)的垃圾回收不同的影响特点,分别形成了三个子类,即SoftReference、WeakReference和PhantomReference。

软引用(Soft Reference)

软引用的一般使用形式如下:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

通过对象的强引用为参数,创建了一个SoftReference对象,并使栈内存中的srA指向此对象。

此时,进行如下编码:a = null,对于原本a所指向的A对象的垃圾回收有什么影响呢?

先直接看一下下面一段程序的输出结果:

 import java.lang.ref.SoftReference;
 
 public class ReferenceTest {
 
     public static void main(String[] args) {
 
         A a = new A();
         
         SoftReference<A> srA = new SoftReference<A>(a);
 
         a = null;
 
         if (srA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + srA.get());
         }
 
         // 垃圾回收
         System.gc();
 
         if (srA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + srA.get());
         }
 
     }
 }
 
 class A {
 
 }

输出结果为:

1 a对象尚未被回收A@4807ccf6
2 a对象尚未被回收A@4807ccf6

  • 当 a = null后,堆内存中的A对象将不再有任何的强引用指向它,但此时尚存在srA引用的对象指向A对象。

  • 当第一次调用srA.get()方法返回此指示对象时,由于垃圾回收器很有可能尚未进行垃圾回收,此时get()是有结果的,这个很好理解。

  • 当程序执行System.gc();强制垃圾回收后,通过srA.get(),发现依然可以得到所指示的A对象,说明A对象并未被垃圾回收。

那么,软引用所指示的对象什么时候才开始被垃圾回收呢?需要满足如下两个条件:

  1. 当其指示的对象没有任何强引用对象指向它;

  2. 当虚拟机内存不足时。

因此,SoftReference变相的延长了其指示对象占据堆内存的时间,直到虚拟机内存不足时垃圾回收器才回收此堆内存空间。

弱引用(Weak Reference)

弱引用的一般使用形式如下:

A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);

当没有任何强引用指向此对象时, 其垃圾回收又具有什么特性呢?

 import java.lang.ref.WeakReference;
 
 public class ReferenceTest {
 
     public static void main(String[] args) {
 
         A a = new A();
 
         WeakReference<A> wrA = new WeakReference<A>(a);
 
         a = null;
 
         if (wrA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + wrA.get());
         }
 
         // 垃圾回收
         System.gc();
 
         if (wrA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + wrA.get());
         }
 
     }
 
 }
 
 class A {
 
 }

输出结果为:

1 a对象尚未被回收A@52e5376a
2 a对象进入垃圾回收流程

输出的第一条结果解释同上。当进行垃圾回收后,wrA.get()将返回null,表明其指示对象进入到了垃圾回收过程中。因此,对弱引用特点总结为:

  • WeakReference不改变原有强引用对象的垃圾回收时机,一旦其指示对象没有任何强引用对象时,此对象即进入正常的垃圾回收流程。

那么,依据此特点,很可能有疑问:WeakReference存在又有什么意义呢?

其主要使用场景见于:当前已有强引用指向强引用对象,此时由于业务需要,需要增加对此对象的引用,同时又不希望改变此引用的垃圾回收时机,此时WeakReference正好符合需求,常见于一些与生命周期的场景中。

补充
  • 对于SoftReference和WeakReference,还有一个构造器参数为ReferenceQueue,当SoftReference或WeakReference所指示的对象确实被垃圾回收后,其引用将被放置于ReferenceQueue中。

  • 注意上文中,当SoftReference或WeakReference的get()方法返回null时,仅是表明其指示的对象已经进入垃圾回收流程,此时对象不一定已经被垃圾回收。

  • 只有确认被垃圾回收之后,其引用才会被放置于ReferenceQueue中。


 public class ReferenceTest {
 
     public static void main(String[] args) {
 
         A a = new A();
 
         ReferenceQueue<A> rq = new ReferenceQueue<A>();
         WeakReference<A> wrA = new WeakReference<A>(a, rq);
 
         a = null;
 
         if (wrA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + wrA.get());
         }
 
         System.out.println("rq item:" + rq.poll());
 
         // 垃圾回收
         System.gc();
 
         if (wrA.get() == null) {
             System.out.println("a对象进入垃圾回收流程");
         } else {
             System.out.println("a对象尚未被回收" + wrA.get());
         }
 
         try {
             Thread.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
         System.out.println("rq item:" + rq.poll());
 
     }
 }
 
 class A {
 
     @Override
     protected void finalize() throws Throwable {
         super.finalize();
         System.out.println("in A finalize");
     }
 
 }

输出结果为:

1 a对象尚未被回收A@302b2c81
2 rq item:null
3 a对象进入垃圾回收流程
4 rq item:null
5 in A finalize

由此,验证了“仅进入垃圾回收流程的SoftReference或WeakReference引用尚未被加入到ReferenceQueue”。

虚引用(Phantom Reference)

与SoftReference或WeakReference相比,PhantomReference主要差别体现在如下几点:

  1. PhantomReference只有一个构造函数PhantomReference(T referent, ReferenceQueue<? super T> q),因此,PhantomReference使用必须结合ReferenceQueue;

  2. 不管有无强引用指向PhantomReference的指示对象,PhantomReference的get()方法返回结果都是null。


 public class ReferenceTest {
 
     public static void main(String[] args) {
 
         A a = new A();
 
         ReferenceQueue<A> rq = new ReferenceQueue<A>();
         PhantomReference<A> prA = new PhantomReference<A>(a, rq);
 
         System.out.println("prA.get():" + prA.get());
         
         a = null;
         
         System.gc();
         
         try {
             Thread.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
         System.out.println("rq item:" + rq.poll());
 
     }
 }
 
 class A {
 
 }

输出结果为:

1 prA.get():null
2 rq item:java.lang.ref.PhantomReference@1da12fc0

代码中的Thread.sleep(1);作用与上例中相同,都是确保垃圾回收线程能够执行。否则,进入垃圾回收流程而没有真正被垃圾回收的指示对象的虚引用是不会被加入到PhantomReference中的。

与WeakReference相同,PhantomReference并不会改变其指示对象的垃圾回收时机。且可以总结出:ReferenceQueue的作用主要是用于监听SoftReference/WeakReference/PhantomReference的指示对象是否已经被垃圾回收。

  • 虚引用的主要作用是跟踪对象垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。
  • PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 GC 回收,用来实现比 finalization 机制更灵活的回收操作。换句话说,设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理
引用队列(ReferenceQueue)
  • ReferenceQueue 是用来配合引用工作的,没有ReferenceQueue 一样可以运行。

  • SoftReference、WeakReference、PhantomReference 都有一个可以传递 ReferenceQueue 的构造器。PhantomReference 必须和ReferenceQueue关联使用。

  • 创建引用的时候,可以指定关联的队列,当 GC 释放对象内存的时候,会将引用加入到引用队列。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于是一种通知机制。

  • 当关联的引用队列中有数据的时候,意味着指向的堆内存中的对象被回收。通过这种方式,JVM 允许我们在对象被销毁后,做一些我们自己想做的事情。

Reference源码(JDK8)

强软弱虚四种引用,我们有了个大概的认识,我们也知道除了强引用没有对应的类型表示,是普遍存在的。剩下的三种引用都是java.lang.ref.Reference的直接子类。

那就会有疑问了,我们可以通过继承 Reference,自定义引用类型吗?

Abstract base class for reference objects. This class defines the operations common to all reference objects. Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.

JDK 官方文档是这么说的:

  • Reference是所有引用对象的基类。这个类定义了所有引用对象的通用操作。因为引用对象是与垃圾收集器紧密协作而实现的,所以这个类可能不能直接子类化。

Reference 的4种状态:

FinalReference

FinalReference详解-CSDN博客

JVM源码分析之FinalReference完全解读 - 你假笨 (lovestblog.cn)

  • FinalReference主要用于实现对象的终结(finalization)过程
  • FinalReference是Reference类的子类之一,它继承自Reference类。FinalReference的主要用途是与垃圾回收器(Garbage Collector)配合,实现对象的终结过程。这个过程涉及到对象的finalize方法,当一个对象即将被垃圾回收之前,会调用其finalize方法。
  • 需要注意的是,Finalize方法和FinalReference的使用在现代Java编程中并不推荐,因为它们可能导致内存泄漏和性能问题。在现代Java应用中,更推荐使用try-with-resources语句或其他资源管理技术来替代finalize方法进行资源清理。

3.2.4 生存还是死亡?

  • 在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓 刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没 有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是 否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。
  • finalize()方法是对象逃脱死亡命运的最后一次机会
  • 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临 下一次回收,它的finalize()方法不会被再次执行
  • 建议大家尽量避免使用它!!!它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为 不推荐使用的语法。

3.2.5 回收方法区

方法区垃圾收集 的“性价比”通常也是比较低的

方法区的垃圾收集主要回收两部分内容:

  • 废弃的常量
  • 不再使用的类型

3.3 垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。由于引用计数式垃圾收集算法在本书讨论到的主流Java虚拟机中均未涉及,所以我们暂不把它作为正文主要内容来讲解,本节介绍的所有算法均属于追踪式垃圾收集的范畴。

3.3.1 分代收集理论

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

不同分代的类似名词, 在这里统一定义:

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

3.3.2 标记-清除算法

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

  1. 首先标记出所有需要回收的对象。
  2. 在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

它的主要缺点有两个:

  1. 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低。
  2. 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3.3.3 标记-复制算法

标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

  • IBM公司曾有一项专门研 究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此 并不需要按照1∶1的比例来划分新生代的内存空间

  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新 生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会 被“浪费”的

  • 任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活。因此Appel式回收还有一个充当罕见情况的“逃生门”的安 全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

  • 内存的分配担保:如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的

3.3.4 标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存 活的极端情况,所以在老年代一般不能直接选用这种算法

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整 理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

3.3.5 移动对象

是否移动对象都存在弊端:

  1. 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World”。
  2. 但如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。譬如通过“分区空闲分配链 表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘 上存储和访问就是通过硬盘分区表实现的)。内存的访问是用户程序最频繁的操作,甚至都没有之 一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。

移动则内存回收时会更复杂,不移动则内存分配时会更复杂。

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。

  • 移动(标记-整理算法):对象停顿时间会变长,吞吐量会提高
  • 不移动(标记-清除算法):对象停顿时间会更短,吞吐量会下降

HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的,这也从侧面印证这点。

还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。

3.4 HotSpot的算法细节实现

GC相关博客:JVM GC?我比《深入理解Java虚拟机》再深入一点点_技术交流_牛客网 (nowcoder.com)

3.4.1 根节点枚举

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的“Stop The World”的困扰。

即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、 ZGC等收集器,枚举根节点时也是必须要停顿的。

由于目前主流Java虚拟机使用的都是准确式垃圾收集(这个概念在第1章介绍Exact VM相对于 Classic VM的改进时介绍过),所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

补充:什么是OopMap

图解 OopMap、Safe Point、Safe Region - 知乎 (zhihu.com)

GC Roots 枚举的过程中,是需要暂停用户线程的,对栈进行扫描,找到哪些地方存储了对象的引用。然而,栈存储的数据不止是对象的引用,因此对整个栈进行全量扫描,显然是很耗费时间,影响性能的。因此,在 HotSpot 中采取了空间换时间的方法,使用 OopMap 来存储栈上的对象引用的信息。在 GC Roots 枚举时,只需要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots。

OopMap 中存储了两种对象的引用:

  • 栈里和寄存器内的引用
    在即时编译中,在特定的位置记录下栈里和寄存器里哪些位置是引用

  • 对象内的引用
    类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据
    注:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为有效地址或偏移量,因此,实际地址=所在段的起始地址+偏移量

在 JVM中,一个线程为一个栈,一个栈由多个栈桢组成,一个栈桢对应一个方法,一个栈帧可能有多个 OopMap。

假设,这两个方法都只有一个 OopMap,并且是在方法返回之前:

// 方法1存储在栈帧3
public void testMethod1() {
    // 栈里和寄存器内的引用
    DemoD demoD = new DemoD();
}

// 方法2存储在栈帧8
public void testMethod2() {
    // 栈里和寄存器内的引用
    DemoA demoA = new DemoA();
    // 对象内的引用
    demoA.setDemoC(new DemoC());
    
    // 栈里和寄存器内的引用
    DemoA demoB = new DemoB();
} 

那么 testMethod1() 和 testMethod2() 的 OopMap 如下图所示:

因此,可以理解为 OopMap 就是商场的商品清单,清单上记录着每一种商品的所在位置和数量,通过清单可以直接到对应的货架上找到商品。

如果没有这份清单,需要寻找一件商品的时候,就只能从头开始,按顺序翻找每一个货架上的商品,直到找到对应的商品。

3.4.2 安全点

图解 OopMap、Safe Point、Safe Region - 知乎 (zhihu.com)

在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。

只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。只有在 Safe Point 才会生成(或更新)对应的 OopMap。

Safe Point 就是一个安全点,可以理解为用户线程执行过程中的一些特殊位置。线程执行到 Safe Point 的时候,OopMap 保存了当前线程的上下文,当线程执行到这些位置的时候,说明线程当前的状态是确定的,线程有哪些对象、使用了哪些内存。

那么,哪些地方适合放置 Safe Point?

◉ 所有的非计数循环的末尾
(防止循环体的执行时间太长,一直进入不了 Safe Point)

◉ 所有方法返回之前

◉ 每条 Java 编译后的字节码的边界

例如,在方法返回之前插入 Safe Point,那么栈帧8只有一个 OopMap:

除此之外,Safe Point 的数量不能太少,太少会导致进入 Safe Point 的前置时间过长,以至于垃圾回收线程等待的时间太长。

Safe Point 的数量也不能太多,太多意味着将会频繁生成(或更新)OopMap ,会有性能损耗。

当所有线程都到达Safe Point,有两种方法中断线程:

◉ 抢占式中断(Preemptive Suspension)
JVM会中断所有线程,然后依次检查每个线程中断的位置是否为Safe Point,如果不是则恢复用户线程,让它执行至 Safe Point 再阻塞。

◉ 主动式中断(Voluntary Suspension)
大部分 JVM 实现都是采用主动式中断,需要阻塞用户线程的时候,首先做一个标志,用户线程会主动轮询这个标志位,如果标志位处于就绪状态,就自行中断。

因此,可以理解为 OopMap + Safe Point 就是商店的商品清点,只需要在特定的时间进行,例如每天开始营业之前和结束营业之后。而不是每卖出一件商品,或者每上架一件商品,就进行一次商品清点。

定时进入 SafePoint:每经过-XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻 Stop the world 的任务执行,可以设置-XX:GuaranteedSafepointInterval=0关闭这个定时,我推荐是关闭。

安全点导致长时间停顿案例:

真是绝了!这段被JVM动了手脚的代码! (qq.com)

3.4.3 安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走 到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于 这种情况,就必须引入安全区域(Safe Region)来解决。

中断的线程(用户线程处于Sleep状态或者Blocked状态)会被认定为在安全区域,如果 GC 过程中,中断的线程被唤醒,需要校验一下主动式中断策略的标志位是否为真(相当于判断 GC 是否完成),如果为真就继续保持中断,否则就被唤醒继续执行。

Safe Region 是一片区域,在这个区域的代码片段,引用关系不会发生变化,因此,在 Safe Region 中任意地方开始垃圾收集都是安全的。

线程执行到 Safe Region 时,首先标记线程已经进入 Safe Region,当线程将要离开 Safe Region 时,线程需要检查 JVM 是否已经完成 GC Roots 枚举。如果尚未完成,则需要一直等待,直到 GC Roots 枚举完成。

可以理解为 Safe Region 就是商店休假的整个时间段,在此期间,商品的数量没有发生改变,是确定的。

3.4.4 记忆集与卡表

解决对象跨代引用所带来的问题。

在回收新生代的时候,有可能有老年代的对象引用了新生代对象,所以老年代也需要作为根,但是如果扫描整个老年代的话效率就又降低了。

所以就搞了个叫记忆集(Remembered Set)的东西,来记录跨代之间的引用而避免扫描整体非收集区域。

事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题,因此我们有必要进一步理清记忆集的原理和实现方式。

记忆集就是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。
  • 根据跨代引用假说只需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
  • 虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

根据记录的精度分为

  • 字长精度,每条记录精确到机器字长。
  • 对象精度,每条记录精确到对象。
  • 卡精度,每条记录精确到一块内存区域。

最常见的是用卡精度来实现记忆集,称之为卡表。

JVM GC?我比《深入理解Java虚拟机》再深入一点点_技术交流_牛客网 (nowcoder.com)

我来解释下什么叫卡。

拿对象精度来距离,假设新生代对象 A 被老年代对象 D 引用了,那么就需要记录老年代 D 所在的地址引用了新生代对象。

那卡的意思就是将内存空间分成很多卡片。假设新生代对象 A 被老年代 D 引用了,那么就需要记录老年代 D 所在的那一块内存片有引用新生代对象。

也就是说堆被卡切割了,假设卡的大小是 2,堆是 20,那么堆一共可以划分成 10 个卡。

因为卡的范围大,如果此时 D 旁边在同一个卡内的对象也有引用新生代对象的话,那么就只需要一条记录。

一般会用字节数组来实现卡表,卡的范围也是设为 2 的 N 次幂大小。来看一下图就很清晰了。

假设地址从 0x0000 开始,那么字节数组的 0号元素代表 0x0000~0x01FF,1 号代表0x0200~0x03FF,依次类推即可。

然后到时候回收新生代的时候,只需要扫描卡表,把标识为 1 的脏表所在内存块加入到 GC Roots 中扫描,这样就不需要扫描整个老年代了。

用了卡表的话占用内存比较少,但是相对字长、对象来说精度不准,需要扫描一片。所以也是一种取舍,到底要多大的卡。

还有一种多卡表,简单的说就是有多张卡表,这里我画两张卡表示意一下。

上面的卡表表示的地址范围更大,这样可以先扫描范围大的表,发现中间一块脏了,然后再通过下标计算直接得到更具体的地址范围。

这种多卡表在堆内存比较大,且跨代引用较少的时候,扫描效率较高。

补充:收集器中解决跨代引用问题

个人理解猜测,有待证实

  • 部分收集(Partial GC)包括:新生代收集(Minor GC/Young GC)、老年代收集(Major GC/Old GC)、混合收集(Mixed GC)
  • 部分收集(Partial GC)(详细看3.3.1分代收集理论介绍)才会有跨代引用问题,整堆收集(Full GC)无跨代引用问题

收集器:

  • 经典收集器都需要维护1个Minor GC时用的卡表(新生代区域GC卡表,进行Minor GC时老年代赃卡表区域加入GC Roots进行扫描),FUll GC是整堆的,无跨代引用问题,不需要卡表。
  • 目前只有CMS收集器会有单独收集老年代的行为,不维护卡表的代价就是当CMS发生Old GC时,要把整个新生代作为GC Roots来进行扫描。
  • G1收集器把堆内存分割为很多区域(Region),是Mixed GC模式:每个Region都维护有自己的记忆集
    • G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这 种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更 复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃 圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额 外内存来维持收集器工作。
  • Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题(见3.4.5节)的发生概率。
  • ZGC也是一款不分代的收集器,也就代表着ZGC中只存在一种GC类型,同时也不需要记忆集这种概念存在,因为是单代的堆空间,所以每次回收都是扫描所有页面,不需要额外解决跨代引用问题。ZGC就完全没有使 用记忆集,它甚至连分代都没有,连像CMS中那样只记录新生代和老年代间引用的卡表也不需要,因而完全没有用到写屏障,所以给用户线程带来的运行负担也要小得多。

3.4.5 写屏障

卡表一般都是通过写屏障来维护的,写屏障其实就相当于一个 AOP,在对象引用字段赋值的时候加入更新卡表的代码。

写后屏障更新卡表:

void oop_field_store(oop* field, oop new_value) {
 // 引用字段赋值操作
*field = new_value;
 // 写后屏障,在这里完成卡表状态更新
post_write_barrier(field, new_value);
 }

这其实很好理解,说白了就是当引用字段赋值的时候判断下当前对象是老年代对象,所引用对象是新生代对象,于是就在老年代对象所对应的卡表位置置为 1,表示脏,待会需要加入根扫描。

不过这种将老年代作为根来扫描会有浮动垃圾的情况,因为老年代的对象可能已经成为垃圾,所以拿垃圾来作为根扫描出来的新生代对象也很有可能是垃圾。

不过这是分代收集必须做出的牺牲。

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影 响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

为了避伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。

在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启 卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损 耗,是否打开要根据应用实际运行情况来进行测试权衡。

3.4.6 并发的可达性分析

当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行。

在根节点枚举(见3.4.1节)这个步骤中,由于GC Roots相比起整个Java堆中全部的对象毕竟还算是极少数,且在各种优化技巧(如OopMap)的加持下,它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了。

可从GC Roots再继续往下遍历对象图,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长,这听起来是理所当然的事情。

要知道包含“标记”阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等 比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器,同理可知,如果能够削减这部分停顿时间的话,那收益也将会是系统性的。

想解决或者降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?为了能解释清楚这个问题,我们引入三色标记(Tri-color Marking)作为工具来辅 助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

简化描述:

  • 白色:表示还未搜索到的对象。
  • 灰色:表示正在搜索还未搜索完的对象。
  • 黑色:表示搜索完成的对象。

关于可达性分析的扫描过程,读者不妨发挥一下想象力,把它看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程。

如果用户线程此时是冻结的,只有收集器线程在工作,那不会有任何问题。

如果用户线程与收集器是并发工作呢?收集器在对象图上标记颜色,同时用户线程在修改引用 关系——即修改对象图的结构,这样可能出现两种后果。一种是把原本消亡的对象错误标记为存活, 这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误,下面演示了这样的致命错误具体是如何产生的。

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

只要打破这两个条件任意一个就不会发生漏标的情况。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)

这时候可以通过以下手段来打破两个条件:

  • 利用写屏障在黑色引用白色对象时候,将白色对象置为灰色,这叫增量更新。

  • 利用写屏障在灰色对象删除对白色对象的引用时,将白色对象置为灰,其实就是保存旧的引用关系。这叫STAB(snapshot-at-the-beginning)

CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。

补充:增量式 GC

所谓的增量式 GC 其实就是在应用线程执行中,穿插着一点一点的完成 GC,来看个图就很清晰了

这样看起来 GC 的时间跨度变大了,但是 mutator(在垃圾收集场景下将应用程序称为 mutator ) 暂停的时间变短了。

在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式的CMS收集器效果很一般,从 JDK 7开始,i-CMS模式已经被声明为“deprecated”,即已过时不再提倡用户使用,到JDK 9发布后i CMS模式被完全废弃。

补充:GC线程并行和并发

在谈论垃圾收集器的上下文语境中,它们可以理解为:

  • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

3.5 经典垃圾收集器

这个关系不是一成不变的,由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP 214)。

jvm垃圾收集器图里CMS和Serial Old为什么连在一起:

在JVM的垃圾收集器图中,CMS和Serial Old为什么连在一起‌,表示它们可以配合使用。具体来说,CMS(Concurrent Mark Sweep)垃圾回收器主要针对老年代和永久代(在Java 1.8版本中,永久代被元空间取代)进行垃圾回收,而不会收集新生代。CMS是一种预处理垃圾回收器,它不能等到老年代内存用尽时才开始回收,而需要在内存用尽前完成回收操作,否则会导致并发回收失败。因此,CMS有一个触发阈值,默认是老年代或永久代达到92%时开始执行回收操作。当CMS无法正常工作时,可以作为备用方案的是Serial Old收集器,它会接管CMS的工作,确保垃圾回收的进行。这种备用方案的存在,体现了在设计JVM垃圾收集器时考虑的容错性和稳定性‌。

3.5.1 Serial收集器

特点概括:

  • 新生代收集器
  • 标记-复制算法
  • 单线程工作,且必须暂停其他所有工作线程。

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代 收集器的唯一选择。

这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

似乎已经把Serial收集器描述成一个最早出现,但目前已经老而无用,食之无味,弃之可惜的“鸡肋”了,但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 获得最高的单线程收集效率

Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew收集器

特点概括:

  • 新生代收集器
  • 标记-复制算法
  • 多线程工作,且必须暂停其他所有工作线程。并行收集的多线程收集器
  • 除了Serial收集器外,目前只有它能与CMS 收集器配合工作(在JDK 9中完全取消了Serial收集器+CMS 收集器组合的支持)

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器完全一致,与Serial收集器相比并没有太多创新之处,在实现上这两种收集器也共用了相当多的代码。

除了Serial收集器外,目前只有它能与CMS 收集器配合工作。(在JDK 9中完全取消了Serial收集器+CMS 收集器组合的支持,CMS收集器只能与ParNew收集器配合工作了)

ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程 交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分 之百保证超越Serial收集器。当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。

它默认开启的收集线程数与处理器核心数量相同,在处理器核 心非常多(譬如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常 普遍)的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3.5.3 Paralel Scavenge收集器

特点概括:

  • 新生代收集器
  • 标记-复制算法
  • 多线程工作,且必须暂停其他所有工作线程。并行收集的多线程收集器
  • 吞吐量优先:达到一个可控制的吞吐量(Throughput)

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值, 即:

如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分 钟,那吞吐量就是99%。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的: 系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得 更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间 的确在下降,但吞吐量也降下来了。

-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。如果读 者对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设 置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或 XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就 由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。

3.5.4 Serial Old收集器

特点概括:

  • 老年代收集器
  • 标记-整理算法
  • 单线程工作,且必须暂停其他所有工作线程。

Serial Old是Serial收集器的老年代版本。

这个收 集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用 途:

  • 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用(需要说明一下,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解)
  • 另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

3.5.5 Paralel Old收集器

特点概括:

  • 老年代收集器
  • 标记-整理算法
  • 多线程工作,且必须暂停其他所有工作线程。并行收集的多线程收集器
  • 吞吐量优先:达到一个可控制的吞吐量(Throughput)

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

3.5.6 CMS收集器

特点概括:

  • 老年代收集器
  • 标记-清除算法
  • 并发收集:整个过程中耗时最长的并发标记和并发清除都是与用户线程一起工作
    • 重新标记阶段使用增量更新(详见3.4.6节)修正并发标记阶段期间产生变动的那一部分对象的标记记录。
    • 并发清除阶段:标记-清除算法不需要移动存活对象,所以可以并发。
  • 最短回收停顿时间优先

整个过程分为四个步骤,包括:

  • 1)初始标记(CMS initial mark)
    • 需要“Stop The World”。但是初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
  • 2)并发标记(CMS concurrent mark)
    • 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  • 3)重新标记(CMS remark)
    • 需要“Stop The World”。重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录(详见3.4.6节中关于增量更新的讲解),这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短。
  • 4)并发清除(CMS concurrent sweep)
    • 并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  • CMS收集器对处理器资源非常敏感。
    • 事实上,面向并发设计的程序都对处理器资源比较敏 感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
    • 为了缓解这种情况,虚拟机提 供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种, 所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样, 是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的 时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变 慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式的CMS收集器效果很一般,从 JDK 7开始,i-CMS模式已经被声明为“deprecated”,即已过时不再提倡用户使用,到JDK 9发布后i CMS模式被完全废弃。
  • CMS收集器无法处理“浮动垃圾”(Floating Garbage)。
    • 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    • 同样也是由于在垃圾收集阶段用户线程还需要持续 行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满 足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将 得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了
  • CMS收集器会有大量空间碎片产生。
    • CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

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

CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作:

  • 除了一个面向低延迟一个面向高吞吐量的目标不一致外,技术上的原因是Parallel Scavenge收集器及后面提到的G1收集器等都没有使用HotSpot中原本设计的垃圾收集器的分代框架,而选择另外独立实现。Serial、ParNew收集器则共用了这部分的框架代码。

3.5.7 Garbage First收集器

特点概括:

  • 整堆收集器 物理不分代,逻辑分代 。

    • 基于Region的堆内存布局。
    • 每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
    • Mixed GC模式。衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
  • 标记-整理算法

  • 并发收集

    与CMS对比:

    • 基于Region的堆内存布局,Region里面存在的跨Region引用问题解决:每个Region都维护有自己的记忆集。Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。
    • 最终标记阶段使用原始快照(SATB)算法(详见3.4.6节)修正并发标记阶段期间产生变动的那一部分对象的标记记录。
    • 和CMS比,筛选回收的时候是并行,但是没有内存碎片(因为CMS是标记清除算法,可以并行,G1是标记整理,不利用别的方法不能并发,即想要并发整理内存需要付出额外的代价)
  • 在延迟可控的情况下获得尽可能高的吞吐量 G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 换言之,它并非纯粹地追求低延迟,也在乎吞吐量。(低延迟+高吞吐量是不可能的,都是trade-off)

G1收集器的 运作过程大致可划分为以下四个步骤:

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  3. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部

从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序 一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控 制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC) 中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。

从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率 (Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收 集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路 从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。

G1和CMS两款垃圾收集器比较:

  • G1:指定最大停顿时间、分Region的内存布局、按收益动态确定回收集
  • 与CMS 的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存 空间碎片,垃圾收集完成之后能提供规整的可用内存。
  • G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高。
  • 虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂
  • 在执行负载的角度上,同样由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会 有不同,譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行 同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索 (SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也 会让对比结果继续向G1倾斜。

3.5.8 垃圾收集器发展史

初始阶段:

Serial收集器 jdk1.3

Serial Old收集器 jdk1.4

并行阶段:

ParNew收集器 没查到啥时候 猜测 1.4 或者1.5

Parallel Scavenge收集器 jdk1.4 吞吐量

Parallel Old收集器 jdk1.6 吞吐量

并发阶段:

CMS(Concurrent Mark Sweep) (划时代) jdk 1.5 响应速度

G1收集器 jdk1.7 (Java9默认) 在延迟可控的情况下获得尽可能高的吞吐量

3.5.9 小小总结对比垃圾收集器

Serial:Serial收集器是最基础、历史最悠久的收集,这个收集器是一个单线程工作的收集器

ParNew:ParNew收集器实质上是Serial收集器的多线程并行版本

Parallel Scavenge:多线程并行版本,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)

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

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

Shenandoah:Shenandoah反而更像是G1 的下一代继承者,Shenandoah是由RedHat公司独立发展的新型收集器项目,Shenandoah不仅要进行并发的垃圾标记,还要并发地进行对象清理后的整理动作

ZGC:ZGC(“Z”并非什么专业名词的缩写,这款收集器的名字就叫作Z Garbage Collector)是一款在 JDK 11中新加入的具有实验性质[1]的低延迟垃圾收集器,是由Oracle公司研发的。ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

  • ZGC也采用基于Region的堆内存布局,但 与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称 为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。
  • ZGC收集器有一个标志性的设计是它采用的染色指针技术(Colored Pointer,其他类似的技术中可 能将它称为Tag Pointer或者Version Pointer)。

Epsilon收集器:Epsilon收集器由RedHat公司在JEP 318中提出,在此提案里Epsilon被形容成一个无操作的收集器 (A No-Op Garbage Collector),而事实上只要Java虚拟机能够工作,垃圾收集器便不可能是真正“无操 作”的。原因是“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是本书为这一部分 所取的标题——“自动内存管理子系统”

  • 一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责,其 中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能 的垃圾收集器也必须实现的内容。
  • 如果读者的应用只要运行数分钟甚至数秒, 只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为 的Epsilon便是很恰当的选择。

经典垃圾收集器总结

  1. 优先调整堆的大小让 JVM 自适应完成。
  2. 如果内存小于 100M,使用串行收集器
  3. 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  4. 如果是多 CPU、需要高吞吐量、允许停顿时间超过 1 秒,选择并行或者 JVM 自己选择
  5. 如果是多 CPU、追求低停顿时间,需快速响应(比如延迟不能超过 1 秒,如互联网应用),使用并发收集器 官方推荐 G1,性能高。现在互联网的项目,基本都是使用 G1。

3.5.10 设置垃圾收集器

JVM垃圾收集器组合--各种组合对应的虚拟机参数实践 - 三国梦回 - 博客园 (cnblogs.com)

jdk1.8设置垃圾收集器:

  • 默认-XX:+UseParallelGC :Parallel Scavenge + Parallel Old

  • 启用CMS垃圾收集器用于老年代,设置-XX:+UseConcMarkSweepGC,设置后默认会再设置-XX:+UseParNewGC

    • 当以吞吐量为主的垃圾回收器(-XX:+UseParallelGC)无法满足应用程序的延时要求时,Oracle建议使用的垃圾回收器是CMS或者G1(-XX:+UseG1GC)

      默认情况下,此选项是禁用的,HotSpot VM会根据计算机的配置和JDK版本自动选择收集器。

      启用此选项后,-XX:+UseParNewGC选项将自动开启,并且不应禁用它,因为在JDK 8中不推荐使用以下选项组合:-XX:+UseConcMarkSweepGC -XX:-UseParNewGC。

      而CMS一旦老年代产生了很多内存碎片,它还会使用Serial Old进行清除。

      所以,-XX:+UseConcMarkSweepGC=ParNew+CMS+Serial Old

      所有的垃圾回收算法,除了ConcurrentMarkSweep外, 都会stop-the-world,比如他们在进行垃圾回收时,会停止所有的应用程序线程。ConcurrentMarkSweep 试图在后台进行大部分的工作,尽量把停顿时间削减到最小,但是它仍然会有stop-the-world阶段。而且,当ConcurrentMarkSweep 收集失败时,会回退到MarkSweepCompact(Serial Old)算法(该算法会完全阻止应用程序的运行)

  • 启用G1收集器:-XX:+UseG1GC

  • 启用Serial收集器:-XX:+UseSerialGC

  • 垃圾收集器在可视化工具里的显示:

    • -XX:+UseSerialGC:年轻代:Copy 老年代:MarkSweepCompact 串行收集器是最古老最稳定以及效率高的收集器,可能会产生较长的停顿。

    • -XX:+UseParallelGC:年轻代:PS Scavenge 老年代:PS MarkSweep 并发收集 吞吐量优先

    • -XX:+UseConcMarkSweepGC:年轻代:ParNew 老年代:ConcurrentMarkSweep 延时优先 并发收集,低停顿

    • -XX:+UseG1GC:年轻代:G1 Young Generation 老年代:G1 Old Generation 使用“垃圾优先”算法,该算法将堆分割成许多较小的空间

3.6 低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟 (Latency),三者共同构成了一个“不可能三角”。三者总体的表现会随技术进步而越来越好,但是要在这三个方面同时具有卓越表现的“完美”收集器是极其困难甚至是不可能的,一款优秀的收集器通 常最多可以同时达成其中的两项。

在内存占用、吞吐量和延迟这三项指标里,延迟的重要性日益凸显,越发备受关注。其原因是随着计算机硬件的发展、性能的提升,我们越来越能容忍收集器多占用一点点内存;硬件性能增长,对软件系统的处理能力是有直接助益的,硬件的规格和性能越高,也有助于降低收集器运行时对应用程序的影响,换句话说,吞吐量会更高。但对延迟则不是这样,硬件规格提升,准确地说是内存的扩 大,对延迟反而会带来负面的效果,这点也是很符合直观思维的:虚拟机要回收完整的1TB的堆内存,毫无疑问要比回收1GB的堆内存耗费更多时间。由此,我们就不难理解为何延迟会成为垃圾收集器最被重视的性能指标了。现在我们来观察一下现在已接触过的垃圾收集器的停顿状况,如图所示。

由图 可见,在CMS和G1之前的全部收集器,其工作的所有步骤都会产生“Stop The World”式的停顿; CMS和G1分别使用增量更新和原始快照(见3.4.6节)技术,实现了标记阶段的并发,不会因管理的堆 内存变大,要标记的对象变多而导致停顿时间随之增长。但是对于标记阶段之后的处理,仍未得到妥善解决。CMS使用标记-清除算法,虽然避免了整理阶段收集器带来的停顿,但是清除算法不论如何优 化改进,在设计原理上避免不了空间碎片的产生,随着空间碎片不断淤积最终依然逃不过“Stop The World”的命运。G1虽然可以按更小的粒度进行回收,从而抑制整理阶段出现时间过长的停顿,但毕竟 也还是要暂停的。

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,筛选回收阶段(Copy和Compact)是并行的。所以Copy和Compact是黄色。

Shenandoah和ZGC,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定 的,与堆的容量、堆中对象的数量没有正比例关系。这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”。

3.6.1 Shenandoah收集器

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC_shenandoah 对比zgc-CSDN博客

Java虚拟机:低延迟垃圾收集器_shenandoah收集器的工作过程-CSDN博客

总的来说,Shenandoah和G1有三点主要区别:

  1. G1的回收是需要STW的,而且这部分停顿占整体停顿时间的80%以上,Shenandoah则实现了并发回收。

  2. Shenandoah不再区分年轻代和年老代。

  3. Shenandoah使用连接矩阵替代G1中的卡表。

连接矩阵

G1中每个Region都要维护卡表,既耗费计算资源还占据了非常大的内存空间,Shenandoah使用了连接矩阵来优化了这个问题。

连接矩阵可以简单理解为一个二维表格,如果Region A中有对象指向Region B中的对象,那么就在表格的第A行第B列打上标记。

比如,Region 1指向Region 3,Region 4指向Region 2,Region 3指向Region 5:

相比G1的记忆集来说,连接矩阵的颗粒度更粗,直接指向了整个Region,所以扫描范围更大。但由于此时GC是并发进行的,所以这是通过选择更低资源消耗的连接矩阵而对吞吐进行妥协的一项决策。

Shenandoah收集器回收会扫描所有页面为什么还需要连接矩阵???

Shenandoah是一种垃圾收集器,它采用了不同的算法来优化垃圾收集的性能。在Shenandoah垃圾收集器中,垃圾收集过程中扫描所有页面的行为是出于性能优化的需要。

连接矩阵(Connection Matrix)是一种数据结构,通常用于模拟图的邻接矩阵,其中每个元素代表图中两个节点之间的连接。在垃圾收集器中,连接矩阵可能用于表示对象之间的引用关系,尤其是在分析或更新对象图时。

尽管Shenandoah垃圾收集器在执行垃圾收集时会扫描所有页面,但它还需要连接矩阵来维护对象之间的引用关系,并且在垃圾收集过程中进行更新。这是因为垃圾收集算法可能需要这些信息来确定哪些对象是可达的,哪些对象需要被回收。

简单来说,虽然Shenandoah垃圾收集器扫描所有页面,但它还需要连接矩阵来分析和更新引用,以确定和跟踪应用程序的活动对象,并计划最优的垃圾收集策略。

我的理解:

连接矩阵可以简单理解为一个二维表格,如果Region A中有对象指向Region B中的对象,那么就在表格的第A行第B列打上标记。

比如,Region 1指向Region 3,Region 4指向Region 2,Region 3指向Region 5:

Region 3被回收了之后,如果没有连接矩阵,我们不知道哪个Region指向3,即不会知道Region 1指向Region 3,不会更新Region 1的指针到最新,导致指向内容丢失。

转发指针

转发指针的性能优势
想要达到并发回收,就需要在用户线程运行的同时,将存活对象逐步复制到空的Region中,这个过程中就会在堆中同时存在新旧两个对象。那么如何让用户线程访问到新对象呢?

此前,通常是在旧对象原有内存上设置保护陷阱(Memory Protection Trap),当访问到这个旧对象时就会发生自陷异常,使程序进入到预设的异常处理器中,再由处理器中的代码将访问转发到复制后的新对象上。

自陷是由线程发起来打断当前执行的程序,进而获得CPU的使用权。这一操作通常需要操作系统参与,那么就会发生用户态到内核态的转换,代价十分巨大。

所以Rodney A.Brooks提出了使用转发指针来实现通过旧对象访问新对象的方式:在对象头前面增加一个新的引用字段,在非并发移动情况下指向自己,产生新对象后指向新对象。那么当访问对象的时候,都需要先访问转发指针看看其指向哪里。虽然和内存自陷方案相比同样需要多一次访问转发的开销,但是前者消耗小了很多。

转发指针的问题

转发指针主要存在两个问题:修改时的线程安全问题和高频访问的性能问题。

1.对象体增加了一个转发指针,这个指针的修改和对象本身的修改就存在了线程安全问题。如果通过被访问就可能发生复制了新对象后,转发对象修改之前发生了旧对象的修改,这就存在两个对象不一致的问题了。对于这个问题,Shenandoah是通过CAS操作来保证修改正确性的。

2.转发指针的加入需要覆盖所有对象访问的场景,包括读、写、加锁等等,所以需要同时设置读屏障和写屏障。尤其读操作相比单纯写操作出现频率更高,这样高频操作带来的性能问题影响巨大。所以Shenandoah在JDK13中对此进行了优化,将内存屏障模型改为引用访问屏障,也就是说,仅仅在对象中引用类型的读写操作增加屏障,而不去管原生对象的操作,这就省去了大量的对象访问操作。

Shenandoah的运行步骤

  1. 初始标记(Init Mark)[STW] [同G1]
    标记与GC Roots直接关联的对象。
  2. 并发标记(Concurrent Marking)[同G1]
    遍历对象图,标记全部可达对象。
  3. 最终标记(Final Mark)[STW] [同G1]
    处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集。
  4. 并发清理(Concurrent Cleanup)
    回收所有不包含任何存活对象的Region(这类Region被称为Immediate Garbage Region)。
  5. 并发回收(Concurrent Evacuation)
    将回收集里面的存货对象复制到一个其他未被使用的Region中。并发复制存活对象,就会在同一时间内,同一对象在堆中存在两份,那么就存在该对象的读写一致性问题。Shenandoah通过使用转发指针将旧对象的请求指向新对象解决了这个问题。这也是Shenandoah和其他GC最大的不同。
  6. 初始引用更新(Init Update References)[STW]
    并发回收后,需要将所有指向旧对象的引用修正到新对象上。这个阶段实际上并没有实际操作,只是设置一个阻塞点来保证上述并发操作均已完成。
  7. 并发引用更新(Concurrent Update References)
    顺着内存物理地址线性遍历堆空间,更新并发回收阶段复制的对象的引用。
  8. 最终引用更新(Final Update References)[STW]
    堆空间中的引用更新完毕后,最后需要修正GC Roots中的引用。
  9. 并发清理(Concurrent Cleanup)
    此时回收集中Region应该全部变成Immediate Garbage Region了,再次执行并发清理,将这些Region全部回收。

补充:

jdk12 - 聊聊ShenandoahGC的Brooks Pointers - code-craft - SegmentFault 思否

在垃圾收集器GC中,‌evacuation‌指的是将存活的对象从一个区域转移到另一个区域的过程。这个过程通常发生在年轻代空间用完之后,应用线程被暂停,然后年轻代堆区中的存活对象被复制到存活区。如果还没有存活区,则选择任意一部分空闲的小堆区用作存活区。这种复制或转移的过程被称为evacuation,它与年轻代收集器的工作原理相似‌。

如果在evacuation期间,其他线程通过旧的引用访问到了from-region的旧对象,它就需要根据旧对象的forwarding pointer找到to-region的拷贝对象;等所有旧对象的引用都更新完之后,后续就可以回收from-region的旧对象。

Shenandoah收集器:Shenandoah收集器并发复制的时候更新Brooks Pointers

  • evacuation的第一步是拷贝from-region的对象到to-region
  • evacuation的第二步使用CAS改变from-region的对象的forwarding pointer由自己变为指向to-region的拷贝对象
  • evacuation的第三步就是遍历heap,更新引用到to-region的拷贝对象

3.6.2 ZGC收集器

使用了读屏障染色指针内存多重映射等技术来实现可并发的标记-整理算法
ZGC收集器有一个标志性的设计是它采用的染色指针技术

ZGC是Oracle官方研发并JDK11中引入,并于JDK15中作为生产就绪使用,其设计之初定义了三大目标:

1.支持TB级内存

2.停顿控制在10ms以内,且不随堆大小增加而增加

3.对程序吞吐量影响小于15%

随着JDK的迭代,目前JDK16及以上版本,ZGC已经可以实现不超过1毫秒的停顿,适用于堆大小在8MB到16TB之间。

ZGC的内存布局
ZGC和G1一样也采用了分区域的堆内存布局,不同的是,ZGC的Region(官方称为Page,概念同G1的Region)可以动态创建和销毁,容量也可以动态调整。

ZGC的Region分为三种:

1.小型Region容量固定为2MB,用于存放小于256KB的对象。

2.中型Region容量固定为32MB,用于存放大于等于256KB但不足4MB的对象。

3.大型Region容量为2MB的整数倍,存放4MB及以上大小的对象,而且每个大型Region中只存放一个大对象。由于大对象移动代价过大,所以该对象不会被重分配。

重分配集(Relocation Set)

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC_shenandoah 对比zgc-CSDN博客

染色指针的三大优点

染色指针

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC_shenandoah 对比zgc-CSDN博客

染色指针的三大优点

染色指针的在GC过程中的作用

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC_shenandoah 对比zgc-CSDN博客

染色指针的三大优点
1.由于染色指针提供的“自愈”能力,当某个Page被清除后可以立刻被回收,而无需等待修正全部指向该Page的引用。

2.ZGC完全不需要使用写屏障,原因有二:由于使用染色指针,无需更新对象体;没有分代所以无需记录跨代引用。

3.染色指针并未完全开发使用,剩下的18位提供了非常大的扩展性。

而染色指针有一个天然的问题,就是操作系统和处理器并不完全支持程序对指针的修改。

多种内存映射
染色指针只是JVM定义的,操作系统、处理器未必支持。为了解决这个问题,ZGC在Linux/x86-64平台上采用了虚拟内存映射技术。

ZGC为每个对象都创建了三个虚拟内存地址,分别对应Remapped、Marked 0和Marked 1,通过指针指向不同的虚拟内存地址来表示不同的染色标记。

分代
ZGC没有分代,这一点并不是技术权衡,而是基于工作量的考虑。所以目前来看,整体的GC效率还有很大提升空间。

读屏障
ZGC使用了读屏障来完成指针的“自愈”,由于ZGC目前没有分代,且ZGC通过扫描所有Region来省去卡表使用,所以ZGC并没有写屏障,这成为ZGC一大性能优势。

NUMA
多核CPU同时操作内存就会发生争抢,现代CPU把内存控制系统器集成到处理器内核中,每个CPU核心都有属于自己的本地内存。

在NUMA架构下,ZGC会优先在自己的本地内存上分配对象,避免了内存使用的竞争。

在ZGC之前,只有Parallet Scavenge支持NUMA内存分配。

ZGC的运行步骤

ZGC和Shenadoah一样,几乎所有运行阶段都和用户线程并发进行。其中同样包含初始标记、重新标记等STW的过程,作用相同,不再赘述。重点介绍以下四个并发阶段:

并发标记
并发标记阶段和G1相同,都是遍历对象图进行可达性分析,不同的是ZGC的标记在染色指针上。

并发预备重分配
在这个阶段,ZGC会扫描所有Region,如果哪些Region里面的存活对象需要被分配的新的Region中,就将这些Region放入重分配集中。

此外,JDK12后ZGC的类卸载和弱引用的处理也在这个阶段。

并发重分配
ZGC在这个阶段会将重分配集里面的Region中的存货对象复制到一个新的Region中,并为重分配集中每一个Region维护一个转发表,记录旧对象到新对象的映射关系。

如果在这个阶段用户线程并发访问了重分配过程中的对象,并通过指针上的标记发现对象处于重分配集中,就会被读屏障截获,通过转发表的内容转发该访问,并修改该引用的值。

ZGC将这种行为称为自愈(Self-Healing),ZGC的这种设计导致只有在访问到该指针时才会触发一次转发,比Shenandoah的转发指针每次都要转发要好得多。

另一个好处是,如果一个Region中所有对象都复制完毕了,该Region就可以被回收了,只要保留转发表即可。

并发重映射
最后一个阶段的任务就是修正所有的指针并释放转发表。

这个阶段的迫切性不高,所以ZGC将并发重映射合并到在下一次垃圾回收循环中的并发标记阶段中,反正他们都需要遍历所有对象。

性能测试

TP99(第99百分位数,99th Percentile)是一个统计术语,用于描述在一组数据中,有99%的数据点小于或等于这个值。

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器

在G1、Shenandoah或者ZGC这些越来越复杂、越来越先进的垃圾收集器相继出现的同时,也有一 个“反其道而行”的新垃圾收集器出现在JDK 11的特征清单中——Epsilon,这是一款以不能够进行垃圾 收集为“卖点”的垃圾收集器。

原因是“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是本书为这一部分 所取的标题——“自动内存管理子系统”。

在实际生产环境中,不能进行垃圾收集的Epsilon也仍有用武之地。

Epsilon也是有着类似的目标,如果读者的应用只要运行数分钟甚至数秒, 只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为 的Epsilon便是很恰当的选择。

3.7.2 收集器的权衡

我们应该如何选择一款适合自己应用的收集器呢?这个问题的答案主要受以下三个因素影响:

  • 应用程序的主要关注点是什么?
  • 运行应用的基础设施如何?
  • 使用JDK的发行商是什么?版本号是多少?是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑 或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?

3.7.3 虚拟机及垃圾收集器日志

1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc:

2)查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc, 用通配符将GC标签下所有细分过程都打印出来,如果把日志级别调整到Debug或者Trace(基于版面 篇幅考虑,例子中并没有),还将获得更多细节信息:

3)查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之 后使用-Xlog:gc+heap=debug:

4)查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+Print GCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog: safepoint:

5)查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收 集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后 使用-Xlog:gc+ergo*=trace:

6)查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution, JDK 9之后使用-Xlog:gc+age=trace:

GC日志

3.7.4 垃圾收集器参数总结

补充JVM配置参数

JVM配置参数 - 简书 (jianshu.com)

根据jvm参数开头可以区分参数类型,共三类:“-”、“-X”、“-XX”,

标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容;

例子:-verbose:class,-verbose:gc,-verbose:jni……

-version :查看 Java 版本

-help :查看 java 命令的使用帮助

非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;

例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k……

非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;

高级参数(Advanced Options)

例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps……

开发人员使用最多的参数,用于 JVM 调优和 debug,不同的 JDK 版本可能会发生变化,这些参数以 “-XX” 开头。

“-XX” 参数有 2 种类型:Boolean 类型和需要参数的类型。

Boolean 类型:用于启用默认情况下禁用的功能,或者禁用默认情况下启用的功能,此类选项不需要参数。

格式:-XX:[+-] , “+” 或 “-” 表示启用或者禁用 OptionName 属性。

-XX:+UseConcMarkSweepGC 表示启用 CMS 垃圾收集器

-XX:+UseG1GC 表示启用 G1 垃圾收集器

-XX:+PrintCommandLineFlags 表示启用打印 JVM 设定的值,例如堆空间大小和选定的垃圾收集器

需要指定参数值的类型:用于指定某个参数为某个值。

格式:-XX:=,表示 OptionName 属性的值是 value。

-XX:ThreadStackSize=size 设置线程堆栈大小(以字节为单位),字母“k”或“K”表示千字节,“m”或“M”表示兆字节,“g”或“G”表示千兆字节。“-XX:ThreadStackSize ”等效于 “-Xss”。

-XX:InitialHeapSize=size 设置内存分配池的初始大小(以字节为单位),此值必须为 0 或 1024 的倍数且大于 1 MB,“-XX:InitialHeapSize”等效于 “-Xms”。

-XX:MaxHeapSize=size 设置内存分配池的最大大小(以字节为单位),此值必须是 1024 的倍数且大于 2 MB,“-XX:MaxHeapSize ”等效于 “-Xmx”。

-XX:MaxGCPauseMillis=time 设置最大 GC 暂停时间的目标(以毫秒为单位)

轻松永远记住经典jvm参数 (baidu.com)

科普文:一文搞懂jvm实战(五)通用JVM参数配置_jvm 内存设置-CSDN博客

[转帖]JVM参数配置及调优_51CTO博客_jvm配置和调优参数

3.8 实战:内存分配与回收策略

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

3.8.1 对象优先在Eden分配

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

3.8.2 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组,本节例子中的byte[]数组就是典型的大对象。大对象对虚拟机的内存分配来说 就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对 象”,我们写程序的时候应注意避免。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易 导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复 制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。

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

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存 活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中(详见第2章)。对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

3.8.4 动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到 XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。

3.8.5 空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看 XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无 法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代 本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与 老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对 象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实 地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下 都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。。参见代码清单3-11,请读 者先以JDK 6 Update 24之前的HotSpot运行测试代码。

3.9 本章小结

本章介绍了垃圾收集的算法、若干款HotSpot虚拟机中提供的垃圾收集器的特点以及运作原理。通 过代码实例验证了Java虚拟机中自动内存分配及回收的主要规则。

垃圾收集器在许多场景中都是影响系统停顿时间和吞吐能力的重要因素之一,虚拟机之所以提供多种不同的收集器以及大量的调节参数,就是因为只有根据实际应用需求、实现方式选择最优的收集 方式才能获取最好的性能。没有固定收集器、参数组合,没有最优的调优方法,虚拟机也就没有什么 必然的内存回收行为。因此学习虚拟机内存知识,如果要到实践调优阶段,必须了解每个具体收集器 的行为、优势劣势、调节参数。

自己的总结:

收集器从一开始的单线程到多线程,再到并发,都是为了让收集内存时的Stop The World时间更短。

因为吞吐量、收集时间、内存占用是不可能都达到的。

随着技术的发展,内存也占用越高,技术发展可以提高吞吐量,但是会提升垃圾收集时间(内存越大,收集的垃圾越多,耗时越长),所以现在的垃圾收集器更关注去降低垃圾收集时间。

并发的垃圾收集器虽然可以和工作线程一起运行,延时降低了,但是整个GC的并发收集周期都会很长。
例如目前最先进的Shenandoah、ZGC,虽然所有的步骤基本都是并发的,但是整个GC的并发收集周期都会很长。
并发的目标是本着内存收集的频率能满足内存分配的频率,这样就永远有内存可以用。
收集的频率无法满足内存分配的频率时的解决策略:

  1. 是尽可能地增加堆容量大小,获得更多喘息的时间。
  2. 引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。
自己的收集器总结:

初始串行阶段:Serial收集器 Serial Old收集器
并行阶段:ParNew收集器 Parallel Scavenge收集器 Parallel Old收集器
并发阶段:

  • CMS收集器(标记-清除算法,并发标记问题解决使用增量更新,并发清除)
  • G1收集器(标记-整理算法、复制算法,并发标记问题解决使用原始快照SATB,多线程并行回收整理)
  • Shenandoah(标记-整理算法、复制算法,并发标记问题解决使用原始快照SATB,
    • 并发回收整理:转发指针(旧引用访问复制后的对象)、连接矩阵(分析和更新引用))
  • ZGC收集器(标记-整理算法、复制算法,并发标记问题解决使用原始快照SATB(有待确认),
  • 并发回收整理:重分配集(用来判定回收哪些Region)、读屏障(ZGC使用了读屏障来完成指针的“自愈”)、染色指针(旧引用访问复制后的对象)、内存多重映射(操作系统支持)、NUMA(ZGC会优先在自己的本地内存上分配对象))

标签:03,收集器,对象,内存,GC,引用,JVM,垃圾
From: https://www.cnblogs.com/hellodeyang/p/18579794

相关文章

  • JVM学习-02-Java内存区域与内存溢出异常
    第二章、Java内存区域与内存溢出异常2.1概述介绍Java虚拟机内存的各个区域讲解这些区域的作用、服务对象以及其中可能产生的问题2.2运行时数据区域2.2.1运行时数据区域程序计数器:当前线程所执行的字节码的行号指示器,每条线程都需要有一个独立的程序计数器(线程私有),......
  • JVM调优实战
    JVM调优实战1.常用命令jpsjps-ml显示java进程信息jinfo进程号显示这个进程的运行的java程序的信息jstat-gc进程号显示内存每块区域占用的大小jstat-gc进程号时间每时间(ms)显示当前内存每块区域占用的大小jstack进程号显示进程号里所有的线程信息jm......
  • task03 变量与函数
    一.变量变量是一个名字,它所指代的是一段数据使用=来对这段数据的区域进行赋值新的值会覆盖旧的值新值的数据类型不必与旧值相同2.变量命名规则必须又字母或下划线开头命名可由字母数字和下划线组成大小写敏感尽量避免使用保留字命名3.更新变量4.多变量赋值二.函数函......
  • 洛谷 P1036 [NOIP2002 普及组] 选数 C语言
    题目:https://www.luogu.com.cn/problem/P1036题目描述已知 nn 个整数 x1,x2,⋯ ,xn,以及 1 个整数 k(k<n)。从 n 个整数中任选 k 个整数相加,可分别得到一系列的和。例如当 n=4,k=3,4 个整数分别为 3,7,12,19时,可得全部的组合与它们的和为:3+7+12=223+7+19=297+12......
  • 03os模块的一些总结
    一、使用pycharm创建python项目首先创建一个文件夹来存放我后面要写的python项目创建一个day16来存放今天写的python项目开始编写代码吧!!二、os模块先设置一下字体os中的system是用来执行系统命令的,在windows中查看ip的命令是ipconfig,如果使用os.system在windows中......
  • JVM相关
    JVM相关JVM组成什么是程序计数器总结详细介绍Java堆总结介绍方法区总结直接内存总结虚拟机栈垃圾回收是否涉及栈内存栈内存分配越大越好吗?方法内的局部变量是否线程安全什么情况下导致栈内存溢出?总结堆栈的区别是什么?类加载器什么是类......
  • 学期:2024-2025-1 学号:20241303 《计算机基础与程序设计》第10周学习总结
    作业信息这个作业属于哪个课程<班级的链接>(如2024-2025-1-计算机基础与程序设计)这个作业要求在哪里<作业要求的链接>(如2024-2025-1计算机基础与程序设计第十周作业)这个作业的目标<写上具体方面>计算机科学概论(第七版)第12,13,14章并完成云班课测试,《C语言程序设计......
  • jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
    拓展阅读JVMFULLGC生产问题I-多线程通用实现JVMFULLGC生产问题II-如何定位内存泄露?线程通用实现JVMFULLGC生产问题III-多线程执行队列的封装实现,进一步抽象jvisualvmjava性能分析工具jvm-44-jvm内存性能分析工具EclipseMemoryAnalyzerTool(MAT)/内存......
  • ADI公司推出多款用于无线通信设备和数据处理的 ADUM7703-8BRIZ/ADL6010SCPZNHMC342LC4
    ADI公司推出多款用于无线通信设备和数据处理的ADUM7703-8BRIZ/ADL6010SCPZNHMC342LC4TR/ADAU1772BCPZ器件ADAU1772BCPZ是一款具有四个输入和两个输出的编解码器,内置数字处理引擎,可执行滤波、电平控制、信号电平监控和混合。从模拟输入至DSP内核再到模拟输出的路径经过优化,实现......
  • 【人人都能学得会的NLP - 文本分类篇 03】长文本多标签分类分类如何做?
    【人人都能学得会的NLP-文本分类篇03】长文本多标签分类分类如何做?NLPGithub项目:NLP项目实践:fasterai/nlp-project-practice介绍:该仓库围绕着NLP任务模型的设计、训练、优化、部署和应用,分享大模型算法工程师的日常工作和实战经验AI藏经阁:https://gitee.com/fas......