GC

概念

在计算机科学中,垃圾收集(GC)是一种自动内存管理方式。

垃圾收集器,或者仅仅是收集器,试图回收垃圾,或者被程序不再使用的对象占用的内存。

垃圾收集是John McCarthy在1959年左右发明的,用于简化Lisp中的手动内存管理

垃圾收集本质上与手动内存管理相反,手动内存管理要求程序员指定释放和返回内存系统的对象。然而,许多系统使用多种方法的组合,包括堆栈分配和区域推断等其他技术。与其他内存管理技术一样,垃圾收集可能会占用程序中全部处理时间的很大一部分,因此可能对性能产生重大影响。有了良好的实现和足够的内存,根据应用程序的不同,垃圾收集可以比手动内存管理更快,反之亦然,过去在次优GC算法中也是如此。

除内存之外的资源(如网络套接字、数据库句柄、用户交互窗口以及文件和设备描述符)通常不会由垃圾收集处理。

用于管理此类资源(尤其是析构函数)的方法也可以用来管理内存,而不需要GC。

一些GC系统允许这样的其他资源与内存区域相关联,在收集时,该区域会导致资源被回收;这就是终结。终结可能会引入限制其可用性的复杂因素,例如停用和回收特别有限的资源之间难以忍受的延迟,或者缺乏对哪个线程执行回收工作的控制。

为什么学习

学习 Java GC 机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。

理解 GC

  • 触发的时机

Eden 满了触发 Minor GC;

老年代满了触发 Full GC,或者小于时被 HandlePromotionFailure 参数强制 Full gc;

可以通过参数配置进行调整。

  • 清理的什么

通过从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。

  • 做了什么

垃圾回收算法。

新生代:停止-复制

老年代:标记-整理

对象是否存活

Java 引用

  • 强引用(Strong Reference)

Object obj = new Object(),这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用(Soft Reference)

它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。

  • 弱引用(Weak Reference)

它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

在JDK1.2之后,提供了WeakReference类来实现弱引用。

  • 虚引用(Phantom Reference)

最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。

详细内容,参见 Java 引用

需要回收的区域

内存模型

在上面介绍的五个内存区域中,有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。

因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。

所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。

判断对象是否是垃圾的算法

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:

(1)找到所有存活对象;

