JVM详解

本文详细介绍了JVM的工作机制,包括类加载过程(加载、连接、初始化)、双亲委派机制及其作用,以及对象的加载步骤。探讨了JVM的垃圾回收机制,如新生代、老年代的回收策略,如Minor GC和Major GC,以及可达性分析法和引用类型。还讨论了对象的finalization机制,垃圾收集器(如Serial、Parallel Scavenge、CMS、G1等)的选择与应用场景。此外,文章还提到了内存分配、并发安全问题以及JVM如何解决这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

本文主要是关于jvm的一些个人理解,如果文中用词或者理解方面出现问题,欢迎指出。此文旨在分享jvm的整体知识,对于细节方面不会去深究,若有细节方面的问题,欢迎指出

JVM

类的加载过程

  • 类加载过程
  • 加载-》连接-》初始化
  • 其中连接又可分为 检查-》准备-》解析
  • 对象加载过程
  • 类加载检查-》分配内存-》初始化零值-》设置对象头-》执行init方法
  • TODO

双亲委派机制分析

在这里插入图片描述一个类加载进来的时候,

  • 首先,检查请求的类是否已经被加载过,是则直接加载,返回类
  • 不是则向上抛给父加载器,父加载器加载到了就返回该类,没有加载到则调用自身加载器加载,如果最下层的加载器都加载不到该类,就会报ClassNotFoundException
  • 补充:NoClassDefFoundError
    常见的场景就是:
    1、类依赖的class或者jar不存在
    2、类文件存在,但是存在不同的域中
    3、大小写问题,javac编译的时候是无视大小的,很有可能你编译出来的class文件就与想要的不一样!这个没有做验证。
  • 当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
  • 另外,有趣的一点是父子加载器之间的关系并不是靠继承决定的,而是看他们的“优先级”。

双亲委派机制的作用

  • 在这之前我们需要知道JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类,比如有两个object类,ExtensionClassLoader与AppClassLoader加载的类在jvm看来是不同的

  • 如此,双亲委派机制就可以保证 Java 的核心 API 不被篡改,可以避免类的重复加载。

  • 如果想自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

  • 注:为什么会有自定义类加载器?

    1. 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
    2. 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
      相关源码
 private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }
                
                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

对象的加载过程

在这里插入图片描述补充内容

  • 对象在内存的表现形式:
- 仅针对 Hotspot 虚拟机,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据(即方法区中的类信息)的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
摘自[javaguide](https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F?id=step1%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%A3%80%E6%9F%A5)
  • 对象的访问定位
    句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
    直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
	优缺点:这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

在这里插入图片描述

  • 分配方式详解
    参考链接:指针碰撞 空闲列表
    1.指针碰撞
    适用于堆内存完整的情况,已分配的内存和空闲内存分表在不同的一侧,通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
    在这里插入图片描述

    2.空闲列表
    适用于堆内存不完整的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS。

在这里插入图片描述

  • JVM是如何解决分配内存时并发安全问题的
   1.TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
   2.CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

jvm的垃圾回收

为什么要将内存区域划分为新生代和老年代
答:从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

补充

  • Eden 区->Survivor 区后对象的初始年龄变为 1 ,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
  • 对象晋升到老年代的年龄阈值:Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。
  • 分配担保机制 影响:大对象直接进入老年代 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 为什么要这样呢? 为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
  • 引用类型:强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1.强引用(StrongReference)
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,当内存空间不足,Java 虚拟机宁愿抛OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)
如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
3.弱引用(WeakReference)
如果一个对象只具有弱引用,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
软引用与弱引用都可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中
4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须(其他是可以)和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
  • 针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
    1.部分收集 (Partial GC):
    1.1新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
    1.2老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    1.3混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

    2.整堆收集 (Full GC):收集整个 Java 堆和方法区。

