Java设计思想深究----JVM垃圾回收(GC GarbageCollection)(图文)

本文解析了Java中自动垃圾回收机制,包括新生代、老年代和永久代的内存管理,以及为何Java开发者无需担心内存泄漏。讨论了内存分配、GC频率与内存泄漏平衡及调优策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 

一、回收的是什么垃圾 Garbage?

二、在Java环境里,为什么开发人员就没担心过内存泄露的问题了?

三、自动垃圾回收机制

四、堆分代思想

4.1 新生代 Young Generation

4.2 老年代 Old Generation

         4.3 永久代 Permanent Generation

五、一些相关的问题

5.1 新生代与老年代的内存比例

5.2 新生代中Eden区与SurvivorSpace区的内存比例

5.3 如何对GC进行调优?


回收的是什么垃圾 Garbage?

我们都知道,编程是思想在内存上的映射,而内存的本质职能还是数据的快速读写。

以C语言这种面向过程的语言为例,

#include <stdio.h>
int main()
{
    int a = 0;
    return 0;
}

当我们定义了一个变量a,并赋值,栈内存上有一个整形大小的区域便被申请,并被直接引用,当主函数return 0时,进程结束,该内存就会被操作系统自动free释放掉(物理上的擦除)。

许多初级使用只使用堆栈变量。

堆栈很好,因为它是自动的,但它也有两个缺点:(1)编译器需要提前知道变量的大小,(2)堆栈空间有点有限。例如:在Windows中,在Microsoft链接器的默认设置下,堆栈被设置为1mb,并不是所有的变量都可用。

如果你在编译时不知道你的数组有多大,或者你需要一个大的数组或结构体,你需要“B计划”。

方案B被称为“堆”。通常可以创建操作系统允许的大小的变量,但必须自己动手。

#include <stdio.h>
int main()
{
    int size = 4;
    int *p = (int *)malloc(size);
    return 0;
}

这样我们便在堆内存上申请了10个整型大小的内存,并用整形指针变量p指向了这部分首地址:

之后便可以主动使用这部分内存进行读写操作。处于堆内存上的内存段,并不会被Stack的自动回收机制回收,待主程序运行结束后,便会由操作系统自动回收动态分配的内存。

但是,我们都知道,内存上的“土地”,有限的同时,寸土寸金

为此,C语言提供了回收机制,回收指定的malloc申请的区域:

free(p);

如果使用结束后,如果没有对这部内存进行回收,之后的此处被占用的内存在主程序结束前就是实打实的浪费,程序将占用比实际需求的更多的内存空间,这种情况被称为“内存泄漏”,“泄漏的”内存不能用于任何事情,直到你的程序结束和操作系统恢复它的所有资源。

因此,对于C语言开发者,不仅有动态空间的申请需求,还需要严谨的在合适的时刻释放掉自己申请的堆内存,最大可能的避免内存泄露的问题。更棘手的是,如果错误的释放了malloc内存,后续引用指向的数据已被擦除,继续使用将造成更严重的后果。

综上所述,我们得到了“垃圾”的定义:作用域结束后的内存片段称之为内存垃圾

在Java环境里,为什么开发人员就没担心过内存泄露的问题了?

我们都知道,Java语言的一大特性就是“帮”开发者避免了一个很棘手的问题:

什么时候动态申请内存,什么时候释放这块内存?

在Java环境里,这些任务全部委托给了JVM(Java Visual Machine),就是老生常谈的Java虚拟机。同时为开发者提供了一套面向对象开发的框架,让开发者专注业务,不用担心物理层发生的事情,如果读者对类与对象实例化的过程有所模糊,请阅读这篇博文:

Java设计思想深究----类与对象实例化(图文)_kevinmeanscool的博客-优快云博客

自动垃圾回收机制

以上博文为基础,我们了解了JVM对于对象生命周期的大部分处理(加载->连接->初始化->使用)。还剩下一个问题:JVM如何处理对象生命周期的最后一步----卸载?

