【JVM】一篇聊透内存泄露,栈内存溢出,JVM中的垃圾回收器超详细

在这里插入图片描述


更多相关内容可查看

内存泄露

原因:对象引用未清理,导致垃圾回收器无法回收,也就是大家常说的OOM。

示例

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakExample {
    private List<String> list = new ArrayList<>();

    public void addString(String value) {
        list.add(value); // 持续添加字符串,导致内存无法释放
    }

    public static void main(String[] args) {
        MemoryLeakExample example = new MemoryLeakExample();
        while (true) {
            example.addString("Leak " + System.nanoTime());
        }
    }
}

-Xmx:是JVM的一个参数,用于设置Java程序的最大堆内存大小。例如 -Xmx512m 表示最大堆内存为512MB。合理配置这个参数有助于避免OOM(OutOfMemoryError)。详细的JVM调优可查看JVM调优原理、思路、真正意义上解决性能瓶颈(附实际调优案例)


栈内存溢出

原因:递归调用未设定终止条件。

示例

public class StackOverflowExample {
    public static void recursiveMethod() {
        recursiveMethod(); // 无限递归调用
    }

    public static void main(String[] args) {
        try {
            recursiveMethod();
        } catch (StackOverflowError e) {
            System.out.println("Stack overflow occurred!");
        }
    }
}

无限递归导致栈空间耗尽。是因为递归反复调用方法,调用方法的时候会创建栈帧,每个栈帧存储该方法的局部变量和调用信息。当递归没有终止条件时,这些栈帧不断积累,最终导致栈空间耗尽,从而引发栈内存溢出。

-Xss :是JVM的一个参数,用于设置每个线程的栈大小。例如,-Xss512k表示每个线程的栈大小为512KB,所以当栈内存溢出的时候,可以通过调节Xss参数来解决

强引用、软引用、弱引用、虚引用的区别?

强引用:最常见的引用类型,如 Object obj = new Object();。只要强引用存在,垃圾回收器不会回收被引用的对象。

软引用:可以用来描述一些还有用但不一定需要的对象。在内存不足时,垃圾回收器会回收软引用指向的对象,适用于缓存场景。

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}

弱引用:比软引用更弱,当垃圾回收器工作时,如果一个对象只被弱引用引用,就会被回收。适合用于不需要强保持的对象。

虚引用:几乎没有使用场景,主要用于跟踪对象的生命周期。当一个对象只被虚引用引用时,它仍然可以被回收。虚引用与引用队列结合使用,允许在对象被回收时收到通知。

判断对象是否可以被回收

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0
    时就可以被回收。它有一个缺点不能解决循环引用的问题;
  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots
    没有任何引用链相连时,则证明此对象是可以被回收的

JVM 运行时堆内存分代方式

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。

在这里插入图片描述

  • 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数–XX:NewRatio来指定 )
  • Eden: from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 )

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。

新生代

  • Eden 区 :Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
  • Servivor from 区: 上一次 GC 的幸存者,作为这一次 GC 的被扫描者,扫描完还存活的就放到Servivor to 区中了。
  • Servivor to 区: 保留了Servivor from后 MinorGC 过程中的幸存者

MinorGC 原理:

  • 把 Eden 和 ServivorFrom区域中存活的对象复制到 ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo不够位置了就放到老年区);
  • 清空 Eden 和 ServicorFrom 中的对象;
  • ServicorTo 和 ServicorFrom 互换,原ServicorTo 成为下一次 GC 时的 ServicorFrom区。

老年代

  1. 对象存活时间:老年代主要存放那些在多次垃圾回收(GC)中仍然存活的对象。通常,经过几轮的Minor GC后,仍然存活的对象会被转移到老年代。

  2. 内存管理:老年代的垃圾回收通常较少且耗时,因为老年代的对象通常较大,且需要全堆扫描。相较于新生代,老年代的垃圾回收更为复杂,常见的方式有Full GC。

  3. GC策略:老年代的垃圾回收策略与新生代不同。老年代使用的GC策略通常是标记-清除或标记-整理,而新生代多使用复制算法。

  4. 堆内存分配:在堆内存中,老年代的空间通常比新生代更大,适合存放长期存在的对象。通过分代收集,JVM能更高效地管理内存,减少频繁的内存回收。

