Java-JVM-垃圾回收

参考文章:

jvm垃圾回收器-G1 ZGC篇

G1与ZGC

一文彻底搞懂八种JVM垃圾回收器

Java GC 面试终结者

java堆中的新生代的s区满了会触发young gc吗 jvm 新生代

0.是什么

垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一项功能,用于自动管理内存。

其主要任务是回收不再被引用的对象所占用的内存,以便该内存可以被重新分配和使用。

1.为什么

咱们的对象创建了后,不用了,这个时候不清理调的话,一直占着内存,久而久之,内存就不够用了。

  • 自动内存管理:在Java中,程序员不需要手动管理内存分配和释放,这减少了内存泄漏和其他内存管理错误的风险。

  • 减少内存泄漏:垃圾回收器会定期检查堆内存,回收不再被引用的对象,从而防止内存泄漏。注意,是帮忙,而不是必定能杜绝。

  • 提高程序稳定性:自动管理内存减少了程序崩溃和其他不稳定因素,因为它能确保已分配的内存最终会被释放。

  • 简化编程:程序员可以专注于业务逻辑,而不必担心内存管理的细节,从而提高开发效率。

2.STW

STW(Stop-The-World)是指在某些情况下,JVM会暂停所有应用程序线程以执行垃圾回收或其他维护任务。

什么叫暂定,暂停就是你那些别的玩意必定阻塞。

举例下:假设请求进来了,我要清理垃圾,你处理请求的那个线程给我先等着,我垃圾回收一千个小时,你也给我等着,我搞完你再继续弄。

所以,垃圾回收的一个关键点也在于,如何减少STW时长。

嗯,咱们的Java程序卡顿很明显,也有可能是在频繁的GC,频繁的STW,导致停顿。

3.垃圾回收背景

3.1 GC范围

JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

img

3.1.1 堆空间

对象创建出来,是放在堆空间的。而垃圾回收,就主要回收堆空间里的。

注意,对象存在一个年龄,每次gc存活后,年龄会+1。

下面是JDK8的一个堆空间示意图,注意两个默认值。

  • 新生代:老年代 = 1:2(新生代默认是老年代的一半)
  • e:s0:s1 = 8:1:1
# 看一个jvm参数
java -Xms3g -Xmx3g -XX:NewRatio=2 -XX:SurvivorRatio=8 -jar myapp.jar
    
-Xms3g 设置堆的初始大小为3GB。
-Xmx3g 设置堆的最大大小为3GB。
-XX:NewRatio=2 设置新生代与老年代的比例为1:2。
-XX:SurvivorRatio=8 设置Eden区与每个Survivor区的比例为8:1。

image-20240527205227721

区域名称 存放对象描述
Eden区 (E) 大部分新创建的对象
Survivor区 (S0) 从Eden区复制过来的存活对象,S0和S1轮换使用
Survivor区 (S1) 从Eden区复制过来的存活对象,S0和S1轮换使用
老年代 (O) 从Survivor区晋升过来的长期存活对象

3.1.2 方法区

元空间(Metaspace)是JVM用于存储类元数据的内存区域。

嗯,之前的文章提到了,想什么静态变量、常量之类的,都存在这里了,全局共享。

  • 类元数据:包括类的字段、方法、字节码、常量池等。

  • 方法元数据:包括方法的参数、局部变量、字节码等。

  • 类加载器元数据:包括类加载器的信息。

  • 运行时常量池:包括类和接口的常量池。

一般情况下,这里面的东西是不会回收的,但是呀,你的静态变量、常量之类的,它总归写在一个类里面的。

还有个典型的现象,就是常量池里的东西,它跟类没关系,但是这个常量池里的某些数据,它可能没人用了。

  • 类卸载:当一个类不再被使用,并且其所有的实例都已被回收,JVM可以卸载该类。卸载类时,其元数据会被从元空间中回收。

  • 类加载器卸载:如果一个类加载器及其加载的所有类都不再被引用,那么该类加载器及其加载的所有类的元数据都会被回收。

  • 废弃常量

    String str="abc";
    str=null;
    // str不用这个"abc"了,其它地方也没人用这个。
    // 如果此时发生GC并且有必要(内存不够用)的话,这个"abc"常量有可能被回收,清理出常量池。
    

3.2 GC对象

额,那么,谁是垃圾?

对于JVM来说,当对象不再被任何活动线程或其他对象引用时,就认为这些对象是“垃圾”,可以被垃圾回收器回收。


先来个简单的例子大概认识下什么是垃圾,在2.2.2 垃圾识别算法再介绍2个识别垃圾的主要算法:引用计数法和可达性分析法。

public class GCDemo {
    private static class Node {
        int value;
        Node next;

        Node(int value) {
            this.value = value;
        }
    }

    public static void main(String[] args) {
        // 创建一些节点对象
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);

        // 构建链表:node1 -> node2 -> node3 -> node4
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;

        // 打破引用:node1 -> node2, node3 -> node4 (node2不再指向node3)
        node2.next = null;

        // 使node1不再被引用
        node1 = null;

        // 请求垃圾回收
        System.gc();
    }
}

现在从内存结构层面分析一下。

  • 创建对象并建立引用

在堆区分配 Node 对象

Node@1 (value=1, next=Node@2)
Node@2 (value=2, next=Node@3)
Node@3 (value=3, next=Node@4)
Node@4 (value=4, next=null)

在虚拟机栈的 main 方法栈帧中,有局部变量 node1, node2, node3, node4 分别指向堆中的对象。

image-20240528122427497

  • 打破引用

设置 node2.next = null,使得 Node@2 不再指向 Node@3

Node@1 (value=1, next=Node@2)
Node@2 (value=2, next=null)
Node@3 (value=3, next=Node@4)
Node@4 (value=4, next=null)

image-20240528122702988

  • 移除根引用

设置 node1 = null,此时虚拟机栈中的局部变量 node1 不再指向任何对象。

null
Node@2 (value=2, next=null)
Node@3 (value=3, next=Node@4)
Node@4 (value=4, next=null)

image-20240528122742588

  • 提示gc
        // 请求垃圾回收
        System.gc();

此时呢,这个短暂的瞬间,node1是没有指向的,这个就是垃圾对象。

但是,方法很快就执行完了,方法栈会销毁,最后这4个node都会变成垃圾。

image-20240528170306740

3.2.1 Java中的4种引用

在Java中,有四种引用类型,分别是强引用、软引用、弱引用和虚引用。它们的主要区别在于垃圾回收器对它们的处理方式不同。