回答这个问题前,首先要清楚JVM是一个架设在“内存道路上”的“立体工厂”(JDK1.8为例):

整理这个严肃的组件图,得到:

 JVM把一个对象卸载的过程称作:自动垃圾回收机制(Garbage Collection)

这个过程的目的只有一个:寻找 Java堆中的无用对象的实例,清理掉。

过程大致是这样的:

  • Marking 标记垃圾点

根据某个规则(最后会介绍这个标准)标记符合规则的实例为 “垃圾Garbage”,制作一个“待回收垃圾点”的引用列表。

  • Normal Deletion 回收已标记的垃圾

清理待回收垃圾点(擦除被标记的内存区域),“待回收垃圾点”的引用列表逻辑上变为了“空闲点”引用列表,内存分配器将保存“空闲点”的引用列表,并在需要内存分配时搜索空闲空间。

  • Deletion with Compacting 整理空间

整理内存,使得非空闲点紧凑的同时,空闲点也紧凑排列,方便后续所需时可使数据呈现高内聚。

 这样便结束了一轮的垃圾回收,内存空间又如回收前一般。在标记垃圾点时,提到了依据一个标准,每个虚拟机都有自己使用的标准,JVM是HotSpot虚拟机,使用根搜索算法标记实例

  • 根搜索算法:基本思想就是选定一些对象作为 GC Roots,并组成根对象集合,然后从这些作为 GC Roots的对象作为起始点,搜索所走过的引用链( ReferenceChain)。如果目标对象到 GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达,则说明目标对象是可以被回收的对象。这个回收标志保存在堆内存中的实例对象头中。
标记垃圾点后

回收垃圾

作为 GC Root的对象可以主要分为四种:

  1. JVM栈中引用的对象;

  2. 方法区(MetaSpace)中,静态属性引用的对象;

  3. 方法区(MetaSpace)中,常量引用的对象;

  4. 本地方法栈中,JNI(即Native方法)引用的对象;

在 JDK1.2之后,Java将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。

    1. 强引用

如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OOM 错误,使程序异常终止。比如String str = "hello"这时候str就是一个强引用。

    2. 软引用

内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出OOM异常。

    3. 弱引用

如果一个对象具有弱引用,在垃圾回收时候,发现即回收

    4. 虚引用

如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。

到这里就结束了对自动垃圾回收机制(GC)的基本介绍。

然而实际的情况是,Java开发中,对象的生命周期绝大部分是短暂的(临时变量)。 如果以以上的机制去进行,我们可以模拟一下一个过程:

经历过n次GC后,全局对象被整理的紧凑,此时程序执行到一个体量不大的方法中:

方法执行结束后,引用被Stack回收,局部临时引用的堆内存变为逻辑上的“垃圾”:

 这时候根据自动垃圾回收机制,垃圾回收器需要从头到尾遍历一遍内存,挨个根据根搜索算法检验是否为垃圾点,那么这个遍历的周期就为Cycle。 

Cycle设置的太小,高频率的遍历对安全JVM的机器而言是一笔沉重的资源开销,显然不合理。

Cycle设置的太大,逻辑上已经是垃圾的实例无法及时得到回收,内存泄漏问题会愈发严重。

此时,基础自动垃圾回收机制,产生了一个矛盾:JVM垃圾回收频率 vs 内存泄漏的程度

 对此为了优化GC的过程,JDK推出了堆分代思想

堆分代思想

JVM的内存被分为了三个主要部分:新生代(Young),老年代(Old)和永久代(Permanent)。

分代的依据就是对象实例的年龄(每经历过一次GC,年龄age+1,这个年龄保存在堆内存中的实例对象头中)。

 新生代 Young Generation

所有新产生的对象全部都在新生代中, Eden区保存最新的对象实例,即保存age=0的对象实例。

