Java-JVM-垃圾回收

参考文章:

jvm垃圾回收器-G1 ZGC篇

G1与ZGC
一文彻底搞懂八种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.2.4 垃圾识别算法

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的不会。

2.3 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。

2.3.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。

2.3.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:启用自适应大小策略。

2.3.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。

2.4 GC算法

怎么清理垃圾

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

2.4.1 复制算法

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

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

1.原理

  • 初始化:将内存空间分为两块,From空间和To空间。初始化时,只使用From空间。

  • 分配对象:对象在From空间中分配,直到From空间用尽。

  • 开始GC:当From空间用尽时,开始垃圾收集过程。

  • 存活对象复制:将From空间中仍然存活的对象复制到To空间。

  • 更新引用:更新所有引用,使它们指向新位置。

  • 清空From空间:完成复制后,清空From空间。

  • 交换角色:From空间和To空间角色互换,下一次分配从新的From空间开始。

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区,是不是有印象了。

2.4.2 标记-清除算法

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

1.原理

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

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

2.示意图

image-20240528224313812

3.优缺点

  • 优点

    • 实现简单
  • 缺点

    • 内存碎片

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

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

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

image-20240528224517636

4.JVM应用

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

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

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

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

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

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

2.4.3 标记-整理算法

有时候也称标记-压缩。

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

1.原理

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

2.示意图

image-20240529102208029

3.优缺点

  • 优点

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

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

4.JVM应用

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

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

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

2.4.4 总结

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

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

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

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

2.5 JVM对象分代的意义

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

  • 频繁消亡的对象

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

  • 存活较久的对象

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

2.6 垃圾回收器

几乎所有的垃圾回收器,都有分代的思想。

先来补补英语:

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

image-20240529194913945

额,这个图,第一排的虚线可以看做是搭配关系,新生代和老年代的搭配。

奇怪的是,第二排CMS和Serial Old GC咋还来个虚线,搜了下表示CMS垃圾收集器在某些情况下可能会退化为Serial Old GC。

具体来说,如果CMS垃圾收集器在垃圾收集过程中出现问题,例如“Concurrent Mode Failure”或者内存不足以完成并发清除时,JVM会退化为使用Serial Old GC进行垃圾收集。


垃圾回收器 英文全称 类型 特点 作用域 算法 备注
Serial GC Serial Garbage Collector 串行 工作线程暂停,一个线程进行垃圾回收 新生代 复制算法
Serial Old GC Serial Old Garbage Collector 串行 工作线程暂停,一个线程进行垃圾回收 老年代 标记-整理算法
ParNew GC Parallel New Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 新生代 复制算法 Serial GC的多线程版
CMS GC Concurrent Mark-Sweep Garbage Collector 并行 用户线程和垃圾回收线程同时执行 老年代 标记-清除算法 低暂停
低延迟要求的应用,例如响应时间要求高的交互式应用程序。
Parallel GC
(Parallel Scavenge GC)
Parallel Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 新生代 复制算法 和ParNew相比,能动态调整内存分配情况,JDK8默认
Parallel Old GC Parallel Old Garbage Collector 并行 工作线程暂停,多个线程进行垃圾回收 老年代 标记-整理算法 替代串行的Serial Old GC。
吞吐量优先的应用,例如批处理和后台任务。
G1 Garbage-First Garbage Collector 并行 用户线程和垃圾回收线程同时执行 整堆 分区算法 在延迟可控的情况下尽可能提高吞吐量,JDK9默认
ZGC Z Garbage Collector 并行 用户线程和垃圾回收线程同时执行 整堆 分页算法 STW的时间不超过10ms,且不会随着堆的大小增加而增加

列举一些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 会自动激活。

停顿与吞吐量

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

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

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

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

2.6.1 Serial GC与Serial Old GC

这两个玩意就是早期的,那个时候,硬件配置相对较低,主要特点包括内存容量较小、CPU 单核、并发应用场景相对较少。

基于这些限制条件,Serial 系列的垃圾收集器采用了简单高效、资源消耗最少、单线程收集的设计思路。

image-20240529201314210

1.Serial GC

特性 详情
类型 串行垃圾收集器
特点 单线程垃圾收集,STW停顿时间长
适用场景 单核或小型应用程序
算法 复制算法进行新生代垃圾收集

2.Serial Old GC

特性 详情
类型 串行垃圾收集器
特点 单线程垃圾收集,STW停顿时间长
适用场景 单核或小型应用程序
算法 标记-整理算法进行老年代垃圾收集

2.6.2 ParNew GC与CMS GC

1.ParNew GC

特性 详情
类型 并行垃圾收集器
特点 多线程垃圾收集,减少停顿时间
适用场景 多核CPU环境,与CMS GC配合使用效果最佳
算法 复制算法进行新生代垃圾收集

