【转载】JVM 学习——垃圾收集器与内存分配策略
本文主要是对《深入理解java虚拟机 第二版》第三章部分做的总结,文章中大部分内容都来自这章内容,也是博客 JVM 学习的第二部分。
简述
说到垃圾收集(Garbage Collection,GC),很多人可能会认为这是 Java 自有的特性,曾经我也一度这样想,后来才知道 GC 的历史要远远长于 Java,它第一次真正使用是在 Lisp 中,现在,像 python、go 等都有自己的垃圾收集器。在 GC 最开始设计时,人们在思考 GC 时就需要完成三件事情:
- 哪些内存需要进行回收?
- 什么时候对这些内存进行回收?
- 如何进行回收?
经过将近半个多世纪的发展,内存的动态分配与垃圾回收技术现在已经非常成熟,看起来是进入半自动化时代,但是我们依然需要去学习 GC 和内存分配,因为,当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这一块进行必要的监控和调节。
回到 Java 语言,在前面介绍的 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不絮地执行着出栈和入栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此,这几块区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分的内存和回收都是动态的,垃圾回收器主要关注的也是这部分的内存。
判断对象是否已死
Java 的堆里存放的几乎所有的对象实例,在进行垃圾回收前,第一件事情就是要确定哪些对象还”存活”着、哪些对象已经”死去”(即不可能再被任何途径使用的对象)。
判断的方法
引用计数算法(Reference Counting)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
可达性分析算法
基本思想:通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。
在 Java 中,可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
两种方法对比
引用计数法 | 可达性分析 | |
---|---|---|
优点 | 实现简单,效率高(很少使用这种方法) | 在主流的商业程序语言(Java、C#等)的主流实现中,都使用这种方法 |
缺点 | 无法解决对象之间相互循环引用问题(主流的 JVM 都没有使用这种方法) | 实现稍微有些复杂 |
对象的四种引用
在 Java 中,如果仅仅把对象分为引用和没有被引用这两种状态,那么在一些场景下就无能为力了,比如:我们希望有这样一类对象,当内存空间充足时,则能保留在内存之中,而如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。因此,在 JDK1.2 之后,Java 就对引用的概念进行了扩充,将引用非为一下四种:
引用类型 | 定义 | 声明方式 | 回收条件 |
---|---|---|---|
强引用( Strong Reference) | 强引用就是指在程序代码之中普遍存在的 | 类似于Object obj= new Object() 这类的引用 |
只要强引用还在,永不会回收 |
软引用( Soft Reference) | 软引用是用来描述一些还有用但并非必需的对象 | 使用SoftReference 类来声明 |
系统将要发生内存溢出异常之前,将会把这些对象列入回收范围,进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。 |
弱引用( Weak Reference) | 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些 | 使用 WeakReference 类实现弱引用 |
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象 |
虚引用(WeakReference) | 它是最弱的一种引用关系,一个引用是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 | 使用PhantomReference 类来实现虚引用 |
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知 |
生存还是死亡
要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行
finalize()
方法; - 当对象没有覆盖
finalize()
方法,或者finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行; - 如果对象要在
finalize()
中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。 任何一个对象的finalize()
方法都只会被系统自动调用一次。
这里有两点要注意:
- 如果一个对象被判定有必要执行
finalize()
方法,那这个对象会先被放置在一个叫做F-Queue
的队列中,并由虚拟机自动建立的、低优先级的Finalizer
线程去执行它。这里的 “执行” 指的是虚拟机会触发这个方法,但不会承诺等待它运行结束,原因是:如果一个对象在执行finalize()
时运行缓慢,或者发生死循环,将很有可能导致F-Queue
队列中其他对象永久处于等待,甚至整个内存回收系统崩溃。 - 不鼓励大家使用这种方法来拯救对象。相反,建议大家尽量避免使用它,因为它不是 C/ C++ 中的析构函数,而是 Java 刚诞生时为了使 C/ C++ 程序员更容易接受它所做出的一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。 关闭外部资源,使用
try- finally
或者其他方式都可以做得更好、更及时,所以笔者大家完全可以忘掉 Java 语言中有这个方法的存在。
回收方法区
很多人认为方法区(或者 HotSpot 的永久代)是没有垃圾收集的,Java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的 “性价” 一般比较低。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
判断一个常量是否是 “废弃常量” 比较简单,而要判定一个类是否是 “无用的类” 的条件则相对苛刻很多。类需要同时满足下面 3 个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收;
- 加载该类的
ClassLoader
已经被回收; - 该类对应的
java. lang. Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
是否对类进行回收, HotSpot 虚拟机提供了 -Xnoclassgc
参数进行控制,还可以使用 -verbose: class
以及 -XX:+ TraceClassLoading
、- XX:+ TraceClassUnLoading
查看类加载和卸载信息。
在大量使用反射、动态代理、 CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法
本节主要是介绍一下垃圾收集算法的思想,并不涉及具体的实现。
标记-清除算法
标记-清除(Mark-Sweep)算法,有两个阶段
- 首先标记所有需要回收的对象;
- 在标记完成后统一进行回收。
执行过程如下图所示。
复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
算法执行过程如下图所示
现在的商业虚拟机都采用这种收集算法来回收新生代。将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[ 1]。 当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8: 1。 当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保( Handle Promotion)。 如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
标记-整理算法
标记-整理算法让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
算法执行过程如下图所示
分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集( Generational Collection) 算法。
一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
算法对比
算法 | 优点 | 缺点 |
---|---|---|
标记-清除 | 最基础的算法,不是一般的简单 | 一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片 |
复制 | 实现简单,运行高效 | 减少了内存使用空间;而且在对象存活率较高时需要进行较多的复制操作(不适合老年代) |
标记-整理 | 根据老年代的特点提出的一种算法,适合老年代 | 只适合于某些特定情况 |
分代收集 | 使用多种收集算法,根据各自的特点选用不同的收集算法 | 在具体的实现上比前面的更加复杂 |
HotSpot 的算法实现
上面介绍的基础的理论,这一节讲述一下 HotSpot 虚拟机如何实现这些算法的。
枚举根节点
当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在 HotSpot 的实现中,是使用一组称为 OopMap 的数据结构来达到这个目的的。
安全点
在 OopMap 的协助下, HotSpot 可以快速且准确地完成 GC Roots 枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将会需要大量的额外空间,这样 GC 的空间成本将会变得更高。
实际上,HotSpot 并没有为每条指令都生成 OopMap,而只是在 “特定的位置” 记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始 GC,只有在达到安全点时才能暂停。
Safepoint 的选定既不能太少以至于让 GC 等待时间太长,也不能多余频繁以至于过分增大运行时的负载。所以,安全点的选定基本上是以 “是否具有让程序长时间执行的特征” 为标准进行选定的——因为每条指令执行的时间非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,”长时间执行” 的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 Safepoint。
对于 Safepoint, 另一个需要考虑的问题是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来: 抢先式中断( Preemptive Suspension) 和主动式中断( Voluntary Suspension)
- 抢占式中断:它不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让它 “跑” 到安全点上。
- 主动式中断:当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
现在几乎没有虚拟机采用抢占式中断来暂停线程从而响应 GC 事件。
安全区域
在使用 Safepoint 似乎已经完美地解决了如何进入 GC 的问题,但实际上情况却并不一定。Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。但如果程序在 “不执行” 的时候呢?所谓程序不执行就是没有分配 CPU 时间,典型的例子就是处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应 JVM 的中断请求,JVM 也显然不太可能等待线程重新分配 CPU 时间。对于这种情况,就需要安全区域(Safe Regin)来解决了。
在线程执行到 Safe Region 中的代码时,首先标识自己已经进入了 Safe Region,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为 Safe Region 状态的线程了。在线程要离开 Safe Region 时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开 Safe Region 的信号为止。
垃圾收集器
垃圾收集器是内存回收的具体实现,这里讨论的收集器是 JDK 1.7 Update 14 之后的 HotSpot 虚拟机(目前 G1 仍然处于实验状态),这个虚拟机包含的所有收集器如下图所示。
下面会介绍一下这几种收集器的特性、基本原理和使用场景,并重点分析 CMS 和 G1 这两个相对复杂的收集器,了解它们的部分运作细节。
注:这里只是介绍这些收集器,进行一下比较,但并非是挑选一个最好的收集器,目前到现在为止还没有最好的收集器出现,更没有万能的收集器,我们只是选择对具体应用最合适的收集器。
Serial 收集器
它曾是最基本、发展历史最悠久的收集器,它是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会是使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。Stop The World 这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。下图展示了 Serial/Serial old 收集器的运行过程。
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本。ParNew/Serial old 收集器的运行过程如下图所示
ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。(CMS收集器第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。)
CMS 作为老年代的收集器,却无法与 JDK 1. 4. 0 中已经存在的新生代收集器 Parallel Scavenge 配合工作,只能选择ParNew或者Serial收集器中的一个。ParNew 收集器也是使用 -XX:+UseConcMarkSweepGC
选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC
选项来强制指定它。
由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保证可以超越 Serial 收集器。但是,当 CPU 的数量增加时,它对于 GC 时系统资源的有效利用还是很有好处的,它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(使用超线程时)的环境下,可以使用 -XX:ParallelGCThreads
参数来限制垃圾收集的线程数。
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
它与其他收集器的不同之处在于:它的关注点与其他收集器不同。CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量( Throughput)。
所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量:
- 控制最大垃圾收集停顿时间,
-XX:MaxGCPauseMillis
,设置时间小一点并不能使用系统的收集速度更快,因为 GC 停顿时间缩短是以牺牲吞吐量和新生代空间来换取的; - 直接设置吞吐量大小,
-XX:GCTimeRatio GC
,CTimeRatio是指垃圾收集时间占总时间的比率。
Parallel Scavenge 收集器经常称为 “吞吐量优先” 收集器。Parallel Scavenge 收集器还提供一个参数 -XX:+ UseAdaptiveSizePolicy
,当这个参数打开后,就不需要收工指定一些细节参数了(如:新生代的大小等),虚拟机会动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC 自适应的调解策略(GC Ergonomics)。自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 “标记-整理” 算法。
这个收集器的主要意义在于给 Client 模式下的虚拟机使用,如果在 Server 模式下,那么它主要还有两大用途:
- 在 JDK1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用;
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Serial Old 收集器的工作过程如下图所示
Parallel old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和 “标记-整理” 算法。 这个收集器是在 JDK 1. 6 中才开始提供的。在此之前,如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old( PS MarkSweep) 收集器外别无选择(还记得上面说过 Parallel Scavenge 收集器无法与 CMS 收集器配合工作吗?)。由于老年代 Serial Old 收集器在服务端应用性能上的拖累,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合“给力”。
知道 Parallel old 收集器出现后,”吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel old 收集器,Parallel old 收集器的工作过程如下图所示
CMS 收集器
CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间为目标,多数应用于互联网站或者B/S系统的服务器端上。
CMS 是基于 “标记—清除” 算法实现的,整个过程分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
有以下几个特点:
- 其中,初试标记、重新标记这两个步骤仍然需要 “Stop The World”;
- 初始标记只是标记一下 GC Roots 能直接关联到的对象,速度很快;
- 并发标记阶段就是进行 GC Roots Tracing 的过程;
- 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初试标记阶段稍长一些,但远比并发标记的时间短。
CMS 收集器的运作步骤如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
- 优点
- 并发收集、低停顿, Sun 公司的一些官方文档中也称之为并发低停顿收集器( Concurrent Low Pause Collector)。
- 缺点
- CMS 收集器对 CPU 资源非常敏感。
- CMS 收集器无法处理浮动垃圾( Floating Garbage),可能出现 “Concurrnet Mode Failure” 失败而导致另一次 Full GC 的产生。如果在应用中老年代增长不是太快,可以适当调高参数
-XX: CMSInitiatingOccupancyFraction
的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是 CMS 运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX: CM SInitiatingOccupancyFraction
设置得太高很容易导致大量” Concurrent Mode Failure” 失败,性能反而降低。 - 收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次 Full GC。CMS 收集器提供了一个
-XX:+UseCMSCompactAtFullCollection
开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
G1 收集器
G1 是一款面向服务器应用垃圾收集器,与其他GC收集器想必,G1具备以下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短 Stop The World 停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1 收集器仍然可以通过并发的方式让Java程序继续执行;
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一半时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的 “标记-清理” 算法不同,G1从整体上看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,小号在垃圾收集上的时间不能超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
下图展示 G1 收集器的运行步骤
G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短;
- 并发标记(Concurrent Marking):从 GC Roots 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行;
- 最终标记(Final Marking):最终标记则是为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行;
- 筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划,根据 Sun 公司透露的信息来看,这个阶段是可以做到与用户程序并发执行。
垃圾收集器对比
垃圾收集器 | 特性 | 使用场景 |
---|---|---|
Serial 收集器 | 复制算法;单线程;新生代;简单而高效;需要进行 stop the world。 | 它是虚拟机运行在 Client 模式下的默认新生代收集器 |
ParNew 收集器 | 复制算法;Serial 的多线程版本;新生代;默认的线程数与 CPU 数一致 | 它是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。 |
Parallel Scavenge 收集器 | 复制算法;并行多线程;新生代;吞吐量优先原则;有自适应调节策略 | 适合后台运算而不需要太多交互的任务 |
Serial Old 收集器 | 标记-整理算法;老年代;单线程; | 这个收集器的主要意义在于给 Client 模式下的虚拟机使用 |
Parallel Old 收集器 | 标记-整理;老年代;多线程;与 parallel scavenge 收集器结合实现吞吐量优先 | 与 Parallel Scavenge 结合使用,适用那些注重吞吐量以及对 CPU 资源敏感的场合 |
CMS 收集器 | 标记-清除;老年代;并发收集、低停顿;有三个缺点(参见上面) | 非常适合那些重视响应速度,希望系统停顿时间最短的应用 |
G1 收集器 | 分代收集;空间整合;可预测的停顿 | 面向服务器应用垃圾收集器 |
垃圾收集器参数总结
参数 | 描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio GC | 时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
-XX:+PrintGCDetails | 告诉虚拟机在发送垃圾收集行为时打印内存回收日志,并在进程退出的时候输出当前的内存各区域分配情况 |
内存分配与回收策略
本节主要探讨给对象分配内存的部分,对象主要分配在新生代的 Eden 区上,少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,取决于使用的哪种垃圾收集器组合以及 jvm 的参数设置。下面会介绍几条最普遍的内存分配规则。
对象优先在Eden分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
。
- 新生代 GC( Minor GC): 指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
- 老年代 GC( Major GC/ Full GC): 指发生在老年代的 GC, 出现了 Major GC, 经常会伴随至少一次的 Minor GC( 但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。 Major GC 的速度一般会比 Minor GC 慢 10 倍以上。
堆空间分配例子:
-verbose: gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails -XX:SurvivorRatio=8
在运行时通过 -Xms20M
、-Xmx20M
、-Xmn10M
这 3 个参数限制了 Java 堆大小为 20MB, 不可扩展,其中 10MB 分配给新生代,剩下的 10MB 分配给老年代。-XX:SurvivorRatio=8
决定了新生代中 Eden 区与一个 Survivor 区的空间比例是 8: 1
大对象直接进入老年代
所谓的大对象是指:需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。
大对象对虚拟机的内存分配来说是一个坏消息()遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来”安置”它们。
-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配(避免了在 Eden 以及两个 Survivor 区之间发送大量的内存复制)。 PretenureSizeThreshold
参数只对 Serial 和 ParNew 两款收集器有效, Parallel Scavenge 收集器不认识这个参数。
长期存活的对象将进入老年代
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。 对象在 Survivor 区中每熬过一次 Minor GC, 年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX: MaxTenuringThreshold
设置。
动态对象年龄判断
为了适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold
才能晋升老年代。如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold
中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。当大量对象在 Minor GC 后仍绕存活,就需要老年代进行空间分配担保,把 Survivor 无法容纳的对象直接进入老年代。如果老年代的判断到剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为经验值),则进行一次 Full GC。