执行一次垃圾回收的流程

  • 判断对象是否死亡
  1. 引用计数法(未实际使用):引用计数法就是在对象上维护一个引用计数器,有多少个引用这个对象就有多少个数,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。但是它有一个缺陷,就是如果存在循环引用会导致无法回收循环引用的对象,简单来说就是两个对象互相有对方的引用,假如A对象有b(指向B对象的引用),B对象有a(指向A对象的引用),这样会导致它们的引用计数器一直不能为0,就会导致问题出现。
  2. 可达性分析法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
    可作为 GC Roots 的对象包括下面几种:
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象:本地变量表存储方法中的参数和局部变量,例如
    public void a(B b){
    //c就是局部变量,也叫指向129这个对象的引用
    int c = 129;
    }
    所以基于这个方法,传进来的b对象(不是引用)和129这个对象可以作为GC Roots
  • 本地方法栈(Native 方法)中引用的对象
    在用Native修饰的方法中存储的的参数以及局部变量对象也可以作为GC Roots
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
 ```final static final staitic修饰的字段分别存储在方法区中的常量池、理论上·是方法区中的静态区(JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中)、跟static存储位置一样,final和final static的区别就是在类加载的时候在准备阶段,会将static字段设置成默认值(零值),而final static会被设置成程序中赋予的值
- 在方法区中有个常量池,里面存储了不少常量引用,通过解析这些引用可以获得对象,```

5.所有被同步锁持有的对象

不可达的对象就一定会被清除吗?

  • 即使在可达性分析法不可达从的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;
  • 可达性分析法中不可达(没有引用链)的对象被第一次标记,之后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行,也就是说当对象覆盖了finalize方法并且这个方法没有被虚拟机调用虚拟机才会视为必要执行。
  • 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,在此期间,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
  • (使用标记清理算法)可达性分析法的流程:一个对象重写了了finalize方法去自定义一些资源的释放–》从一系列GC Roots节点中追溯对象,当没有引用链的对象会被标记并执行一次筛选,如果筛选出这个对象的finalize方法被重写过并且虚拟机没有调用过finalize方法,会将这个对象放进队列中,这个时候对象的状态是可复活状态,之后由一个低优先级的,虚拟机创建的Finalizer线程触发其finalize()方法,如果在执行finalize()方法中,这个对象与引用链上的任何一个对象建立了联系,那么它会被移除这个"即将回收"集合,这个时候,被移出"即将回收"集合的对象不会再执行finalize()方法,对象会变成’‘不可触及’'的状态。否则会出队进入即将回收集合,进入集合中的对象会被第二次标记,这代表它真的会被回收–》Collectot(垃圾收集器)对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记可达对象,则将其回收。
  • 注意:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表中。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放,如果不够,就简单的将内存标记为可用的。

对象的finalization机制

  • Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑(比如可以重写此方法去释放io资源);
  • 当可达性分析法发现没有引链用指向一个对象,即:垃圾收集此对象之前,总会先调用这个对象的finalize()方法;
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放的工作,比如关闭文件、套接字和数据库连接。
  • 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,
  • 在finalize()时可能会导致对象复活;
  • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会;
    一个糟糕的finalize()会严重影响GC的性能;
  • 由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:
    1. 可触及的:从根节点开始,可以到达这个对象;
    2. 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活;
    3. 不可触及的:对象的finalize()被调用,并且没有复活,那么就进入不可触及状态。不可触及的对象是不可能被复活,因为finalize()方法只会调用一次。
    扩展:从思路上来看,可达性分析算法很简单,就两步,第一步找出GC Roots,第二步从GC Roots开始向下遍历整个对象图。但在真正的实现中,还是有很多地方值得注意的。
