JVM常见的垃圾回收器

发布于:2024-04-29 ⋅ 阅读:(29) ⋅ 点赞:(0)

1、回收方法区:

        方法区回收价值很低,主要回收废弃的常量和无用的类。

方法区中的存储:
    方法区中存储的是加载的类的信息,常量,静态变量,即时编译后的代码等数据,所以回收的对象也就是这些内容。

何种场景需要回收:
    既然回收方法区不是必须的,虽然效率低下,但是当内存不够使用的时候依然是会抛出OOM的,那么我们需要知道什么场景下需要去回收方法区。首先我们需要弄明白方法区会回收那些对象。在JDK1.7之前,常量池是在方法区中的,在此版本及以后则将其移到堆中。基于目前版本主要是1.8及以上,故我们以1.8为准。在此版本上,主要回收的是无用的类。如何判定一个类是无用的:
 

  • 无用的类回收有以下条件:

        ①JAVA实例已经全部被回收,在堆中没有该类的示例存在;

        ②该类的classLoader被回收;

        ③该类的java.long.class对象没有被引用,不会被其他方法通过反射访问该类的方法

        与堆中的对象回收机制不同,不是不用即回收。HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。
 

2、垃圾回收器:

Serial、Serial Old、PawNew、CMS、Parallel Scavenge、Parallel Old、G1

  • Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
  • Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

CMS垃圾收集器
 

CMS(并发标记清除收集器)(Concurrent Mark Sweep) 

        CMS是基于“标记-清除"算法实现的、以最短停顿时间为目标的垃圾收集器,CMS垃圾回收器主要针对老年代进行垃圾回收

        CMS回收垃圾的整个过程分为以下四个阶段:

                即初始标记阶段、并发标记阶段、重新标记阶段 和 并发清除阶段。

                (涉及STW,暂停用户线程的阶段主要是:初始标记 和 重新标记)

        CMS垃圾收集器在并发标记阶段并发清除阶段能让用户线程和GC线程并发执行,因此在整个垃圾收集过程中用户不会感到明显的卡顿:

  •         初始标记(Initial-Mark)阶段: 初始标记阶段会暂停所有的用户线程, 单线程标记与GC Roots直接关联的对象, 单线程标记是为了确保标记过程的准确性和一致性。这个阶段的主要任务仅仅只是标记出与GCRoots能直接关联到的对象。一旦标记完成之后就会立即进入并发标记阶段,恢复之前被暂停的所有用户线程。此外,由于与GC Roots直接关联对象比较少,所以初始标记阶段的速度非常快。
  •         并发标记(Concurrent-Mark)阶段:在并发标记阶段中,程序的工作线程会和垃圾回收线程并发执行或者交叉执行,垃圾回收线程会进行可达性分析,会从GC Roots直接关联的对象开始向下遍历堆中的对象图,标记所有可达的对象 。这个过程耗时较长但是不需要停顿用户线程。
  •         重新标记(Remark)阶段:由于工作线程在并发标记阶段可能会造成对象的引用发生变化,即原本可达的对象变成了垃圾对象,不可达对象变成了可达对象,所以重新标记阶段垃圾回收器会暂停所有工作线程,对在并发标记阶段中发生变化的对象进行重新标记、修正
    • 注意只能修正原有对象不能修正新增对象,即只能修正原有对象非可达变可达、可达变非可达。
  •         并发清除(Concurrent-Sweep)阶段:清理在标记阶段判断已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

CMS缺点:

  1. 内存碎片问题:CMS 使用的是标记-清除算法,它会在回收垃圾对象之后产生大量的内存碎片。当老年代空间碎片太多时,如果无法找到一块足够大的连续内存存放对象时,将不得不提前触发一次Full GC。 

  2. 无法处理浮动垃圾:并发标记阶段用户线程可能会产生新对象,重新标记阶段只能修正原有对象非可达变可达、可达变非可达,不能新增,那么这些新增对象有可能在并发标记和并发清理阶段变为浮动垃圾对象,CMS 无法直接处理这些浮动垃圾,需要等到下一次 GC 才能回收。

  3. 并发标记引起的CPU资源紧张:在并发标记阶段,用户线程与垃圾回收线程并发执行,这可能会增加一定程度的 CPU 消耗

  4. 回收时要确保用户线程有足够内存:不能等老年代满了再回收,而是老年代内存到达某个阈值后回收,防止用户线程在并发执行过程中新创建对象导致内存不够,导致虚拟机补偿使用Serial Old收集器进行回收并处理内存碎片,从而浪费更多时间。默认情况下,当老年代使用了92%的空间后就会触发CMS垃圾回收机制。可以通过-XX:CMSInitiatingOccupancyFraction来设置 

  • 优点:
    • 并发速度快;
    • 低停顿:仅初始标记和重新标记阶段需要停顿用户线程,这两个阶段运行速度很快,CMS垃圾收集器在并发标记阶段并发清除阶段能让用户线程和GC线程并发执行,因此在整个垃圾收集过程中用户不会感到明显的卡顿。