(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

引用计数算法(Reference Counting Collector)

堆中每个对象(不是引用)都有一个引用计数器。

当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。

当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。

当一个对象被垃圾收集时,它引用的任何对象计数减1。

  • 优点

引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利(OC的内存管理使用该算法)。

  • 缺点

难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历(根搜索算法)。

根搜索算法

核心思想

这种算法的基本思路:

  1. 通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。

  2. 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。

  3. 重复 2

  4. 搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

Java和C#中都是采用根搜索算法来判定对象是否存活的。

如下图中的Object5、Object6、Object7,虽然它们三个依然相互引用,但是它们其实已经没有作用了,这样就解决了引用计数算法的缺陷。

根搜索算法

基本概念

  • 根集(Root Set)

所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。

标记可达对象

JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。

下图3.0中所展示的JVM中的内存布局可以用来很好地阐释这一概念:

gc-graph

GC 根对象

首先,垃圾回收器将某些特殊的对象定义为GC根对象。

所谓的 GC 根对象包括:

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

(2)方法区中的常量引用的对象;

(3)方法区中的类静态属性引用的对象;

(4)本地方法栈中JNI(Native方法)的引用对象。

(5)活跃线程。

接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。

回收器将访问到的所有对象都标记为存活。

存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。

其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。

这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。

标记阶段注意点

(1)开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。

暂停应用线程以便JVM可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。

触发安全点的原因有许多,但最常见的应该就是垃圾回收了。

(2)暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。

(3)在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:

3.1 如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。

筛选的条件是此对象是否有必要执行 finalize() 方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。

当对象没有覆盖 finalize() 方法,或 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。

3.2 如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行 finalize() 方法。

finalize() 方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize() 方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在 finalize() 方法中成功拯救自己,只要在 finalize() 方法中让该对象重新引用链上的任何一个对象建立关联即可。

而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

(4)实际上GC判断对象是否可达看的是强引用。

当标记阶段完成后,GC 开始进入下一阶段,删除不可达对象。

回收算法

Tracing算法(Tracing Collector)或 标记—清除算法

标记—清除算法是最基础的收集算法,为了解决引用计数法的问题而提出。

它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。

  • 优点

不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。

  • 缺点

(1)标记和清除过程的效率都不高。(这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量)

(2)标记清除后会产生大量不连续的内存碎片。

虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败(在Java中就是一次OutOfMemoryError)不得不触发另一次垃圾收集动作。

标记—清除算法-过程

标记—清除算法-效果

Compacting 算法(Compacting Collector) 或 标记—整理算法

该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。

在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

  • 优点

(1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。

(2)使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。

  • 缺点

GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

标记—整理算法-过程

标记—整理算法-效果

Copying 算法(Copying Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。

它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。

一种典型的基于Coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

  • 优点

(1)标记阶段和复制阶段可以同时进行。

(2)每次只对一块内存进行回收,运行高效。

(3)只需移动栈顶指针,按顺序分配内存即可,实现简单。

(4)内存回收时不用考虑内存碎片的出现(得活动对象所占的内存空间之间没有空闲间隔)。

  • 缺点

需要一块能容纳下所有存活对象的额外的内存空间。

因此,可一次性分配的最大内存缩小了一半。

Adaptive算法(Adaptive Collector)

在特定的情况下,一些垃圾收集算法会优于其它算法。

基于 Adaptive 算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。

因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行 GC,以便提高回收效率。

Java内存分配机制

内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。

三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

Generation

新生代(Youn Generation)

几乎所有新生成的对象首先都是放在年轻代的。

新生代内存按照 8:1:1 的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。

  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;

  2. 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);

  3. 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;

  4. 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;

  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用 -XX:MaxTenuringThreshold 控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

老年代(Old Generation)

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。

因此,可以认为年老代中存放的都是一些生命周期较长的对象。

内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

  • 大对象

一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。

比如:

byte[] data = new byte[4*1024*1024]

这种一般会直接在老年代分配存储空间。

当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

永久代(Permanent Generation)

在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。

用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。

GC 与内存分配

GC机制的基本算法是:分代收集。

下面阐述每个分代的收集方法。

新生代

从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。

经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。

在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

  • Stop-and-copy

在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。

这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)

由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到老年代。

用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.

老年代

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。

解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。

Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

方法区(永久代)

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。

对于无用的类进行回收,必须保证3点:

  1. 类的所有实例都已经被回收

  2. 加载类的ClassLoader已经被回收

  3. 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。

HotSpot 提供 -Xnoclassgc 进行控制

使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息

-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;

-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持

  • 注意

JVM 规范中没有要求 JVM 实现方法区的垃圾回收,这个区域的垃圾回收效率非常之低。

触发 GC 的条件

GC分为Scavenge GC和Full GC。

Scavenge GC:发生在Eden区的垃圾回收。

Full GC: 对整个堆进行整理,包括Young、Tenured和Perm。

Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。

在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。

触发的场景

有如下原因可能导致Full GC:

  1. 年老代(Tenured)被写满;

  2. 持久代(Perm)被写满;

  3. System.gc()被显示调用;

  4. 上一次GC之后Heap的各域分配策略动态变化.

调用时机的进一步理解

当应用程序空闲时,即没有应用线程在运行时,GC会被调用。

因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。

  1. Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。

  2. 在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。

由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。

特点

经过上述的说明,可以发现垃圾回收有以下的几个特点:

(1)垃圾收集发生的不可预知性

由于实现了不同的垃圾回收算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。

(2)垃圾收集的精确性

主要包括 2 个方面:

(a)垃圾收集器能够精确标记活着的对象;

(b)垃圾收集器能够精确地定位对象之间的引用关系。

前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。

而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。

(3)现在有许多种不同的垃圾收集器,每种有其算法且其表现各异,既有当垃圾收集开始时就停止应用程序的运行,又有当垃圾收集开始时也允许应用程序的线程运行,还有在同一时间垃圾收集多线程运行。

(4)垃圾收集的实现和具体的JVM 以及JVM的内存模型有非常紧密的关系。

不同的JVM 可能采用不同的垃圾收集,而JVM 的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot 系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。

(5)随着技术的发展,现代垃圾收集技术提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。

使用的注意点

针对以上特点,我们在使用的时候要注意:

(1)不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。

(2)Java中提供了一些和垃圾收集打交道的类,而且提供了一种强行执行垃圾收集的方法–调用 System.gc(),但这同样是个不确定的方法。

Java 中并不保证每次调用该方法就一定能够启动垃圾收集,它只不过会向JVM发出这样一个申请,到底是否真正执行垃圾收集,一切都是个未知数。

(3)挑选适合自己的垃圾收集器。

一般来说,如果系统没有特殊和苛刻的性能要求,可以采用JVM的缺省选项。否则可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。

系统具有较高的配置,有比较多的闲置资源,可以考虑使用并行标记/清除收集器。

(4)关键的也是难把握的问题是内存泄漏。

良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大漏洞。

(5)尽早释放无用对象的引用。

大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null,暗示垃圾收集器来收集该对象,还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。

最佳实践

针对上面的特征,有以下的编程建议。

根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

(1) 不要显式调用 System.gc()

此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

(2) 尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

(3) 对象不用时最好显式置为 null

一般而言,为 null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 null,有利于GC收集器判定垃圾,从而提高了GC的效率。

(4) 尽量使用StringBuffer,而不用String来累加字符串

由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,

Str5=Str1+Str2+Str3+Str4, 这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。

避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

(5) 能用基本类型如int,long,就不用Integer,Long对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

(6) 尽量少用静态对象变量

静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

(7) 分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。

集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

GC 相关函数

System.gc() 方法

命令行参数监视垃圾收集器的运行:

使用 System.gc() 可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。

在命令行中有一个参数 -verbosegc 可以查看Java使用的堆内存的情况,它的格式如下:

java -verbosegc classfile

需要注意的是,调用 System.gc() 也仅仅是一个请求(建议)。

JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。

finalize() 方法

在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源。

但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象以释放资源,这个方法就是 finalize()。

它的原型为:

protected void finalize() throws Throwable

在 finalize() 方法返回之后,对象消失,垃圾收集开始执行。

原型中的 throws Throwable 表示它可以抛出任何类型的异常。

  • 意义

之所以要使用 finalize(),是存在着垃圾回收器不能处理的特殊情况。

假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个 finalize() 方法。

  • 特殊区域

特殊的区域例如:

1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。

这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。

但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。

2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。

换言之,finalize() 的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。

因为在Java中并没有提够像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是@Override Object这个类中的finalize()方法。

比如:销毁通知。

一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。

只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。

JAVA里的对象并非总会被垃圾回收器回收。

  1. 对象可能不被垃圾回收

  2. 垃圾回收并不等于“析构”

  3. 垃圾回收只与内存有关。

也就是说,并不是如果一个对象不再被使用,是不是要在 finalize() 中释放这个对象中含有的其它对象呢?

不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。

当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。

垃圾收集器

按照执行机制划分

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

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

(3)并发标记扫描垃圾回收器(CMS Garbage Collector)

(4)G1垃圾回收器(G1 Garbage Collector)

每种类型都有自己的优势与劣势,在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。

重要的是,我们编程的时候可以通过向JVM传递参数选择垃圾回收器类型。

每种类型理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。

串行垃圾回收器

串行垃圾回收器通过持有应用程序所有的线程进行工作。

它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。

它最适合的是简单的命令行程序(单CPU、新生代空间较小及对暂停时间要求不是非常高的应用)。

是client级别默认的GC方式。

通过JVM参数 -XX:+UseSerialGC 可以使用串行垃圾回收器。

并行垃圾回收器

并行垃圾回收器也叫做 throughput collector。它是JVM的默认垃圾回收器。

与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程。

适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式。

可用 -XX:+UseParallelGC 来强制指定,用 -XX:ParallelGCThreads=4 来指定线程数。

并发标记扫描垃圾回收器

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。

并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。

(1)当标记的引用对象在Tenured区域;

(2)在进行垃圾回收的时候,堆内存的数据被并发的改变。

相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。

如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。

通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

HotSpot 的垃圾收集器

在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所有线程的执行。

这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。

Serial 收集器

新生代收集器,使用停止复制算法,使用一个线程进行GC,串行,其它工作线程暂停。

使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)