1. 强引用 (Strong Reference)

这是Java中的默认引用类型。任何通过赋值创建的对象引用都是强引用。

  • 只要一个对象被强引用关联,垃圾回收器就永远不会回收这个对象。
  • 即使在内存不足的情况下,JVM也不会回收强引用的对象,会抛出OutOfMemoryError
public void exampleMethod() {
    String str = new String("Hello");
    // str 在此作用范围内是活跃的,不会被回收
    // 一些操作...
} // 作用范围结束,str 引用失效

2. 软引用 (Soft Reference)

软引用是一种在内存不足时才会被回收的引用类型。它通常用于实现内存敏感的缓存。

  • 只有在JVM内存不足时,才会回收这些对象。

  • 软引用与SoftReference类一起使用。

import java.lang.ref.SoftReference;

String str = new String("Hello");
SoftReference<String> softRef = new SoftReference<>(str);
str = null; // 强引用被移除,现在只有软引用指向这个对象

3. 弱引用 (Weak Reference)

弱引用是一种比软引用更弱的引用类型。只要垃圾回收器运行,不管内存是否充足,都会回收该引用指向的对象。

  • 用于实现规范化映射(如WeakHashMap)。
  • 弱引用与WeakReference类一起使用。
import java.lang.ref.WeakReference;

String str = new String("Hello");
WeakReference<String> weakRef = new WeakReference<>(str);
str = null; // 强引用被移除,现在只有弱引用指向这个对象

4. 虚引用 (Phantom Reference)

虚引用是最弱的一种引用类型,主要用于跟踪对象被垃圾回收的状态。

  • 虚引用本身并不决定对象的生命周期。
  • 用于清理堆外内存或者实现对象的预先清理机制。
  • 虚引用可以与PhantomReferenceReferenceQueue类一起使用。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

String str = new String("Hello");
ReferenceQueue<String> refQueue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(str, refQueue);
str = null; // 强引用被移除,现在只有虚引用指向这个对象

// 检查引用队列以了解对象是否已被回收
if (refQueue.poll() != null) {
    System.out.println("对象已被回收");
}

方法中的变量都是在局部变量表中,结束后都会gg,区分几种引用的意义在哪?

我觉得主要体现在成员变量上。

强引用 (Strong Reference):对象必须长期存在,并且不希望被垃圾回收。例如,核心业务逻辑中必须持续存在的数据结构。

public class UserService {
    private List<User> userList = new ArrayList<>();

    public void addUser(User user) {
        userList.add(user);
    }

    public List<User> getUsers() {
        return userList;
    }
}

软引用 (Soft Reference):实现缓存,缓存的数据可以在内存不足时被回收,避免内存溢出。

import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class ImageCache {
    private Map<String, SoftReference<byte[]>> imageCache = new HashMap<>();

    public void cacheImage(String key, byte[] imageData) {
        imageCache.put(key, new SoftReference<>(imageData));
    }

    public byte[] getImage(String key) {
        SoftReference<byte[]> ref = imageCache.get(key);
        return (ref != null) ? ref.get() : null;
    }
}

弱引用 (Weak Reference):实现规范化映射,如WeakHashMap,用于自动清除不再使用的键值对,避免内存泄漏。

import java.lang.ref.WeakReference;
import java.util.WeakHashMap;

public class SessionManager {
    private WeakHashMap<SessionKey, Session> sessionMap = new WeakHashMap<>();

    public void addSession(SessionKey key, Session session) {
        sessionMap.put(key, session);
    }

    public Session getSession(SessionKey key) {
        return sessionMap.get(key);
    }
}

虚引用 (Phantom Reference):跟踪对象的回收状态,在对象被回收时执行清理操作,如关闭资源、释放堆外内存。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class ResourceCleaner {
    private ReferenceQueue<MyResource> refQueue = new ReferenceQueue<>();
    private Set<PhantomReference<MyResource>> refs = new HashSet<>();

    public void trackResource(MyResource resource) {
        PhantomReference<MyResource> phantomRef = new PhantomReference<>(resource, refQueue);
        refs.add(phantomRef);
    }

    public void cleanUp() {
        PhantomReference<? extends MyResource> ref;
        while ((ref = (PhantomReference<? extends MyResource>) refQueue.poll()) != null) {
            // 执行清理操作
            refs.remove(ref);
        }
    }
}

上面的例子中,这几个类的成员变量使用了不同的引用类型。

public class UserService {
    private List<User> userList = new ArrayList<>();

我一个等号直接强引用,意思是不希望它销毁,关键是,当UserService没了时,userList它也会没了呀,我咋知道UserService活多久?

3.2.2 对象生命周期

1.局部变量

局部变量的生命周期与其所在的方法的执行周期相同。一旦方法执行完毕,局部变量及其引用的对象就会被垃圾回收(前提是没有其他引用)。

public void someMethod() {
    User user = new User();
    // User对象在someMethod方法中被创建和使用
} // 方法结束,user引用失效,对象可以被回收

2.成员变量

成员变量的生命周期与其所在对象的生命周期相同。只要持有成员变量的对象存在,成员变量引用的对象也不会被回收。

public class UserService {
    private UserRepository userRepository = new UserRepository();

    public void saveUser(User user) {
        userRepository.save(user);
    }
    // UserRepository对象的生命周期与UserService对象相同
}

3.静态变量

静态变量的生命周期与类的生命周期相同,通常与应用程序的生命周期相同。

public class Application {
    public static UserService userService = new UserService();
    // userService的生命周期与Application类相同
}

4.依赖注入管理的Bean

在Spring Boot中,Bean的生命周期由Spring容器管理。根据Bean的作用域(如singletonprototype),其生命周期有所不同。

@Service
public class UserService {
    // UserService的生命周期由Spring容器管理
}

@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}
  • 单例Bean(Singleton Scope)

    默认情况下,Spring Bean是单例的,这意味着在Spring容器中只有一个实例,且其生命周期与Spring容器相同。

  • 原型Bean(Prototype Scope)

    原型作用域的Bean在每次请求时都会创建一个新的实例,其生命周期仅限于一次请求。

这里,不就是上小节提到的问题吗,我们的UserService如果是单例的,那么它里面的userList始终不会销毁。

3.2.3 垃圾实例

现在,列举几个常见的情况。

对象会在以下几种情况下被视为垃圾,进而可能被垃圾回收:

1. 没有任何引用指向该对象