image-20240529201213310

好,回忆下这章节开始的图,ParNew GC是可以和Serial Old GC搭配的,效果就是下面这样。

image-20240529201500397

2.CMS GC (Concurrent Mark-Sweep GC)

特性 详情
类型 并发垃圾收集器
特点 低停顿,通过并发标记和清除阶段减少停顿时间
适用场景 低停顿需求的应用程序,例如需要快速响应的服务器应用
算法 标记-清除算法进行老年代垃圾收集

image-20240529201146325

CMS这里看起来复杂点,但是它是适合咱们服务器应用的,它的目的是尽可能缩短垃圾收集时用户线程的停顿时间

  • 初始标记(Initial Mark):

    • 标记从GC Roots直接可达的对象。
    • 该阶段需要短暂停顿(STW)
  • 并发标记(Concurrent Mark):

    • 并发地遍历对象图,从初始标记的对象开始,标记所有可达对象。
    • 该阶段与应用程序线程并发执行,不会导致应用程序暂停。
  • 重新标记(Remark):

    • 修正并发标记期间由于应用程序运行导致的标记变化。
    • 该阶段需要短暂停顿(STW),但停顿时间比初始标记阶段短。
  • 并发清除(Concurrent Sweep):

    • 并发地清除未标记的对象,回收它们的内存。
    • 该阶段与应用程序线程并发执行,不会导致应用程序暂停。

CMS的优缺点也是明显的,注意下Serial Old GC、Parallel Old GC分别是单、多线程的标记整理,而我们的CMS是标记清除

清除的特性是什么,是简单,不需要移动对象。所以,咱们清除是不会STW的,而我们做标记整理的整理时,要移动对象,就必然涉及到STW(对比下配图)。

但是,好是好,CMS又带来内存碎片的问题了,嗯,总是很难有完美的事情。

  • 优点
    • 低停顿时间:CMS GC的设计目标是最小化垃圾收集的停顿时间,适用于对响应时间要求高的应用程序。
    • 并发执行:标记和清除阶段与应用程序线程并发执行,减少了应用程序的停顿时间。
  • 缺点
    • 内存碎片化:由于采用标记-清除算法,CMS GC在清除阶段不会移动对象,可能导致内存碎片化问题。
    • 内存占用高:CMS GC在并发标记和清除阶段需要额外的内存空间。
    • 失败处理:如果在并发清除阶段内存不足,可能会触发“Concurrent Mode Failure”,导致Full GC,停顿时间较长。

退化机制

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

为什么CMS就容易出问题?

为啥会内存不够or对象晋升不到老年代,对象晋升不到老年代的原因核心还是内存不够嘛。嗯,内存不够?CMS内存碎片的问题暴露出来了,零零散散的,很不好放大点的对象。

有办法避免吗?

问题已经出现了,不能不管吧,所以为了确保能放,无奈触发Serial Old GC。关键又来了,能不能尽量避免不触发这个退化啊。

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

CMS主动整理内存参数

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

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

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

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

那问题又来了,如果我搭配-XX:+UseCMSCompactAtFullCollection,每次都整理,那反正整理要都STW,我咋不直接用Parallel Old GC?

哈哈,那我都这样搞了,不认可内存碎片的问题,给他强制整理,那么我觉的CMS此时已经失去了低停顿的优势,我直接选Parallel Old GC。

image-20240529205617464

2.6.3 Parallel GC与Parallel Old GC

image-20240529201647930

1.Parallel GC

特性 详情
类型 并行垃圾收集器
特点 多线程垃圾收集,提高垃圾收集的吞吐量
适用场景 多核CPU环境,注重吞吐量的应用程序(例如批处理任务)
算法 复制算法进行新生代垃圾收集

2.Parallel Old GC

特性 详情
类型 并行垃圾收集器
特点 多线程垃圾收集,提高老年代垃圾收集的吞吐量
适用场景 多核CPU环境,注重吞吐量的应用程序
算法 标记-整理算法进行老年代垃圾收集

Parallel Old GC经常对比CMS,它咋就比CMS吞吐量高了?

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

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

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

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

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

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

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

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

2.6.4 G1

有没有一种玩意,能够尽量的兼具CMS和Parallel Old GC,G1在做这件事情。

特性 详情
类型 并行和并发垃圾收集器
特点 分区堆结构、按优先级回收、混合垃圾收集、预测停顿时间、减少内存碎片
适用场景 需要平衡低停顿时间和高吞吐量的应用程序,如大型企业级应用、交互式应用程序、在线交易系统、实时数据处理系统
算法 标记-整理算法(标记-复制算法用于年轻代)和分区堆结构

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

2.6.5 ZGC

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