Serial Old收集器

老年代收集器,单线程收集器,串行,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。

ParNew 收集器

新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,关注缩短垃圾收集时间。

使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

Parallel Old收集器

老年代收集器,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。

Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。

使用 -XX:+UseParallelOldGC 开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

Parallel Scavenge 收集器

新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。

比如:JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。

使用 -XX:+UseParallelGC 开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);

使用 -XX:GCTimeRatio 来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。

使用 -XX:MaxGCPauseMillis 设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数 -XX:+UseAdaptiveSizePolicy 可以进行动态控制, 如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。

CMS

老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。

使用 -XX:+UseConcMarkSweepGC 进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集

过程

CMS收集的执行过程是:

初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 预清理(CMS-concurrent-preclean) -> 可控预清理(CMS-concurrent-abortable-preclean)-> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) -> 并发重设状态等待下次CMS的触发(CMS-concurrent-reset)

具体的说,先2次标记,1次预清理,1次重新标记,再1次清除。

详细过程

1,首先jvm根据-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly来决定什么时间开始垃圾收集;

2,如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了-XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc;

3,如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定什么时候触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数没有设置的原因;

4,当cms gc开始时,首先的阶段是初始标记(CMS-initial-mark),是stop the world阶段,因此此阶段标记的对象只是从root集最直接可达的对象; CMS-initial-mark:961330K(1572864K),指标记时,old代的已用空间和总空间

