JVM GC

JVM GC

虽然内存分配和GC都已经“自动化”了,但内存回收和GC在很多时候都是影响系统性能、并发能力的主要因素之一,并且当我们需要排查各种内存溢出和内存泄露时,你就需要深入底层去了解JVM GC的原理了,以实施对它的监控和调节。本次主要讲解JVM GC的相关内容。

理解JVM GC的方式只需要知道三个问题就行了:

  • 那些内存需要回收?
  • 怎么回收?
  • 什么时候回收?

1 哪些内存需要回收?

先回忆一下Java内存区域的划分,共有:虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、程序计数器(Program Counter Register)、Java堆(Heap)、方法区(Method Area)。

我们知道虚拟机栈、本地方法栈、程序计数器①所占用的内存基本上在类结构确定下来的时候就明确了,②且都是线程私有的,生命周期与线程相同。所以它们的内存分配和内存回收都是确定的,不需要我们过多的去考虑。

而Java堆、方法区则不一样,他们通常只有在程序处于运行期间才能知道到底要创建哪些对象,所以对他们的内存分配和内存回收都是动态的,所以GC主要关注的就是这两个区域。

2 如何回收?

2.1 “判决”

“判决”的目的自然是:确定哪些对象“活着”、哪些对象“死了”。

过去会采用引用计数算法(给对象添加一个引用计数器,每被引用一次就+1;引用失效则-1。当引用为0时,说明对象“死了”,则GC进行回收),但它却解决不了对象间相互循环引用的问题,而容易导致内存泄露。所以很少使用了。

现在主流的算法是——可达性分析算法,思想非常简单:①我们可以将对象看做“节点”,将对象间的引用看做“路径”,那么我们便可以依次画出连通图(图论);②紧接着只要我们找到了一系列必然存活着的“节点”作为根节点(GC Root),再向下进行搜索,就能够将可达和不可达的节点区分出来;这样就解决了对象间相互循环引用的问题,由此我们就确定出来了哪些对象“活着”,哪些对象“死了”(并不一定会马上被回收掉,进行”缓刑“)。

那么哪些对象可作为GC Root呢?

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

2.2 ”缓刑“

在可达性分析算法中被确定不可达的对象并不会马上就被回收掉,而是先执行“缓刑”,即它们还需经过至少两次标记才会被GC回收掉。

大致流程:确定没有与GC Root相连的引用链时,此对象会执行第一次标记;筛选此对象是否需要执行finalize()方法;如果是,则进入F-Quene队列,等待执行finalize()方法;否则,执行第二次标记,被回收。执行finalize()方法时,如果此对象重新与引用链上的对象建立起关联,则将其移除“即将回收”集合,存活;否则回收。

**注:**如果对象已执行过一次finalize()方法,并成功“逃脱”回收,那么第二次再执行“缓刑”的时候,则不会再执行finalize()方法了。即在这个过程中每个对象只有一次执行finalize()方法的机会。

附上流程图,以方便理解:
在这里插入图片描述

2.3 常见垃圾收集算法

说道算法了,可能好些同学又有些害怕了,其实完全没有必要,因为垃圾收集算法的思想都比较简单,很容易理解!(如果你学过操作系统这门课的话,这对你来说一定是小菜一碟)

2.3.1 标记-清除算法(Mark-Sweep)

顾名思义,分为两个阶段:标记、清除。先标记出来需要回收的内存对象,再进行统一回收。

在这里插入图片描述

(用excel画图,颜色选了好长一阵…)

作为最基础的收集算法,自然是跑不了有很明显的缺点了:

  • 效率问题:标记和清除两个过程效率都不高。
  • 空间问题:清除之后,会产大量不连续的碎片,下次要是来个大一点的对象不方便分配内存空间。
2.3.2 复制算法(Copying)

复制算法是为了解决标记-清除算法中的效率、空间问题出现了。它将内存空间分为大小相等的A、B两块,每次只使用其中的一块,当A块内存用完后,就将还存活的对象复制到B块,然后把A块已使用的内存空间一次清理掉。

在这里插入图片描述

这样便完美的解决了标记-清除算法中的效率和空间问题。但最大的缺陷是,内存空间缩小成了以前的一半大小…

2.3.3 标记-整理算法(Mark-Compact)

针对复制收集算法中存活率较高,复制次数增多导致效率降低的问题,提出了标记-整理算法。

与标记-清除算法类似,同样先标记出可回收的对象,但后续是将所有存活的对象向一端移动,然后清理掉端边界以外的内存空间。

在这里插入图片描述