​ 就拿根节点枚举来说。整个方法区那么多类,常量信息,若一个一个来检查那些可做GC Roots,耗费的时间肯定不少,所以在HotSpot虚拟机中就通过一组OopMap的数据结构来记录哪些位置是引用,在类加载完成后就将哪个对象内什么偏移量上是什么数据类型计算出来,这样收集器就可以直接得知这些信息,而不需要依次遍历整个方法区。
​ 而第二步从GC Roots向下遍历对象图则有很多优化措施,例如,如何让用户线程与垃圾收集线程并发运行?并发运行会产生什么问题?有哪些解决方案?
​ 对于CMS收集器是通过增量更新来解决并发标记问题的,而G1,ShenanDoah垃圾收集器则是通过原始快照来(Snapshot At The Beginning,SATB)解决并发标记问题。这两种解决方案分别是破坏了会导致并发标记产生问题的两个必要条件之一。至于这两个必要条件是啥,可参考三色标记理论。
  • 垃圾收集算法

  1. 标记-清除
    - 执行过程:
    当堆中的有效内存空间被耗尽的时候,就会停止整个程序(Stop The World),然后进行两项工作,第一项是标记,第二项是清除。
    标记:Collector从引用根节点开始遍历,标记所有被引用的对象(这里的标记要看用什么标记算法),一般是在对象头中记录为可达对象;
    清除:Collectot对堆内存从头到尾进行线性的遍历,如果发现某个对象头没有标记可达对象,则将其回收。
    缺点:
    效率不算高;
    在进行GC时候,需要停止整个应用程序;
    这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表;

  2. 标记-复制
    - 执行过程:
    jvm将内存一份为二,保证有一份内存是可用的,垃圾回收时标记正在使用的内存中的存活对象,之后将对象复制到另一内存,之后清除使用中的内存

    • 优点:
      没有标记-清除的过程,实现简单,运行高效
      复制过去以后保证空间的连续性,不会出现碎片问题

    • 缺点:
      需要两倍的内存空间
      需要维护对象的引用关系//TODO

  3. 标记-压缩
    背景:
    复制算法的高效性是建立在存活对象少、垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象,如果依然使用复制算法,由于存活对象多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
    标记-清除算法的确可以应用在老年代,但是该算法不仅执行效率低下,而且在执行完内存回收后,还会产生内存碎片,所以JVM的设计者需要在此基础上进程改进,标记-压缩算法由此诞生。
    执行过程:
    第一阶段和标记-清除算法一致,从根节点开始标记所有被引用对象。
    第二阶段将所有的存活对象压缩到内存的另一端,按顺序排放。
    之后,清理边界外所有空间。
    二者的本质差异在于标记清除算法是一种非移动式的回收算法,标记压缩是移动式的。
    可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
    小结
    在这里插入图片描述

  4. 分代收集算法

    • 分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)(JDK8被更新为元空间)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。这也是HotSpot 为什么要分为新生代和老年代。

      4.1 年轻代(Young Generation)的回收算法

      • 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

      • 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空(为了能够有一个空内存能够存储eden区和0区的对象,这也是复制算法的核心), 如此往复。
        在这里插入图片描述

      • 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

      • 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

    4.2 老年代(Old Generation)的回收算法

    	a)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    	
    	b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
    

    4.3 持久代(Permanent Generation)/元空间的回收算法
    无论是持久代还是元空间都是对方法区的一种实现,方法区主要回收的内容有:废弃常量和无用的类

    • 判断废弃常量:如果一个运行时常量池中的常量没有任何被引用的话,就视为废弃常量,并且如果jvm占用内存率过高,就会被清理出去。例如:假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。

    • 判断废弃类:

      • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
      • 加载该类的 ClassLoader 已经被回收。
      • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
        虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
      

垃圾收集器

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

  • 没有最好的收集器,只有最适合的收集器,每个收集器都有各自适应的场景,只有根据场景和业务需求,去寻找到一个最合适的收集器。

  1. Serial 收集器
	Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

	新生代采用标记-复制算法,老年代采用标记-整理算法。
	
	Serial 收集器
	
	虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
	
	但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
  1. ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

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

ParNew 收集器

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

并行和并发概念补充:

    并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

    并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
  1. Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?

-XX:+UseParallelGC

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

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

Parallel Scavenge 收集器

这是 JDK1.8 默认收集器

使用 java -XX:+PrintCommandLineFlags -version 命令查看

-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
  1. Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
  1. Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
  1. CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

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

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

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

CMS 垃圾收集器

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

    对 CPU 资源敏感;
    无法处理浮动垃圾;
    它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
  1. 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 收集器的运作大致分为以下几个步骤:

    初始标记
    并发标记
    最终标记
    筛选回收

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
  1. ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

在 ZGC 中出现 Stop The World 的情况会更少!

详情可以看 : 《新一代垃圾回收器 ZGC 的探索与实践》

参考博客:https://snailclimb.gitee.io/javaguide/#/docs/java/jvm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值