5,下一个阶段是并发标记(CMS-concurrent-mark),此阶段是和应用线程并发执行的,所谓并发收集器指的就是这个,主要作用是标记可达的对象,此阶段不需要用户停顿。 此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark

6,下一个阶段是CMS-concurrent-preclean,此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的,因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分工作以节省时间

此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean

7,下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间 此阶段涉及几个参数:

 -XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束
 -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,
  即当eden使用达到此值时,才会开始abortable-preclean阶段
 -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行
  此阶段会打印一些日志如下:
 CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
 CMS:abort preclean due to time XXX

8,再下一个阶段是第二个stop the world阶段了,即Rescan阶段,此阶段暂停应用线程,停顿时间比并发标记小得多,但比初始标记稍长。

对对象进行重新扫描并标记;

YG occupancy:964861K(2403008K),指执行时young代的情况

CMS remark:961330K(1572864K),指执行时old代的情况

此外,还打印出了弱引用处理、类卸载等过程的耗时

9,再下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理

10,最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构

悲观 Full GC

有2种情况会触发CMS 的悲观full gc,在悲观full gc时,整个应用会暂停。

A,concurrent-mode-failure:预清理阶段可能出现,当cms gc正进行时,此时有新的对象要进行old代,但是old代空间不足造成的。

其可能性有:

1,O区空间不足以让新生代晋级,

2,O区空间用完之前,无法完成对无引用的对象的清理。

这表明,当前有大量数据进入内存且无法释放。

B,promotion-failed:新生代young gc可能出现,当进行young gc时,有部分young代对象仍然可用,但是S1或S2放不下,因此需要放到old代,但此时old代空间无法容纳此。

  • 影响 CMS GC

影响cms gc时长及触发的参数是以下2个:

-XX:CMSMaxAbortablePrecleanTime=5000
-XX:CMSInitiatingOccupancyFraction=80
  • 解决方式

解决也是针对这两个参数来的,根本的原因是每次请求消耗的内存量过大

解决方式:

A,针对cms gc的触发阶段,调整-XX:CMSInitiatingOccupancyFraction=50,提早触发cms gc,就可以缓解当old代达到80%,cms gc处理不完,从而造成concurrent mode failure引发full gc

B,修改-XX:CMSMaxAbortablePrecleanTime=500,缩小CMS-concurrent-abortable-preclean阶段的时间

C,考虑到cms gc时不会进行compact,因此加入-XX:+UseCMSCompactAtFullCollection

(cms gc后会进行内存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次后会进行compact)参数

优缺点

  • 优点

在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。

  • 缺点

CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。

另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用 -XX:CMSInitiatingOccupancyFraction 来设置)的时候就要进行Full GC。

如果用户线程消耗内存不是特别大,可以适当调高 -XX:CMSInitiatingOccupancyFraction 以降低GC次数,提高性能,如果预留的用户线程内存不够, 则会触发Concurrent Mode Failure。

此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此 -XX:CMSInitiatingOccupancyFraction 不宜设的过大。

还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用 -XX:+UseCMSCompactAtFullCollection 来设置是否在Full GC之后进行碎片整理, 用 -XX:CMSFullGCsBeforeCompaction 来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。

G1

  • 概念

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。

因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。

G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域。

  • 使用

通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器。

  • Java8 特性