久而久之,Eden区将会达到容量极限,这时垃圾回收器便执行一次小GC(小GC是StopWorld事件,会暂停其他所有线程,只进行小GC事务)。

小GC执行过后,根据根搜索算法,不具有可达性的实例所占空间将被回收。同时,具有可达性的对象实例因此而“成长”(age=1),进入空的Survivor Space(存活下来后进入幸存区,幸存区分为大小一致的两块,Eden对象成长后,会选择空的幸存区进入,幸存区分两块的原因下文马上讲)。

 随着一段时间过后,Eden区肯定会再次达到容量的极限,触发小GC,小GC会导致整个新生代不具有可达性的对象实例被回收,具有可达性的对象实例集体成长后进入空的Survivor Space:

此时,Eden区会腾空,SurvivorSpace会腾出一个空的区。你可以意识到了,是不是下一次小GC,Eden+非空SurvivorSpace ->又会经历回收成长->移动到空的SurvivorSpace,这是为了避免只有1个SurvivorSpace产生内存碎片

使内存碎片紧凑会导致JVM产生一笔额外的资源开销,因此JVM利用反复在“新区域”有序排列,从而避免内存碎片化的情况发生。上述整体提到的是新生代管理内存采用的算法为 GC复制算法( CopyingGC),也叫标记-复制法(原理并不难)。

不断的循环上述过程,一直具有可达性的对象的实例在不断成长age+1,直到达到JVM参数设定的新生代最大年龄(参数 MaxTenuringThreshold设定,默认值为 15)时,JVM会请这些对象进入到老年代。

老年代 Old Generation

老年代用来存储活时间较长的对象。

年龄达到15的对象实例们,被JVM一个接一个的移动到老年区(Tenured),直到达到老年区的容量极限时,JVM将进行一次大GC(这也是一个 Stopthe world事件,会暂停其他所有线程,优先执行大GC,大GC会对整个堆内存进行遍历,所以也称作FullGC),同样的,根据根搜索算法将不可达的对象实例回收,可达的对象,成长,根据年龄选择对应的存储区域。

你可能已经发现了,老年区并没有像SurvivorSpace那样分2个区避免内存碎片,所以老年区的策略是回收后使可达实例紧凑地存储,就是上文提到的自动垃圾回收机制图文中的过程。

永久代 Permanent Generation

 永久代位于方法区(JDK1.8后称作MetaSpace 元空间),主要存放元数据,例如 Class、 Method的元信息。

元信息加载过程可见:

Java设计思想深究----类与对象实例化(图文)_kevinmeanscool的博客-优快云博客

实际上与 GC要回收的对象其实关系并不是很大,我们可以几乎忽略其对 GC的影响。除了 JavaHotSpot这种较新的虚拟机技术,会回收无用的常量和的类,以免大量运用反射这类频繁自定义 ClassLoader的操作时方法区溢出。

一些相关的问题

新生代与老年代的内存比例

        默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

新生代中Eden区与SurvivorSpace区的内存比例

默认的,Eden区与SurvivorSpace区的比例为 8:2,Eden区远大于幸存区,这是因为据统计,绝大部分的对象实例的生命周期都是极短的。于是,计算机中著名的28准则被应用到了这里。其中SurvivorSpace区的两个组成区是等同大小的,即1:1。

如何对GC进行调优?

 一般而言, GC不应该成为影响系统性能的瓶颈。

现在都是分代 GC,调优的思路就是尽量让对象在新生代就被回收,防止过多的对象晋升到老年代,减少大对象的分配。对 GC 进行完整的监控,监控各年代占用大小、YGC (小GC)触发频率、Full GC (大GC)触发频率,对象分配速率等等。

然后根据实际业务情况,对晋升的年龄调整、各分区大小调整等等。

参数引用:JVM之GC常用配置参数_贾红平-优快云博客_gc参数

自此已结束,作者水平有限,如有疑论,不吝指教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值