演示非可达变为可达:

        在 main 方法中,首先创建了一个对象 obj,并将其设置为 null,使其成为不可达状态。然后,在后续代码中,通过 new 关键字重新创建了一个 MyClass 对象,并将其赋值给 obj,使得 obj 变为了可达状态。在这个过程中,就实现了非可达变可达的情况。

public class NonReachableToReachableExample {
    
    static class MyClass {
        // 类中的成员变量
        int data;
        
        // 类的构造函数
        MyClass(int data) {
            this.data = data;
        }
    }

    public static void main(String[] args) {
        // 创建一个 MyClass 对象并赋值给变量 obj,初始时它是不可达的
        MyClass obj = null;
        
        // 在并发标记阶段后,某个用户线程重新获取了对 obj 的引用,使得 obj 变为可达
        obj = new MyClass(10);
        
        // 在重新标记阶段,obj 被标记为可达
        // 在此之前,obj 是不可达的,但由于用户线程的活动,它变为了可达
        // 因此,在重新标记阶段需要修正这个状态
    }
}

G1垃圾回收器:

        G1(Garbage-First,垃圾优先收集器):

  •         G1垃圾回收器不再把堆划分为连续的分代,而是将堆内存分割成2048个大小相等的Region,各Region根据需要扮演伊甸园区、幸存区、老年代区、巨大区Humongous Region。垃圾优先收集器跟踪各Region里垃圾的回收价值(回收空间大小和预计回收时长),在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,回收优先级最高的那些Region,以达到垃圾优先的效果。
  • 设置最大停顿时间:-XX:MaxGCPauseMillis=默认0.2s
  •          G1从整体来看是基于标记-整理算法实现的回收器,但从局部(两个Region之间)上看又是基于复制算法实现的。
  •         G1在JDK9之后成为了默认的垃圾回收器,取代了Parallel Scavenge 、Parallel Old默认组合,而CMS被声明为不推荐使用的垃圾回收器。  

步骤:

  •         初始标记:初始标记阶段会暂停所有的用户线程, 单线程标记与GC Roots直接关联的对象, 单线程标记是为了确保标记过程的准确性和一致性。这个阶段的主要任务仅仅只是标记出与GCRoots能直接关联到的对象。
  •         并发标记:在并发标记阶段中,程序的工作线程会和垃圾回收线程并发执行或者交叉执行,垃圾回收线程会进行可达性分析,从GC Roots直接关联的对象开始向下遍历堆中的对象图,标记所有可达的对象 。这个过程耗时较长但是不需要停顿用户线程。
  •         最终标记:重新标记所有存活的对象和上个阶段用户线程新产生的可达对象。并发停顿。采用SATB算法,效率比CMS重新标记高。并发停顿。
  •         筛选回收:根据优先级列表,回收价值高的一些Region,将存活对象通过标记复制算法复制到同类型的空闲Region。根据指定的最大停顿时间回收,因此可能来不及回收所有垃圾对象,但能保证回收到最高回收价值的垃圾。并发停顿。

初始标记阶段会暂停所有的用户线程, 单线程标记与GC Roots直接关联的对象, 单线程标记是为了确保标记过程的准确性和一致性。这个阶段的主要任务仅仅只是标记出与GCRoots能直接关联到的对象。一旦标记完成之后就会立即进入并发标记阶段,恢复之前被暂停的所有用户线程。此外,由于与GC Roots直接关联对象比较少,所以初始标记阶段的速度非常快。

Minor GC与Full GC分别在什么时候发生?

        新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC

引用的分类:

        强引用:GC时不会被回收

        软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收

        弱引用:描述有用但不是必须的对象,在下一次GC时被回收

        虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知。
 

 你知道哪些JVM性能调优:

设定堆内存大小

-Xmx:堆内存最大限制。

设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代

-XX:NewSize:新生代大小

-XX:NewRatio 新生代和老生代占比

-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比

设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

JVM的永久代中会发生垃圾回收么?

        垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

类的生命周期:
     

   类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图:

加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象

连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、符号引用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用转换为直接引用

初始化,为类的静态变量赋予正确的初始值

使用,new出对象程序中使用

卸载,执行垃圾回收
 

 Java对象创建过程

1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)

2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”

3.将除对象头外的对象内存空间初始化为0

4.对对象头进行必要设置
 

对象分配规则:

        对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

        大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

        长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

        动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

        空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
 


网站公告

今日签到

点亮在社区的每一天
去签到