在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication

我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。

这个选择在 Java8u20 被引入。

ZGC

ZGC 性能秒杀 G1。

ZGC

  • 为什么这么快

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障), 比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是, 永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

GC 日志

输出日志参数

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

日志分析

[GC (System.gc()) [PSYoungGen: 3686K->664K(38400K)] 3686K->672K(125952K), 0.0016607 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 664K->0K(38400K)] [ParOldGen: 8K->537K(87552K)] 672K->537K(125952K), [Metaspace: 2754K->2754K(1056768K)], 0.0059024 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
  eden space 33280K, 1% used [0x00000000d5c00000,0x00000000d5c534a8,0x00000000d7c80000)
  from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
  to   space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
 ParOldGen       total 87552K, used 537K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
  object space 87552K, 0% used [0x0000000081400000,0x00000000814864a0,0x0000000086980000)
 Metaspace       used 2761K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 299K, capacity 386K, committed 512K, reserved 1048576K
  1. GC日志开头的”[GC”和”[Full GC”说明了这次垃圾收集的停顿类型,如果有”Full”,说明这次GC发生了”Stop-The-World”。因为是调用了System.gc()方法触发的收集,所以会显示”[Full GC (System.gc())”,不然是没有后面的(System.gc())的。

  2. “[PSYoungGen”和”[ParOldGen”是指GC发生的区域,分别代表使用Parallel Scavenge垃圾收集器的新生代和使用Parallel old垃圾收集器的老生代。为什么是这两个垃圾收集器组合呢?因为我的jvm开启的模式是Server,而Server模式的默认垃圾收集器组合便是这个,在命令行输入java -version就可以看到自己的jvm默认开启模式。还有一种是client模式,默认组合是Serial收集器和Serial Old收集器组合。

  3. 在方括号中”PSYoungGen:”后面的”3686K->664K(38400K)”代表的是”GC前该内存区域已使用的容量->GC后该内存区域已使用的容量(该内存区域总容量)”

  4. 在方括号之外的”3686K->672K(125952K)”代表的是”GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”

  5. 再往后的”0.0016607 sec”代表该内存区域GC所占用的时间,单位是秒。

  6. 再后面的”[Times: user=0.00 sys=0.00, real=0.00 secs]”,user代表进程在用户态消耗的CPU时间,sys代表代表进程在内核态消耗的CPU时间、real代表程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。

  7. 至于后面的”eden”代表的是Eden空间,还有”from”和”to”代表的是Survivor空间。

内存分配与回收策略

对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。

以下列举几条最普遍的内存分配规则,供大家学习。

对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

👇Minor GC vs Major GC/Full GC:

Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。

在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。

大对象直接进入老年代

大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。

一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。(还记得吗,新生代采用复制算法回收垃圾)

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

JVM 给每个对象定义了一个对象年龄计数器。

当新生代发生一次 Minor GC 后,存活下来的对象年龄 +1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。

使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。

动态对象年龄判定

如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

空间分配担保

JDK 6 Update 24 之前的规则是这样的:

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

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

JDK 6 Update 24 之后的规则变为:

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC

通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。

这个过程就是分配担保。

Full GC

总结一下有哪些情况可能会触发 JVM 进行 Full GC。

(1)System.gc() 方法的调用 此方法的调用是建议 JVM 进行 Full GC,注意这只是建议而非一定,但在很多情况下它会触发 Full GC,从而增加 Full GC 的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc()。

(2)老年代空间不足 老年代空间不足会触发 Full GC 操作,若进行该操作后空间依然不足,则会抛出如下错误:java.lang.OutOfMemoryError: Java heap space

(3)永久代空间不足 JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代(Permanet Generation),存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space

(4)CMS GC 时出现 promotion failed 和 concurrent mode failure promotion failed,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。

(5)统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间。

参考资料

《深入理解 jvm》

Garbage collection (computer science)

怎么在面试时回答Java垃圾回收机制(GC)相关问题?

深入理解JVM的内存结构及GC机制

Java系列笔记(3) - Java 内存区域和GC机制

从实际案例聊聊Java应用的GC优化

  • ZGC

http://openjdk.java.net/jeps/333

https://juejin.im/entry/5b86a276f265da435c4402d4

  • GC log

GC 日志查看分析

https://doocs.github.io/jvm/#/docs/05-memory-allocation-gc