Java垃圾回收机制详解及性能优化

一、简介

1 垃圾产生的原因

在Java程序中对象在被创建后,随着程序的执行会不断地分配内存空间。但是当这个对象再也没有被使用时,这些占用的内存空间就会变成“垃圾”,并且由于Java语言中没有手动释放内存的机制,这些垃圾将会一直占用内存空间。

2 垃圾回收的定义与作用

垃圾回收(Garbage Collection)指的是Java虚拟机在运行时自动回收那些无用的对象所占用的内存空间。垃圾回收的作用是尽可能的在有限的内存空间下有效地分配和回收内存,防止内存泄漏。

二、垃圾回收算法

在Java中垃圾回收算法主要分为以下几种:

1 标记清除算法

标记清除算法(Mark-Sweep)是最基础的垃圾回收算法。它将垃圾回收分为两个阶段,第一个阶段是标记阶段,通过可达性分析标记出所有仍然存活的对象。第二个阶段是清除阶段,清除所有未被标记的对象。

// Java中的标记清除算法示例代码

// 标记阶段
public void mark() {
    
    
    // 通过可达性分析标记出所有存活的对象
}

// 清除阶段
public void sweep() {
    
    
    for (Object obj : heap) {
    
    
        if (!obj.marked) {
    
    
            heap.delete(obj);
        } else {
    
    
            obj.marked = false;
        }
    }
}

2 标记复制算法

标记复制算法(Mark-Compact)在标记清除算法的基础上进行了改进。它将堆空间分为两个大小相等的区域,每次只使用其中一个区域,垃圾回收时将存活的对象复制到另一个空间中,最后将使用的区域进行一次整理,清除未被引用的对象。

// Java中的标记复制算法示例代码

// 标记阶段
public void mark() {
    
    
    // 通过可达性分析标记出所有存活的对象,并将其复制到"新生代"空间中
}

// 整理阶段(同样也是清除阶段)
public void compact() {
    
    
    // 清除在年轻代空间中未被复制的对象
    // 将年轻代空间中存活的对象复制到老年代空间中
}

3 标记整理算法

标记整理算法(Mark-Compact)在标记复制算法的基础上进行了改进。它同样将堆空间分为两个大小相等的区域,但是与标记复制算法不同的是,它不会将存活的对象复制到另一个空间中,而是在整理阶段将存活的对象向一端移动,并且清除未被引用的对象。

// Java中的标记整理算法示例代码

// 标记阶段
public void mark() {
    
    
    // 通过可达性分析标记出所有存活的对象
}

// 整理阶段(同样也是清除阶段)
public void compact() {
    
    
    int index = 0;
    for (Object obj : heap) {
    
    
        if (obj.marked){
    
    
            // 将存活的对象向堆的一端移动
            heap[index++] = obj;
            obj.marked = false;
        } else {
    
    
            heap.delete(obj);
        }
    }
}

4 分代回收算法

Java虚拟机中还有一个重要概念就是分代回收算法(Generational Collection)。根据观察得出大部分对象都是“朝生夕死”的,即在其创建后不久就变成垃圾。因此在Java虚拟机中,将绝大部分对象都分配在年轻代空间,当年轻代满时,则将存活的对象移到老年代中。在垃圾回收时,对不同年代的对象使用不同的算法进行回收,以达到更好的效果。

// Java中的分代回收算法示例代码

public void collect() {
    
    
    // 对每个年代应用不同的垃圾回收算法
    young.collect();
    old.collect();
}

三、垃圾回收器

Java语言中的垃圾回收器(Garbage Collector)负责自动管理内存。Java虚拟机中的内存划分为年轻代和老年代,根据不同的回收算法,Java垃圾回收器可以分为以下几种类型:

1 串行回收器

串行回收器是最简单、运作时占用CPU最少的垃圾回收器。它在进行垃圾回收时会暂停所有线程,直到垃圾回收完成后才恢复线程运行。由于操作系统需要调度线程,这种方式比较慢,一般只适用于单核处理器或者移动设备等资源受限环境下的应用。

// 设置JVM参数使用串行回收器
java -XX:+UseSerialGC HelloWorld.java

2 并行回收器

并行回收器可以让多个线程同时参与垃圾回收,解决了串行回收器单线程效率低下的问题。使用并行回收器可以大幅提高垃圾回收效率,但也会带来一定的系统开销。并行回收器主要适用于多核CPU的服务器应用场景。

// 设置JVM参数使用并行回收器
java -XX:+UseParallelGC HelloWorld.java

3 并发回收器

并发回收器可以和应用程序线程同时运行,不需要停止应用程序的运行。但是并发回收器的垃圾回收效率相对较低,何时开始回收也无法确定。一般适用于性能要求较高、对响应时间有严格要求的应用场景。

目前Java虚拟机中主要的并发回收器有CMS和ZGC。

// 设置JVM参数使用CMS回收器
java -XX:+UseConcMarkSweepGC HelloWorld.java
 
// 设置JVM参数使用ZGC回收器
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC HelloWorld.java

4 G1回收器

G1回收器采用分代收集的思想,可以在较短时间内完成一次部分区域的回收工作,并且避免产生大量内存碎片。G1回收器还可以利用多个CPU和多个线程并行处理垃圾回收任务,因此能够很好地适应不同的应用场景。虽然G1回收器在某些方面优于CMS和ZGC,但仍然存在一些问题,例如在某些情况下会出现卡顿现象。

// 设置JVM参数使用G1回收器
java -XX:+UseG1GC HelloWorld.java

四、性能优化

除了选择合适的垃圾回收器外还可以通过减少垃圾产生、调整JVM参数等方式进行性能优化

1 减少垃圾产生