永久代

  • 永久代是方法区的一部分,但它的大小通常是固定的,并且内存不够时可能引发 OutOfMemoryError。
  • 垃圾回收主要发生在Full GC期间,因此永久代的GC频率相对较低。

Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区

JVM中一次完整的GC流程

  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1
  • 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到
    Survivor区。
  • 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
  • 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor
    GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
  • Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。

垃圾回收算法

垃圾回收(Garbage Collection,GC)是自动内存管理的一部分,旨在释放不再被使用的内存。不同的垃圾回收算法有不同的实现和用途

1. 标记-清除算法(Mark-and-Sweep)

原理

  • 标记阶段:从根对象开始,遍历所有可达的对象,并将其标记为"活着"。
  • 清除阶段:遍历整个堆,清除未被标记的对象,释放其占用的内存。

优点

  • 简单易实现,能够有效回收不再使用的对象。

缺点

  • 可能导致内存碎片。
  • 清除过程可能导致应用程序暂停(停顿时间长)。

2. 标记-整理算法(Mark-and-Compact)

原理

  • 标记阶段:同标记-清除,标记所有可达对象。
  • 整理阶段:将所有标记的对象移动到堆的一端,释放出一块连续的内存空间。

优点

  • 解决了内存碎片的问题,得到了连续的内存区域。

缺点

  • 实现复杂,相比标记-清除算法,需要更多的内存复制和调整指针。

3. 复制算法(Copying)

原理

  • 将堆分为两个区域(from空间和to空间),只使用其中一个空间进行分配。
  • 当其中一个空间用满时,复制所有活着的对象到另一个空间,并清空当前使用的空间。

优点

  • 内存分配简单,无需碎片整理,所有对象都在连续空间。
  • 每次回收后都可一次性释放整块空内存。

缺点

  • 需要额外的内存空间(至少一倍于活对象的大小)。
  • 只适合新生代。

4. 分代收集算法

原理

  • 将堆分为新生代和老年代,采用不同的回收策略。
  • 新生代采用复制算法,老年代采用标记-清除或标记-整理算法。

优点

  • 考虑到对象的生命周期,通过分代收集提升了回收效率。
  • 可以快速回收新生代中的短命对象,减少停顿时间。

缺点

  • 设计和实现较为复杂,对老年代的配置和回收策略需要合理调优。

5. 清除-修改算法

原理

  • 通过维护一个记录对象引用的表,直接标记和清理不再使用的对象。
  • 可通过引用计数技术实现,对象引用计数为零时自动释放。

优点

  • 允许对象之间的引用关系保持实时更新。

缺点

  • 不适合循环引用的情况,可能导致内存泄漏。
  • 维护引用计数的开销较大。

6. 增量垃圾回收

原理

  • 在程序的执行过程中,将垃圾回收操作与应用程序的执行交替进行,分段进行。

优点

  • 减少了程序的停顿时间,保持了系统的响应性。

缺点

  • 实现复杂,可能需要在多个线程间进行协调。

7. 并发垃圾回收

原理

  • 在应用程序运行的同时进行垃圾回收,使用多个线程并行执行,以提高回收效率。

优点

  • 改善了停顿时间,适用于需要高响应性的大型应用。

缺点

  • 实现复杂,可能对应用程序的性能造成影响,且需要处理多线程中的数据一致性。

jvm中的垃圾回收器的种类

1. 串行垃圾回收器(Serial Garbage Collector)

原理

  • 使用单线程进行所有的垃圾回收操作。
  • 在垃圾回收期间,所有应用线程都会暂停。

算法

  • 采用标记-清除(Mark-and-Sweep)和复制(Copying)算法。

适用场景

  • 小型应用或单线程环境。
  • 堆内存较小(例如小于100MB)。

