JAVA虚拟机(二)——GC垃圾回收

目录

JAVA运行时内存:

新生代:

Eden:

From Servivor:

To Servivor:

老年代:

永久代:

JAVA8 与元数据:

GC垃圾回收:

判断一个对象是否可被回收:

引用计数算法:

 根搜索算法(可达性分析):

四大引用类型:

1、强引用:

2、软引用:

3、弱引用:

4、虚引用:

垃圾收集算法:

标记-清除算法 Mark-Sweep:

复制算法 Copying:

标记-整理算法:

分代收集:

垃圾收集器

1、Serial 垃圾收集器(单线程、串行、复制算法)

2、ParNew垃圾收集器(Serial+多线程)

3、Parallel Scavenge 收集器(多线程复制算法、高效)

4、Serial Old 收集器(单线程标记整理算法 )

5、Parallel Old 收集器(多线程标记整理算法)

6、CMS 收集器(多线程标记清除算法)

7、G1收集器

内存分配与回收策略

Minor GC 和 Full GC

内存分配策略

Full GC的触发条件



JAVA运行时内存:

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

新生代:

用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发

MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

Eden

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

From Servivor

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

To Servivor

保留了一次 MinorGC 过程中的幸存者。

MinorGC 的过程(复制->清空->互换):

MinorGC 采用复制算法。

老年代:

       主要存放应用程序中生命周期长的内存对象。

       老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

       MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没

有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

永久代:

       指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

JAVA8 与元数据:

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。


GC垃圾回收:

垃圾收集主要是针对堆和方法区进行。

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。


判断一个对象是否可被回收:

引用计数算法:

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

但对于两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

 根搜索算法(可达性分析):

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含 以下内容:

1、虚拟机栈(栈帧中的本地变量表)中引用的对象

2、本地方法栈中引用的对象

3、方法区中类静态属性引用的对象

4、方法区中的常量引用的对象

不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。


四大引用类型:

       无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象 是否可达,判定对象是否可被回收都与引用有关。

 Java 具有四种强度不同的引用类型。

1、强引用:

被强引用关联的对象不可能被回收,即使以后不会被JVM用到,也不会被回收。因此强引用是

造成 Java 内存泄漏的主要原因之一。

使用 new 一个新对象的方式来创建强引用。

例如:Object obj = new Object();

2、软引用:

使用 SoftReference 类来创建软引用。

被软引用关联的对象只有在内存不够的情况下才会被回收。

3、弱引用:

需要用 WeakReference 类来实现

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

4、虚引用:

又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。它不能单独使用,必须和引用队列联合使用。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知,是跟踪对象被垃圾回收的状态。

使用 PhantomReference 来实现虚引用。


垃圾收集算法:

标记-清除算法 Mark-Sweep:

最基础的垃圾回收算法,分为两个阶段,标注和清除。

将存活的对象进行标记,然后清理掉未被标记的对象。

不足:

标记和清除过程效率都不高;

会产生大量不连续的内存碎片,导致无法给大对象分配内存。

复制算法 Copying:

        将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还 存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。主要不足是只使用了内存的一半。

       现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为 大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间。

        每次使用 Eden 空间和From Survivor,在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制To Survivor 空间上,同时把这些对象的年龄+1,最后清理 Eden 和From Survivor。最后,To Servicor 和 FromServicor 互换,原 ToServicor 成为下一次 GC 时的 FromServicor区。

       如果有对象的年龄以及达到了老年的标准,也将赋值到老年代区。(当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中)。

       HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

标记-整理算法:

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象

 

分代收集:

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

新生代使用:复制算法

老年代使用:标记 - 清除 或者 标记 - 整理 算法。


垃圾收集器

 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器。以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

1、Serial 垃圾收集器(单线程、串行、复制算法)

       Serial是最基本垃圾收集器,使用复制算法。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

       优点是简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。

2、ParNew垃圾收集器(Serial+多线程)

它是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,垃圾收集过程中同样也要暂停所有其他的工作线程。

ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。

Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

3、Parallel Scavenge 收集器(多线程复制算法、高效)

其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别,虚拟机根据当前系统运行情况自适应的调节新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数。

4、Serial Old 收集器(单线程标记整理算法 )

新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:

新生代 Parallel Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:

是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机老年代垃圾回收使用。

如果用在 Server 模式下,它有两大用途:

在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。

作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

5、Parallel Old 收集器(多线程标记整理算法)

新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:

是 Parallel Scavenge 收集器的老年代版本。

在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

6、CMS 收集器(多线程标记清除算法)

Concurrent mark sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。

最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。

CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要暂停所有的工作线程。

并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。

重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。

并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。

由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

缺点:

吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。

无法处理浮动垃圾,可能出现 Concurrent Mode Failure浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的 内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代CMS。

标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够 大连续空间来分配当前对象,不得不提前触发一次 Full GC。

7、G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,是一款面向服务端应用的垃圾收集器。相比与 CMS 收集器,G1 收集器两个最突出的改进是:

1. 基于标记-整理算法,从局部(两个 Region 之间)上来看是基于“复制”算法实现,不产生内存碎片。

2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

其它收集器进行收集的范围都是整个新生代或者老年 代,而G1 可以直接对新生代和老年代一起回收。

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域(Region),并且跟踪这些区域的垃圾收集进度。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。

区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几 个步骤:

初始标记

并发标记

最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 面,最终标记阶段需要把Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。

筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期 望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。


内存分配与回收策略

Minor GC 和 Full GC

Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执 行,执行速度会比 Minor GC 慢很多。

内存分配策略

1、对象优先在Eden分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

2、大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3、长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

 -XX:MaxTenuringThreshold 用来定义年龄的阈值。

4、动态对象年龄判断

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老 年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一 半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5、空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败, 如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

Full GC的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1、调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方 式,而是让虚拟机管理内存。

2、老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象 进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收 掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代 的年龄,让对象在新生代多存活一段时间。

3、空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一 次 Full GC。

4JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未 配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不 了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC

5Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能 是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC


本节主要总结了垃圾回收的一些算法与GC垃圾收集器,下一节将介绍类加载机制。

如有帮助,非常荣幸,欢迎点赞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值