Java程序中可能会产生大量临时对象,这些对象会给垃圾回收造成额外的负担。因此可以通过以下方式来减少垃圾对象的产生:

  • 尽量使用基本数据类型而不是包装类型;
  • 合理复用对象,避免在频繁的创建新对象;
  • 避免使用字符串拼接操作,可以使用StringBuffer和StringBuilder代替。

2 选择合适的垃圾回收器

在实际应用中应根据应用的需求来选择合适的垃圾回收器。如果应用对响应时间有严格要求,可以选择并发回收器;如果应用需要高吞吐量,可以选择并行回收器;如果需要综合性能和内存占用率,可以选择G1回收器。

3 调整JVM参数

调整JVM参数可以提高Java程序的性能,其中常见的一些参数包括:

  • -Xms:用于设置Java堆内存初始大小;
  • -Xmx:用于设置Java堆内存最大可用大小;
  • -XX:+HeapDumpOnOutOfMemoryError:用于在Java堆内存溢出时自动生成堆转储文件。
// 设置JVM堆内存大小为1G
java -Xms1g -Xmx1g HelloWorld.java

// 设置在Java堆内存溢出时自动生成堆转储文件
java -XX:+HeapDumpOnOutOfMemoryError HelloWorld.java

五、Java常见问题与解决方法

1 内存泄漏问题

内存泄漏

内存泄漏指的是程序在申请内存后,无法释放已经使用完毕的内存空间。这些被遗忘的指针将导致系统占用的内存不断增大,最终导致 OutOfMemoryError 异常。

如何避免内存泄漏

以下措施可以避免内存泄漏:

  • 及时清理无用的对象,避免过度缓存。
  • 使用弱引用(WeakReference)或软引用(SoftReference)来引用某些对象。
  • 将类实现为私有静态类(private static class),以便于垃圾回收。

2 Full GC频繁

Full GC

Full GC 全称“Full Garbage Collection”,当堆空间中没有足够的连续空间时,会触发 Full GC。这个过程会清理整个堆空间,包括 Young,Old 和 Perm 区域。

Full GC触发的原因有哪些

以下几种情况会触发 Full GC:

  • 老年代空间不足。
  • 永久区(PermGen)空间不足。
  • 显式的调用 System.gc() 方法。

如何优化Full GC

以下措施可以优化 Full GC:

  • 增大堆空间大小,以减少频繁 Full GC 的发生。
  • 避免创建过多的对象,控制对象的生命周期。
  • 将长生命周期的对象放入老年代,避免频繁 Young GC。
  • 使用可自行扩展的缓存(如 ConcurrentHashMap、Guava Cache)避免 Perm 区过度膨胀。

3 OutOfMemoryError异常

OutOfMemoryError

OutOfMemoryError 是 Java 虚拟机抛出的一种错误,意味着应用程序在申请内存时发现 JVM 中没有足够的空间分配给新的对象。

如何避免OutOfMemoryError

以下措施可以避免 OutOfMemoryError:

  • 加大堆空间和 Perm 空间的大小。
  • 减少 map、list、hashset 等数据结构不必要的使用。使用 Map、List 等数据结构时,应该在不需要使用的时候清空或移除其中的元素。
  • 避免使用递归,改用迭代。
  • 及时释放对象占用的资源。

六、案例分析

1 CMS回收器在大内存场景下的优化实践

CMS回收器的优点与缺点是什么

CMS 回收器的优点在于它使用并发算法,在回收垃圾对象时不需要停顿应用程序的运行。但由于它采用先标记再清理的算法,因此可能会出现空间碎片导致空间不足。

在大内存场景下使用CMS回收器时有哪些优化方法

  • 配置好目标回收时间(-XX:CMSInitiatingOccupancyFraction),避免执行 Full GC 的频率过高。
  • 执行 Full GC 前尽量通过调整阈值、设置晋升阈值、分配新生代空间来尽量避免进入 Full GC,并控制老年代的空间。例如:-XX:MaxTenuringThreshold、-XX:+UseAdaptiveSizePolicy、-XX:MaxGCPauseMillis 等参数的设置。
  • 避免在有限内存上运行 JVM 和应用程序。JVM 进程和操作系统的其他进程都会占用一定的系统资源,如进程表、文件句柄等,将会对堆空间大小造成影响。
  • 使用 JDK 提供的性能分析工具定位问题。

2 G1回收器的使用及优化技巧

G1 回收器相对于 CMS 回收器的优缺点有哪些

G1 回收器的优点在于它使用分代垃圾回收算法,为每个区域设置预期暂停时间,具有更高的清理效率和更小的暂停时间。同时可以避免出现空间碎片的情况,具有更好的垃圾回收能力。缺点是它使用了更多的线程来执行垃圾回收算法,可能导致系统资源的占用过多,从而影响应用程序的性能。

使用G1回收器时应该注意哪些问题

  • 配置好目标暂停时间(-XX:MaxGCPauseMillis),使得垃圾回收所需要的时间不会超过预期暂停时间。
  • 增加年轻代区域大小(-XX:G1NewSizePercent)和减少晋升阈值(-XX:G1MaxTenuringThreshold)以防止频繁的对象晋升到老年代。
  • 配置完整GC周期(-XX:G1HeapWastePercent)和调堆的参数(-XX:InitiatingHeapOccupancyPercent)确保完整GC的数量不会超过目标值,并且对于扩展GC堆空间足够保留未清理对象。
  • 多测试不同参数配置,并使用 JDK 提供的性能分析工具进行性能监控和调优。
  • 避免在 G1 巨大的堆上部署,以防止日志或调试信息占用过多内存资源。

猜你喜欢

转载自blog.csdn.net/u010349629/article/details/130876763