2.3.4 分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用“分代收集“算法,主要是根据对象存活的周期,将Java堆划分为新生代与老年代,再以它们各自的特点采用最适用的算法。算法同上,并没有任何实质改变。

生存周期特征采用算法
新生代对象存活率低复制算法
老年代对象存活率高标记-清理算法、标记-整理算法

其中对新生代的复制算法有所优化,不再按照1:1的比例来划分内存空间,而是采用Eden(伊甸)区和两个Survivor区,内存空间大小比例为8:1:1的方式来划分。如下图:

在这里插入图片描述
①内存分配时,每次使用Eden和其中一块Survivor区;

②当回收时,将Eden和Survivor中还存活的对象,复制到另一块Survivor区中去。当另一块Survivor区域空间不足时,将会分配到老年代中去;

③清理掉Eden和刚才使用过的Survivor区。

3 什么时候回收?

把这个问题放在最后的原因是,弄清楚如何回收是前提。

GC分为Minor GC与Full GC,只要清楚了它们的触发条件,也就清楚了什么时候进行GC了!

  • Minor GC:
    • 当Eden区中没有足够空间时。
  • Full GC:Full GC一般都会有一次Minor GC
    • 调用System.gc时,不一定执行;
    • 老年代空间不足;
    • 方法区空间不足;
    • 通过Minor GC后进入老年代的平均大小 > 老年代的可用内存。
    • 由Eden区、From Space区向To Space区复制时,对象大小 > To Space可用内存,则把该对象转存到老年代,且老年代的可用内存 < 该对象大小。

(以上内容均整理自《深入理解Java虚拟机:JVM高级特性与最佳实践 第二版》)

JVM的垃圾回收机制是Java语言的一个核心特性,它负责自动管理程序运行时分配的内存[^1]。通过垃圾回收,JVM能够检测并释放不再使用的对象,从而避免内存泄漏和资源浪费[^2]。 ### 垃圾回收的基本概念 在Java中,垃圾回收主要关注的是堆内存中的对象。当一个对象不再被任何引用变量所指向时,这个对象就被认为是可回收的。JVM使用了多种算法和技术来确定哪些对象是可以安全地回收的,包括标记-清除、复制、标记-整理等[^4]。 #### 标记-清除算法 标记-清除算法是最基础的一种垃圾收集算法。它分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。这种方法简单有效,但是存在效率问题以及内存碎片问题[^1]。 ```java // 示例代码 - 创建一些临时对象 public class GCDemo { public static void main(String[] args) { for (int i = 0; i < 10000; i++) { Object obj = new Object(); // 模拟创建大量短生命周期对象 } } } ``` #### 复制算法 为了解决标记-清除算法的内存碎片问题,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况了[^2]。 #### 标记-整理算法 标记-整理算法与标记-清除算法类似,但不同之处在于,在完成标记之后,存活的对象会被移动到内存的一端,然后直接清理掉边界以外的内存。这有助于减少内存碎片,并且提高了内存利用率[^4]。 ### JVM垃圾回收器 JVM提供了几种不同的垃圾回收器,每种都有其特点和适用场景: - **Serial Collector**:单线程工作,适用于小型应用。 - **Parallel Scavenge**:多线程工作,适合于数据量大、多核处理器的应用。 - **CMS (Concurrent Mark Sweep)**:以最短的停顿时间为目标的收集器,适用于重视响应速度的服务。 - **G1 (Garbage First)**:面向服务端应用的新一代垃圾收集器,可以更高效地利用大内存。 选择合适的垃圾收集器对于优化应用程序性能至关重要。例如,如果您的应用对延迟敏感,则可能倾向于使用CMS或者最新的ZGC/Zoncolic GC;而对于吞吐量要求较高的应用,则可以选择Parallel Scavenge[^1]。 ### 调优方法 为了提升Java应用的性能和稳定性,可以通过以下方式进行调优: - **调整堆大小**:合理设置初始堆大小(-Xms)和最大堆大小(-Xmx),避免频繁的Full GC。 - **选择适当的垃圾收集器**:根据应用的特点选择最适合的垃圾收集器。 - **分析GC日志**:通过开启GC日志记录(-XX:+PrintGCDetails -XX:+PrintGCDateStamps > file.log),可以帮助识别潜在的问题区域。 - **优化代码**:减少不必要的对象创建,及时释放不再使用的资源,利用缓存机制等。 通过对JVM垃圾回收机制的理解及相应的调优实践,开发者可以显著提高Java应用的性能表现和系统的整体稳定性[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值