当一个对象没有任何活动的引用指向它时,垃圾回收器会认为该对象是垃圾。例如:

public class Example {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj = null; // 原来的MyClass对象没有引用了
    }
}

在上述代码中,当obj被设置为null时,原来的MyClass对象没有任何引用,成为垃圾。

2. 局部变量的作用域结束

当方法中的局部变量超出其作用域时,随着方法栈的销毁,该变量所引用的对象如果没有其他引用,也会被视为垃圾。例如:

java复制代码public void someMethod() {
    MyClass obj = new MyClass();
    // obj只在这个方法内有效
}
// 当someMethod执行完毕后,obj超出作用域,对象变为垃圾

3. 覆盖引用

当一个变量被重新赋值时,原来引用的对象如果没有其他引用,也会被视为垃圾。例如:

public class Example {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj = new MyClass(); // 原来的MyClass对象没有引用了
    }
}

4. 数据结构中的元素被移除

当集合(如ListMap等)中的元素被移除时,如果这些元素对象没有其他引用,也会被视为垃圾。例如:

List<MyClass> list = new ArrayList<>();
MyClass obj = new MyClass();
list.add(obj);
list.remove(obj); // obj对象没有引用了

5. 线程结束

线程局部变量(ThreadLocal)在线程结束时,其引用的对象会被视为垃圾。例如:

ThreadLocal<MyClass> threadLocal = new ThreadLocal<>();
threadLocal.set(new MyClass());
// 当线程结束时,ThreadLocal变量所引用的对象没有引用了

6. 循环引用

在JVM中,垃圾回收器能够处理循环引用的情况。即使两个对象互相引用,如果它们都没有被其他活动的对象引用,它们也会被视为垃圾。例如:

class Node {
    Node next;
}

public class Example {
    public static void main(String[] args) {
        Node n1 = new Node();
        Node n2 = new Node();
        n1.next = n2;
        n2.next = n1;
        
        n1 = null;
        n2 = null; // n1和n2对象没有其他引用,成为垃圾
    }
}

3.3 垃圾识别算法

1.引用计数法

在引用计数法中,每个对象都有一个引用计数器,记录着指向该对象的引用数量。

当引用计数器为零时,表示没有任何引用指向该对象,该对象可以被释放,回收其占用的内存。

  • 增加引用:当一个新的引用指向对象时,引用计数器加1。

  • 减少引用:当一个引用不再指向对象时,引用计数器减1。

  • 回收对象:当对象的引用计数器变为零时,表示没有引用指向该对象,该对象可以被回收。


好,关键点来了,怎么理解引用和对象?

@Data
public class GC2 {
    public static void main(String[] args) {
        GC2 gc2 = new GC2();
    }
}

通过new GC2()创建了一个新的GC2对象,这个对象存储在堆内存中。

gc2是一个引用变量,存储了这个新对象的内存地址。

还记得指针吗,看一个C。

#include <stdio.h>

int main() {
    int x = 10; // 基本数据类型,x直接存储值10
    int *p = &x; // 指针,p存储的是变量x的内存地址

    printf("Value of x: %d\n", x); // 输出x的值
    printf("Value at address p: %d\n", *p); // 通过指针p访问x的值

    return 0;
}
int *p = &x;
  • p是一个指针变量,存储的是变量x的内存地址。
  • p可以被视为引用,因为它存储的是另一个变量的地址。

在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。

所以呢,咱们的Java的GC,对于上面这种gc2,能够自动化处理,通过gc自动释放空闲的内存,减少内存泄漏的风险。


引用计数法简单,但是存在一些缺点

  • 空间占用:需要额外的空间来存储引用计数。

  • 性能开销:每次增加或减少引用时,都需要更新引用计数器。这会增加一定的性能开销,特别是在引用频繁变化的场景下。

  • 循环引用问题:引用计数法无法处理循环引用的问题。例如,两个对象互相引用,即使它们不再被其他对象引用,引用计数也不会变为零,从而导致内存泄漏。

2.可达性分析法

在Java中,是通过可达性分析来判定对象是否存活的。

基本思路是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain)。

  • 找得到的对象标记为非垃圾对象
  • 其余未标记的,则为垃圾对象。

记住!标记的是什么?标记的是非垃圾对象!即存活对象!

image-20240528185542824

谁是GC Roots对象?

位置 GC Roots对象 描述
虚拟机栈 方法参数、局部变量 栈帧中的本地变量表中引用的对象
本地方法栈 方法参数、局部变量 通过Java Native Interface(JNI)引用的对象
方法区 静态属性 静态属性引用的对象
方法区 常量属性 常量属性引用的对象
活动线程 栈帧中的引用 当前正在运行的线程

看一段代码。

package cn.yang37.jvm.gc;

/**
 * @description:
 * @class: GCRootsExample
 * @author: yang37z@qq.com
 * @date: 2024/5/28 19:08
 * @version: 1.0
 */
public class GCRootsExample {

    // 静态变量 存储在方法区
    private static GCRootsExample staticInstance;

    // 成员变量,作为对象中的属性,存储在堆中。
    private GCRootsExample instance;

    public static void main(String[] args) {

        GCRootsExample obj1 = new GCRootsExample();
        staticInstance = new GCRootsExample();

        GCRootsExample obj2 = new GCRootsExample();
        obj1.instance = obj2;

        obj1 = null;
        obj2 = null;

        // 提示gc
        System.gc();
    }
}

现在拆分为2段。

public class GCRootsExample {

    // 静态变量 存储在方法区
    private static GCRootsExample staticInstance;

    // 成员变量,作为对象中的属性,存储在堆中。
    private GCRootsExample instance;