换用情境

  • 当CPU资源有限或程序对响应时间和性能要求不高时,可以选择该垃圾回收器。

2. 并行垃圾回收器(Parallel Garbage Collector)

原理

  • 采用多线程并行处理的方式来执行垃圾回收,适当利用多核处理器。
  • 在回收过程中,所有用户线程都会被暂停。

算法

  • 使用标记-清除和复制算法,结合并行执行。

适用场景

  • 需要高吞吐量的大型应用程序。
  • 合适在处理大数据量的场景中。

换用情境

  • 若应用对吞吐量敏感且可以接受较长的停顿时间,适合使用并行垃圾回收器。

3. CMS(Concurrent Mark-Sweep)垃圾回收器

原理

  • 将垃圾回收过程分为多个阶段,包括初始标记(单线程)、并行标记、预清理、重新标记和清理。
  • 大部分的回收工作是在应用线程同时运行时进行,减少了停顿时间。

算法

  • 基于标记-清除算法,采用并发标记和清扫。

适用场景

  • 对延迟要求较高的应用,如Web应用、金融应用等。
  • 对停顿时间有较高要求且运行在多核处理器上。

换用情境

  • 当出现较大的停顿时间时,可以考虑切换到CMS,以降低停顿时间。

4. G1(Garbage First)垃圾回收器

原理

  • G1首先进行初始标记,标记所有活跃的根对象,在标记完成后,G1会识别哪些区域包含大量的垃圾对象,优先收集这些区域。这是“垃圾优先”的核心思想,G1采用复制算法,存活的对象会从回收区域复制到其他区域,减少内存碎片

算法

  • 结合了标记-整理和复制算法。

适用场景

  • 大规模应用和需要可预测停顿时间的应用。
  • 堆大小较大,且需要动态调整内存利用率的情况。

换用情境

  • 如果在使用CMS时发现内存碎片问题,可以考虑换成G1以更好地管理内存。

5. ZGC(Z Garbage Collector)

原理

  • 采用分代收集和并发标记,在处理大堆时能够实现低延迟。
  • 整个回收过程尽可能使应用线程保持运行状态。

算法

  • 采用并发标记集(Concurrent Marking Set)和并发清理算法。

适用场景

  • 大内存环境下,需要极低或可控的停顿时间的应用,如云计算和实时服务。

换用情境

  • 如果应用对低延迟要求极高,而传统的垃圾回收器无法满足需求时,可以考虑使用ZGC。

6. Shenandoah垃圾回收器

原理

  • 采用并发标记和并发整理,旨在实现在大堆内存中快速回收。

算法

  • 使用分区标记和并发清理算法。

适用场景

  • 需要低停顿时间的应用,特别是在处理大量内存和高吞吐量的情况下。

换用情境

  • 如果需要实时响应并且其他GC策略无法提供足够低的停顿时间,可以选择Shenandoah。

上述这些是广泛使用或更新的垃圾回收器

新生代垃圾回收器

  • ParNew(Serial+多线程):ParNew是一个并行的年轻代垃圾回收器,它与其他的并行回收器(如CMS)配合使用。写入数据时,会使用多线程来加速垃圾回收过程。
  • Serial:这是一个单线程的Young GC,适用于小型应用和资源受限的环境。
  • Parallel:也称为Parallel Scavenge,它是一个多线程的Young GC,设计目标是优化吞吐量。

老年代垃圾回收器

  • Concurrent Mark-Sweep(CMS):CMS是一个以速度为主的低停滞时间垃圾回收器,它并行地进行标记和清除操作。适用于需要较快响应时间的应用,比如Web应用。
  • G1 (Garbage-First) GC:G1GC是现代的垃圾回收器,旨在实现可预测的停顿时间和高吞吐量。G1将堆划分为多个小的区域(Region),同时进行标记和回收,通过预测最优的回收区域来提高效率。
  • Serial Old:这是Serial GC的老年代实现,适用于小型应用或资源有限的环境。

双亲委派模型

原理:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

为啥要有这个东西:在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来一杯龙舌兰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值