    public static void main(String[] args) {

        GCRootsExample obj1 = new GCRootsExample();
        staticInstance = new GCRootsExample();

        GCRootsExample obj2 = new GCRootsExample();
        obj1.instance = obj2;
        // ...

image-20240528221223307

obj1obj2设置为null

        obj1 = null;
        obj2 = null;

image-20240528221239279

在这个示例中:

  • obj1obj2是局部变量,它们在main方法的栈帧中,因此它们是GC Roots的一部分。
  • staticInstance是一个静态变量,存储在方法区中,它也是GC Roots的一部分。

System.gc()被调用时,垃圾回收器执行可达性分析:

  • 从GC Roots(包括静态变量staticInstance和局部变量obj1obj2)开始。

    • 局部变量obj1obj2,找不到东西。
    • 静态变量staticInstance,序号3的能找到。
  • 不可达的对象被认为是垃圾,可以被回收。

故图里的序号1、2可能被回收,序号3的不会。

3.4 GC类型

清理垃圾也有大扫除和小扫除,叫个专门的名字而已。

垃圾回收类型 负责回收的区域 说明
Young GC(也称Minor GC) 新生代(包括Eden区和Survivor区) 回收新生代中的对象,频率较高,速度较快。
Old GC(也称Major GC) 老年代 回收老年代中的对象,频率较低,时间较长。
目前只有CMS垃圾回收器会单独收集老年代的,其他的是FullGC的时候回收老年区。
Full GC 整个堆(包括新生代和老年代)以及方法区 回收整个堆中的对象,时间最长,所有应用线程暂停,频率应尽量降低。

这里Old GC、Major GC的定义网上传的已经比较混乱了,有的文章里啊,又说Major GC专门清理元空间。

比较好认知的是Young GC和FullGC,Young GC是清理年轻代,FullGC是针对整个新生代、老生代、元空间(Java8)的全局范围的GC。

3.4.1 YoungGC

Young GC(也称为Minor GC)是针对新生代(Young Generation)进行的垃圾回收。

1.什么时候触发

Young GC通常在以下情况下触发

  • 新生代内存不足

    • 当新对象分配到Eden区,并且Eden区没有足够的空间时,会触发Young GC。
    • 如果Eden区满了,JVM会尝试回收Eden区中的不再使用的对象。
  • Survivor区切换

    • 每次Young GC后,存活的对象会从Eden区和一个Survivor区复制到另一个Survivor区。
    • 如果Survivor区的空间不足以容纳这些存活的对象,会触发Young GC。

2.频率

Young GC的频率取决于应用程序的内存使用模式和分配速率。

如果应用程序频繁创建大量短生命周期的对象,Young GC会更频繁地发生。通过调整JVM的参数,可以优化Young GC的性能和频率。

3.JVM参数

注意啊,注意啊,这里一定要结合后文的垃圾回收器来看。

参数 说明 示例
-Xms 设置堆的初始大小 -Xms3g
-Xmx 设置堆的最大大小 -Xmx3g
-Xmn 设置新生代的大小 -Xmn512m
-XX:NewRatio 设置新生代与老年代的大小比例 -XX:NewRatio=2
-XX:SurvivorRatio 设置Eden区与每个Survivor区的比例 -XX:SurvivorRatio=8
-XX:+UseParNewGC 启用并行收集器,以提高Young GC的性能 -XX:+UseParNewGC
java -Xms3g -Xmx3g -Xmn512m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+UseParNewGC -jar myapp.jar

-Xms3g:设置堆的初始大小为3GB。
-Xmx3g:设置堆的最大大小为3GB。
-Xmn512m:设置新生代的大小为512MB。
-XX:NewRatio=2:设置新生代与老年代的比例为1:2。
-XX:SurvivorRatio=8:设置Eden区与每个Survivor区的比例为8:1。
-XX:+UseParNewGC:启用并行Young GC。

3.4.2 OldGC

1. 什么时候触发

Old GC通常在以下情况下触发:

  • 老年代内存不足
    • 当老年代的内存空间不足以存储新对象或从新生代晋升过来的对象时,会触发Old GC。
  • 显式GC调用
    • 手动调用System.gc()Runtime.getRuntime().gc()可能触发Old GC,尽管这只是一个建议,JVM不一定会执行Full GC。
  • 元数据空间不足
    • 在某些情况下,元数据空间(Metaspace)不足也会触发Old GC。

2. 频率

Old GC的频率通常比Young GC低,因为它主要处理生命周期较长的对象。触发Old GC的频率取决于老年代的内存使用情况和对象的晋升速率。

3. JVM参数

注意啊,注意啊,这里一定要结合后文的垃圾回收器来看。

参数 说明 示例
-XX:MaxGCPauseMillis 设置垃圾收集的最大暂停时间 -XX:MaxGCPauseMillis=200
-XX:GCTimeRatio 设置GC时间占应用程序运行时间的比例 -XX:GCTimeRatio=4
-XX:+UseConcMarkSweepGC 启用CMS垃圾收集器 -XX:+UseConcMarkSweepGC
-XX:+UseG1GC 启用G1垃圾收集器 -XX:+UseG1GC
-XX:CMSInitiatingOccupancyFraction 设置CMS垃圾收集器在老年代空间使用率达到多少时开始进行GC -XX:CMSInitiatingOccupancyFraction=70
-XX:+UseAdaptiveSizePolicy 启用自适应大小策略,使JVM自动调整各代的大小以达到最佳性能 -XX:+UseAdaptiveSizePolicy
java -Xms3g -Xmx3g -XX:MaxGCPauseMillis=200 -XX:GCTimeRatio=4 -XX:+UseG1GC -XX:+UseAdaptiveSizePolicy -jar myapp.jar

-Xms3g:设置堆的初始大小为3GB。
-Xmx3g:设置堆的最大大小为3GB。
-XX:MaxGCPauseMillis=200:希望GC的最大暂停时间为200毫秒。
-XX:GCTimeRatio=4:设置GC时间占总时间的1/(1+4)。
-XX:+UseG1GC:启用G1垃圾收集器。
-XX:+UseAdaptiveSizePolicy:启用自适应大小策略。

3.4.3 FullGC

Full GC是针对整个堆(包括新生代和老年代)进行的垃圾回收,通常会引起较长时间的暂停。

1. 什么时候触发

Full GC通常在以下情况下触发:

  • 显式GC调用
    • 手动调用System.gc()Runtime.getRuntime().gc()通常会触发Full GC。
  • 元数据空间不足
    • 当Metaspace(或PermGen,取决于JVM版本)空间不足时,会触发Full GC以释放空间。
  • CMS回收器的GC失败
    • 在使用CMS垃圾收集器时,如果并发清理失败,会触发Full GC。
  • 晋升失败
    • 在新生代GC时,如果存活对象需要晋升到老年代,而老年代空间不足,则可能触发Full GC。

2. 频率

Full GC的频率通常比Young GC和Old GC低,但每次Full GC的停顿时间较长。触发频率主要取决于应用程序的内存使用模式和手动调用频率。

3. JVM参数

注意啊,注意啊,这里一定要结合后文的垃圾回收器来看。

参数 说明 示例
-XX:+UseSerialGC 启用串行垃圾收集器(适用于单线程环境) -XX:+UseSerialGC
-XX:+UseParallelGC 启用并行垃圾收集器 -XX:+UseParallelGC
-XX:+UseConcMarkSweepGC 启用CMS垃圾收集器 -XX:+UseConcMarkSweepGC
-XX:+UseG1GC 启用G1垃圾收集器 -XX:+UseG1GC
-XX:+DisableExplicitGC 禁用显式GC调用 -XX:+DisableExplicitGC
-XX:MetaspaceSize 设置元空间的初始大小 -XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize 设置元空间的最大大小 -XX:MaxMetaspaceSize=256m
-XX:CMSFullGCsBeforeCompaction 设置在进行多少次Full GC后进行一次压缩(适用于CMS垃圾收集器) -XX:CMSFullGCsBeforeCompaction=5
-XX:G1HeapRegionSize 设置G1垃圾收集器的堆区域大小 -XX:G1HeapRegionSize=32m
java -Xms3g -Xmx3g -XX:+UseG1GC -XX:+DisableExplicitGC -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -jar myapp.jar

-Xms3g:设置堆的初始大小为3GB。
-Xmx3g:设置堆的最大大小为3GB。
-XX:+UseG1GC:启用G1垃圾收集器。
-XX:+DisableExplicitGC:禁用显式GC调用。
-XX:MetaspaceSize=128m:设置元空间的初始大小为128MB。
-XX:MaxMetaspaceSize=256m:设置元空间的最大大小为256MB。

3.垃圾回收算法

怎么清理垃圾

算法类型 英文 常见简称 有无碎片 速度 空间开销 是否移动对象 适用场景
复制算法 Copying Algorithm Copy GC/Copying GC 无碎片 高(新区域) 新生代垃圾收集
标记-清除算法 Mark-Sweep Algorithm Mark-Sweep 有碎片 老年代垃圾收集
标记-整理算法 Mark-Compact Algorithm Mark-Compact 无碎片 老年代垃圾收集

3.1 复制算法

复制算法的基本思想是将内存划分为两块大小相等的区域,每次只使用其中的一块。

当这一块的内存用完时,将还存活的对象复制到另一块区域,然后清空当前使用的区域。

1.原理

  • 初始化:将新生代内存空间分为三块区域:Eden 区、From Survivor 区(S0)和 To Survivor 区(S1)。初始化时,对象主要分配在 Eden 区。
  • 分配对象:对象在 Eden 区中分配,直到 Eden 区用尽。
  • 开始 GC:当 Eden 区用尽时,开始垃圾收集过程。
  • 存活对象复制:将 Eden 区和 From Survivor 区中仍然存活的对象复制到 To Survivor 区。
  • 更新引用:更新所有引用,使它们指向新位置。
  • 清空 Eden 区和 From Survivor 区:完成复制后,清空 Eden 区和 From Survivor 区。
  • 交换角色:From Survivor 区和 To Survivor 区角色互换,下一次分配从新的 Eden 区和 From Survivor 区开始。

2.示意图

image-20240529101106500

3.优缺点

  • 优点

    • 无碎片化:通过整体复制和清空,有效避免了内存碎片问题。
    • 速度快:对象分配和回收速度较快,适用于新生代垃圾收集。
  • 缺点

    • 空间浪费:由于需要将内存分为两块,仅能使用其中的一块,导致空间利用率较低。
    • 不适合老年代:老年代中对象存活率较高,复制成本高,效率低下。

为啥对象存活率高就不适合?

存活率高,意味着基本都是存活的对象,由于咱们要迁移存活对象到To区,迁移起来数量多,效率低。

4.JVM应用

在JVM中,复制算法主要用于新生代垃圾收集。

新生代中的对象生命周期较短,存活率低,复制算法能够快速清理大量短生命周期的对象。

具体来说,新生代又分为Eden区和两个Survivor区(S0和S1)。复制算法的工作过程如下:

  • 大部分对象在Eden区分配。

  • 当Eden区满时,触发Minor GC。

  • 将Eden区和一个Survivor区(假设为S0)中的存活对象复制到另一个Survivor区(S1)。

  • 清空Eden区和S0。

  • 交换S0和S1的角色,下次GC时再进行同样的操作。

嗯,回想一下前面的S0区、S1区,是不是有印象了。

3.2 标记-清除算法

标记-清除算法分为两个主要阶段:标记阶段和清除阶段。

1.原理

  • 标记阶段:从根对象开始,通过引用链追踪,标记所有的活动对象。标记过程中,将活动对象的标记位设置为有效状态,表示这些对象是可达的,不会被回收。

  • 清除阶段:在标记阶段完成后,线性遍历整个内存空间,将未被标记的对象视为垃圾对象,将其所占用的内存空间释放,以便下次分配给新的对象使用。

2.示意图

image-20240528224313812

3.优缺点

  • 优点

    • 实现简单
  • 缺点

    • 内存碎片

内存碎片是啥?标记清除执行完成后,那些被释放但不连续的小块内存空间

这些小块内存虽然空闲,但由于它们是零散分布的,因此可能无法满足较大对象的内存分配需求,这就导致了内存碎片问题。

就像下图里面,可用的空间断断续续的。

image-20240528224517636

4.JVM应用

在JVM中,标记-清除算法主要用于老年代的垃圾收集。在使用标记-清除算法时,老年代的内存管理过程如下:

  • 初始阶段,对象在新生代分配,当对象经过多次Minor GC后仍存活,会被晋升到老年代。

  • 触发Full GC:当老年代的内存用尽或接近用尽时,触发Full GC。

  • 标记阶段:从GC Roots开始,标记所有可达的对象。

  • 清除阶段:清除所有未被标记的对象,回收其内存。

  • 内存碎片化:老年代可能会出现内存碎片,影响后续的大对象分配。为了应对碎片化问题,JVM可能会在适当时候触发压缩(Compaction),将存活对象压缩到内存的一端,减少碎片。

3.3 标记-整理算法

有时候也称标记-压缩。

标记-整理算法同样分为两个主要阶段:标记阶段和整理阶段。

1.原理

  • 标记阶段:遍历所有对象,标记所有可达(仍在使用)的对象。
  • 整理阶段:将所有存活的对象移动到内存的一端,使得所有存活对象集中排列,清除后的空闲内存位于内存的另一端。
    • 提前计算新位置:根据存活对象的大小和排列顺序,确定每个存活对象的新位置,避免移动过程中出现空间不足的问题。
    • 直接覆盖不可达对象:在移动存活对象时,不可达对象会被直接覆盖,这样无需额外的清除步骤。

2.示意图

image-20240529102208029

3.优缺点

  • 优点

    • 无碎片化:通过整理阶段,将存活对象压缩到内存的一端,消除碎片化问题。
    • 高效的内存利用:整理后,大块的连续空闲内存可供新对象分配,提升内存利用效率。
  • 缺点

    • 性能开销:对象移动和引用更新的操作比较耗时,尤其是当存活对象较多时。
    • GC暂停时间长:整理阶段需要停止应用程序的执行(STW),暂停时间可能较长。

4.JVM应用

在JVM中,标记-整理算法主要用于老年代的垃圾收集。

由于老年代中对象存活率高,标记-整理算法能够有效处理长生命周期的对象,并避免内存碎片化问题。

典型的应用场景包括Full GC(全面垃圾收集),当老年代内存耗尽或接近耗尽时,会触发Full GC。

3.4 总结

类比整理房间,现在房间里东西太多了,东西都不好放。

标记-清除算法:我们把有用的东西标记好,然后,把不需要的扔掉。看起来倒是空了一些地方,不过房子总还是乱乱的,来个大家具,又不好放了。

标记-整理算法:记好有用的东西后,我们规划好放哪块地方。找好位置,重新给他们摆过去,摆的地方被占了嘛我们就清理掉再摆。很麻烦,很头疼,好就好在,起码有块区域空出来了,大点的东西似乎也好放了。

复制算法:有钱人来两个房间,也别管那么多了,拿好有用的放旁边房间就完事,咱旧房间啥也不管,无脑清理,简单高效。

3.5 JVM对象分代的意义

根据上面提到的几种GC算法的特点,可以知道,不同存活时间的对象,适用的算法是不一样的。

  • 频繁消亡的对象

    新产生的对象多,伴随而来的是垃圾对象也很多,这个时候,我们用复制算法就比较合适,它效率够快,直接挪走一清空就完事,一大块空间立马腾出来。

  • 存活较久的对象

    这个时候,由于大部分对象都是存活着的,实际上清理的垃圾不一定会很多。再用复制算法的话,基本上都在挪存活对象去了。而且这个时候,快不快很重要吗,我们新来的老年对象也没那么频繁。这种场景,反而标记清除和标记整理更合适。

4.垃圾回收器

# 单词复习
Serial:串行的、顺序排列的,一连串(系列)的;
Parallel:平行的;相似的,同时发生的;(计算机)并行的
CMS(Concurrent Mark-Sweep):并发标记清除
Scavenge:从废弃物中捡拾 / 打扫 / 排除废气 / 以…为食。 此处"Scavenge" 通常指的是对新生代的垃圾回收
Sweep:扫除;猛拉;掸去;打扫;席卷;扫视;袭。 此处"Sweep"通常指标记-清除的第二个阶段。

image-20250510184741035

这张图是垃圾回收器的一个简单汇总,其中新生代和老年代在分代垃圾回收器思想下是搭配使用的。

例如:新生代Serial + 老年带 SerialOld

在开始介绍之前,我们先拎清楚一个问题:为什么垃圾回收的时候,必须要STW?

如果不暂停,同时运行着用户线程,那么就无法准确标记出垃圾,就好比刚标记一个对象是垃圾用户线程立马又引用了。

同理,类似于扫地,你妈给你打扫房间的时候,一般都希望你躺床上或者滚出去,不要在房间里添乱。

4.1 分代/垃圾回收器

4.1.1 Serial GC和Serial Old GC

一个GC线程

早期硬件配置相对较低,内存容量较小、CPU 单核、并发应用场景相对较少。

基于这些背景,结合上面的三种经典GC算法,自然而然的诞生了咱们的串行垃圾回收器。

垃圾回收器 英文全称 类型 特点 作用域 算法 备注
Serial GC Serial Garbage Collector 串行 工作线程暂停,一个线程进行垃圾回收 新生代 复制算法
Serial Old GC Serial Old Garbage Collector 串行 工作线程暂停,一个线程进行垃圾回收 老年代 标记-整理算法

image-20240529201314210

4.1.2 Parallel GC和Parallel Old GC

多个GC线程,就是单线程的Serial的进阶版。

随着时代的发展,多核处理器发展起来了,这个时候咱们也自然而然能想到,GC的时候可不可以多个线程来执行,于是就诞生了咱们的Parallel系列。

垃圾回收器 英文全称 类型 特点 作用域 算法 备注
Parallel GC
又称:Parallel Scavenge GC
Parallel Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 新生代 复制算法 和ParNew相比,能动态调整内存分配情况,JDK8默认
Parallel Old GC Parallel Old Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 老年代 标记-整理算法 替代串行的Serial Old GC。
吞吐量优先的应用,例如批处理和后台任务,JDK8默认

image-20240529201647930

4.1.3 ParNew GC和CMS GC

随着咱们程序的发展,内存越来越大, Parallel系列可能性能也不够用了,例如:

1G 200ms,现在程序竟然达到了20G,可能需要STW的时间就要按秒来算了,这对于用户们来说是无法接受的!

你可能说,既然垃圾回收可以多线程,那我来Parallel系列回收器来100个线程不就好了?

实际上这样是不行的哈,你细想一下,咱们的真正能实现的线程数是受咱们CPU数量来决定的,一个4核的机器,你强行增加线程数,反而会增加线程切换的成本,适得其反。

迫切需要一种更理想的垃圾回收器,追求更短的STW时间,CMS就是这样一种产物。

但是,理想归理想,CMS不管在哪个版本,都没有被设置成默认的垃圾回收器,因为极端情况下会存在一些bug。。。

但是,CMS作为垃圾回收器承上启下的产物,是非常有学习意义的。

CMS诞生于JDK5,JDK9时被标记为Deprecated,JDK14被废弃。

垃圾回收器 英文全称 类型 特点 作用域 算法 备注
Parallel New GC Parallel New Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 新生代 复制算法 类似于那个多线程版的Parallel GC,专门搭配CMS GC使用的。
CMS GC Concurrent Mark-Sweep Garbage Collector 并行 用户线程和垃圾回收线程同时执行 老年代 标记-清除算法 低暂停
低延迟要求的应用,例如响应时间要求高的交互式应用程序。

Parallel New GC(年轻代),使用复制算法,是设计出来搭配CMS GC的,与Parallel GC类似,此处不过多讲解。

image-20240529201146325

CMS回收器的一个特点,就是可以并发的去标记垃圾,就下面的框框2部分,用户线程和标记线程是可以一起运行的,以此来降低用户延迟。

image-20250511154956962

你可能会问,这样乱搞不是会出现问题,并发标记的时候会不会出现问题?

  • 之前不是垃圾,后面变成了垃圾。
  • 之前是垃圾,后面不是垃圾了。

在学习之前,先了解下三色标记法,这个就是并发标记的精髓。

4.1.3.1 三色标记法

所谓的三色,就是黑白灰三种颜色的对象,用于表示在垃圾标记过程中某个对象的状态。

颜色 含义 状态特征
黑色image-20250511163239420 对象被检查过,其成员对象也被检查过。 标记完成,不用管了。
白色image-20250511163320981 对象没有被检查 垃圾对象,没有标记,可以清理。
灰色image-20250511163309675 对象被检查,但是其成员还没有全部检查完成。 需分析,重点查找的就是这些灰色对象。

三色标记法的基本思想还是参考咱们的可达性分析法,只不过在分析的过程中引入了这几种颜色。

记住啊,要标记的是非垃圾对象(存活对象),剩下的白色的才是垃圾对象(需要清理)。


1.三色标记法的步骤

截图参考:GoV1.5三色标记法,额,别纠结咋是go语言的哈,这两个三色标记原理一样的。

假设程序中的对象状态如下,其中对象1和4是程序中直接能找到的一些GCRoot对象,2、3、4、5、7是一些可能有用的对象,6呢是一个一眼就能看出没用的对象。

image-20250511170134115

A.步骤1:在一开始时,将所有对象标记为白色。

image-20250511165943190

B.步骤2:访问GCRoot能直接查找到的对象,将其标记为灰色。

只往下走一层哈,就是只查直接能找到的对象1和4。

image-20250511170503874

C.步骤3:访问灰色对象标记表,将其可达的对象标记为灰色;已经查找过的(灰色)对象,更新成黑色。

还是只走一层哈,即遍历我们的对象1和4,往下层打个标记,把对应的对象2和7改成灰色;然后呢,对象1和4自身更新成黑色。

image-20250511170942654

D.步骤4:重复步骤3,逐步查找灰色标记表中的对象,直到灰色标记表中无对象。

查找灰色对象2和7

image-20250511171342801

查找灰色对象3

image-20250511171413792

E.步骤5:灰色标记表中无对象,则完成标记,白色的即垃圾对象
2.三色标记法的问题

三色标记法的优点就是在标记过程中用户线程还能执行,减少了停顿时间。

并发标记看起来是很好的,三色标记法也是存在一些问题的,还是跟咋们之前那个类似于“薛定谔的猫一样的垃圾观测问题”。

A.浮动垃圾问题

本来不是垃圾,突然变成垃圾了。

例如在下方过程中,本来对象2不是垃圾,突然用户线程切断了联系,这个时候对象2应该是个垃圾的,但是三色标记过程已经将它标记成了灰色,导致不会被回收。

这个问题不大,这一轮三色标记结束后,假设对象2还在,下一轮的情况:

还是从GCRoot加载对象1成灰色,但是由于对象1和2之间没有联系了,在从对象1扫描时,是不会更新对象2为灰色的,所以最终对象2还是能被回收掉。

image-20250511173434400

B.漏标(存活对象)问题

本来是垃圾,突然不是垃圾了。

  • 黑色节点对象4突然引用了白色节点对象3
  • 灰色节点对象2突然断开了与白色节点对象3的连接

image-20250511174106442

image-20250511174114599

如果在标记的瞬间触发了上述两个变动,实际上对象4是引用着对象3的,但是由于从灰色节点开始扫描,会错误的导致无法标记到白色对象3。

这样子就会导致对象3被错误的回收!引发问题!!!

问题已经出现了,此处扩展一下,引入一下强弱三色不变式的概念。

image-20250511174355447

强三色不变式:破坏条件1

image-20250511174440028
弱三色不变式:破坏条件2

image-20250511174552188

4.1.3.2 CMS实现

这里要先理解上面的三色标记法哦

image-20250511154956962

1.初始标记(stw)

即三色标记法第一次根据GCRoots找到灰色对象的过程。

2.并发标记

这里就是三色标记法标记存活对象的过程

标记过程中,可能出现一些对象新增关系、也可能出现一些对象丢失关系之类的,更新其dirtyCard。

3.重新标记(stw)

对dirtyCard中的对象(存在变化的对象),避免扫描整个老年代的对象,进行粗粒度的重新标记。

4.并发清理

清理垃圾,恢复垃圾回收中的各种处理参数等,准备下一次垃圾回收。

4.1.3.3 CMS缺陷

  • 漏标问题

  • 内存碎片问题

CMS内存碎片的问题暴露出来了,零零散散的,很不好放大点的对象。

内存不够的问题已经出现了,不能不管吧?所以为了确保能放,无奈触发Serial Old GC。

当CMS GC在垃圾收集过程中出现内存不足(Concurrent Mode Failure)或Promotion Failure(晋升失败,无法晋升成老年代)时,JVM会退化为使用Serial Old GC进行垃圾收集。这通常会导致较长的停顿时间,但可以确保垃圾收集完成,避免内存耗尽。

能优化吗

可以在CMS垃圾收集器运行时定期进行内存整理(Compaction),以减少碎片化,确保有足够大的连续内存块来分配对象,从而降低Concurrent Mode Failure的发生概率。

-XX:+UseCMSCompactAtFullCollection:在每次Full GC时进行内存整理(Compaction)。

-XX:CMSFullGCsBeforeCompaction=N:指定在N次CMS Full GC之后进行一次内存整理。

# 每次都整理
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection

# 每5次整理下
-XX:+UseConcMarkSweepGC
-XX:CMSFullGCsBeforeCompaction=5

4.1.3.4 何为高延迟与高吞吐?

真烦人啊这些玩意,字母长的又像,东西还一大顿,停顿、吞吐量,脑袋都嗡嗡的了我。

在垃圾收集(GC)的上下文中,吞吐量和停顿时间是两个关键的性能指标,且它们之间往往存在反比关系。

指标 定义 目标 场景示例 常用垃圾收集器
吞吐量 应用程序在单位时间内完成的工作量,通常表示为应用程序运行时间与总时间(包括GC时间)的比率 最大化应用程序的运行时间,最小化GC时间 批处理系统、后台任务处理、数据分析 Parallel GC、Parallel Old GC
停顿时间 应用程序线程因垃圾收集而暂停的时间 最小化应用程序的暂停时间,确保快速响应 交互式应用、在线交易系统、实时系统 CMS GC、G1 GC

好好好,停顿好理解,吞吐量嘛,就是程序正经运行时间/总时间,因为STW是独占时间的,咱们工作线程跑不动。所以,吞吐量会受STW的影响。

Parallel Old GC经常对比CMS,说CMS延迟低,Parallel Old GC吞吐量高,后面这个Parallel Old GC比CMS吞吐量高?

抽象啊太抽象了,看起来Parallel Old GC的STW,时间更长啊。

哎哎哎,兄台此言差异,我悟了好半天,听我分析一手。

我觉得啊,这玩意要从宏观角度来看。

  • CMS:单次STW时间短,但是碎片多哇,空间老不够用,可能FullGC次数还多点。
  • Parallel Old GC:单次STW时间长点,但是咱们空间够啊,FullGC相对没那么频繁。

那么整体角度来看,CMS的STW时间可能还会多点,所以应用整体的吞吐量不如Parallel Old GC。但是,对于用户参与的场景,咱们要的就是快。

不行,整个例子,我想想,好,自助餐比较贴切。

  • CMS:我每次拿少点,快快的就干上饭,爽。
  • Parallel Old GC:在那精挑细选,拿的倒是挺多,就是肚子咕咕叫了半天,也好,不用老是跑。

吃饱了回想下,CMS爽是爽,跑的步数不少啊。

4.2 分区/垃圾回收器

4.2.1 G1

垃圾回收器 英文全称 类型 特点 作用域 算法 备注
G1 GC Garbage-First Garbage Collector 并行 用户线程和垃圾回收线程同时执行 整堆 分区算法 在延迟可控的情况下尽可能提高吞吐量,JDK9默认

image-20240529220007294

额,图完全跟前面的不一样了对吧,哈哈,看起来就很高级,我们先看下G1大哥的评论区。

G1不仅能提供能提供规整的内存,而且能够实现可预测的停顿,能够将垃圾回收时间控制在N毫秒内。

这种“可预测的停顿”和高吞吐量特性让G1被称为"功能最全的垃圾回收器"。


内存划分

G1将堆划分为一系列大小不等的内存区域,这些小格子称为Region(每个region为1-32M,2^n)。

一般最多可以有2048个Region,Region大小等于堆大小除以2048。比如堆大小为4096M,则Region大小为2M。

region块

在分代垃圾回收算法的思想下,region逻辑上划分为Eden,Survivor和老年代。

  • E、S、O:每个分区都可能是eden区、survivor区,也可能是old区,动态变化的,但在一个时刻只能是一种分区。

  • H:Humongous是一个特殊的区块,专门用于存放大对象,当一个对象的容量超过了Region大小的一半,就会把这个对象放进Humongous分区。

似乎,一个块有点太大了啊,什么东西需要几个M啊。

Card Table卡表

Card Table是Region的内部结构划分。每个region内部被划分为若干的内存块,被称为card。这些card集合被称为card table,卡表。

比如下面的例子,region1中的内存区域被划分为9块card,9块card的集合就是卡表card table。

card就是很小的区域了,看着存呗。

img

4.2.2 ZGC

4.3 JVM垃圾收集器配置参数

列举部分配置参数

  • -XX:+PrintCommandLineFlags
    • 说明:查看使用的垃圾收集器。
    • 用途:用于打印JVM启动时使用的垃圾收集器和其他JVM配置参数。
  • -XX:+UseSerialGC
    • 说明:指定使用Serial GC和Serial Old GC。
    • 用途:用于强制JVM使用串行垃圾收集器,适用于单线程或小型应用程序。
  • -XX:+UseParNewGC
    • 说明:指定新生代使用ParNew GC。
    • 用途:用于配置JVM在新生代使用ParNew GC,适用于多线程环境。
  • -XX:+UseConcMarkSweepGC
    • 说明:指定老年代使用CMS GC。
    • 用途:用于配置JVM在老年代使用并发标记-清除垃圾收集器,适用于低延迟要求的应用程序。
  • -XX:+UseParallelGC
    • 说明:指定新生代使用Parallel GC。
    • 用途:用于配置JVM在新生代使用并行垃圾收集器,以提高吞吐量。
  • -XX:+UseParallelOldGC
    • 说明:指定老年代使用Parallel Old GC。
    • 用途:用于配置JVM在老年代使用并行垃圾收集器。
    • 备注:如果配置了 -XX:+UseParallelGC-XX:+UseParallelOldGC 会自动激活。

5.常见问题

5.1 e区不够用吗?为啥要有s区?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象直接被送到老年代。

老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。

假设没有s区,我们直接调整老年代的空间。

方案 优点 缺点
增加老年代空间 可以放的对象多,Full GC频率降低。 空间大,但是FullGC的时间更长。
减少老年代空间 可以放的对象少,Full GC所需时间减少 。 空间小,但是FullGC的频率更高。

可以看到,这是有冲突的,直接调大调小,都不太好。

自然而然的,我们想要一种东西,能够缓冲一下,就是所谓的,我想他放的刚刚好啊。

S区存在的意义:

预筛选,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。减少被送到老年代的对象,进而减少Full GC的发生。

5.2 为啥s区要2个?

前面提到,我们需要有个s区来缓冲下,确保对象不被立马放入o区,那么,数量上为啥会是From和to区,两个。

现在,假设只有1个s区。

e区放满,准备YGC。

image-20240530120130227

有的对象没啥用,被回收了,命长的活下来去S区了。

image-20240530120211567

很快,E区又满了。不行,还是得YGC。

image-20240530120314430

问题来了,咱们S区也有的对象得活命啊。

image-20240530120449486

E区好办,活命的挪到S区,问题就出在了,S区的,往哪挪动?

这个时候,S区不可避免的出现了内存碎片问题。

多来了几次后,S区就破败不堪了。

image-20240530120626318

哎,要是S区能像E区一样直接挪走就好了,你看,你自然而然的诉求,我也想要一个新区。

每次我活下来的,我也挪走,这样我这里可以变成空空的了。

下一轮,交换From和To区的身份,循环往复

那么,为啥不来3个,2个就够用了。

posted @ 2024-05-29 22:19  羊37  阅读(143)  评论(0)    收藏  举报