JVM内存分配与回收
概述
== 本问所介绍的关于内存分配和垃圾收集的相关知识并不是一成不变的,当选择不同的垃圾收集器和不同的JVM版本时,这些行为可能会有所不同 ==
在开始介绍jvm的垃圾收集相关内容之前,先统一几个关于垃圾收集的概念
基于分代理论,针对堆内存中不同区域的垃圾收集形成了以下几个概念
- Minor GC/Young GC :年轻代的垃圾收集;
- Major GC/Old GC:老年代的垃圾收集,目前只有CMS有单独针对老年代的垃圾收集行为;
- Full GC:整个堆和方法区的垃圾收集;
- Mix GC:收集整个新生代和部分老年代。目前只有G1有这种垃圾收集行为。
了解垃圾收集和内存分配的意义
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些”自动化“的技术实施必要的监控和调节。
要达到的目的:
-
提高系统吞吐量;
-
单位时间内使系统减少GC造成的系统停顿时间。
选择垃圾收集器的依据:
-
应用程序主要的关注点,是吞吐量还是停顿;
-
应用运行的基础设施;
-
JDK的发行商和版本号。
内存分配
堆内存区域的划分
在G1垃圾收集器出现之前,JVM基于”分代收集“的理论,将堆空间分为了新生代(Young Generation)和老年代(Old Generation)。同时在使用Serial、ParNew等新生代垃圾收集器时,采用了一种更优化的半区复制分代策略来设计新生代的内存布局。
JVM对象内存分配策略
1、栈上分配(行为不受垃圾收集器的影响)
通过逃逸分析的结果,如果一个对象不会逃逸出线程之外,就会优先在栈上为这个对象分配内存,
从JDK6开始支持逃逸分析,到JDK7为默认开启,可以使用-XX:+DoEscapeAnalysis来开启逃逸分析
2、对象优先分配在Eden区
在为对象分配内存时根据垃圾收集器的不同,JVM会有不同的分配策略,以Serial加Serial Old为例。
3、空间分配担保
4、大对象直接进入老年代
5、动态对象年龄判断
6、长期存活对象进入老年代
垃圾收集
垃圾对象的判定
1、引用计数器算法
该算法比较简单,在对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器就加一,引用失效时,计数器就减一。如果计数器为零即为垃圾对象
引用计数器算法无法标记循环引用的对象,比如A对象->B对象,B对象->A对象,当某一时刻A、B对象均未被除了彼此之外的对象引用,但二者的引用计数器均不为零。
2、可达性分析算法
从一系列的GC Root对象出发,向下搜索其引用的其他对象,当一个对象无法被搜索到即不可达时,该对象被认定为不可能再被使用的。
GC Root包含哪些对象?
- 栈中所引用的对象(如:方法参数,局部变量、临时变量)
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,类加载器等
- 所有被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
垃圾收集算法
绿色–存活对象、红色–垃圾对象、白色–空闲空间、灰色–保留空间
1、标记-清除:标记要回收对象,然后统一清除
缺点:
- 效率不稳定,如果标记垃圾对象,当大部分都是要回收的对象时,执行效率降低
- 会导致空间碎片化,当无法找到连续空间为对象分配内存时会触发另一次垃圾收集
2、标记-复制:将内存空间分为两块,对象只在其中一块内存中分配,当这块空间用完了,就将存活的对象复制到另一块内存空间中,并将已使用过的那块内存一次性清理掉
3、标记-整理:在标记完成后,让所有存活对象向内存空间一端移动,然后直接清理掉边界以外的内存
缺点:
- 移动对象会导致垃圾收集的时间变长,如果老年代采用这种算法则会导致STW时间变长
垃圾收集器
收集器 | 回收区域 | 算法 | 使用场景 |
---|---|---|---|
Serial | 新生代 | 标记-复制 | 单核或处理器核心较少、内存等资源受限的环境 |
ParNew | 新生代 | 标记-复制 | JDK1.9之后与CMS配合时进行年轻代收集 |
Parallel Scavenge | 新生代 | 标记-复制 | 垃圾收集时需要控制吞吐量的场景 |
Serial Old | 老年代 | 标记-整理 | JDK5之前除与Serial配合外还可与Parallel Scavenge配合使用,作为CMS CMF的后备方案 |
Parallel Old | 老年代 | 标记-整理 | |
CMS | 老年代 | 标记-清除 | 当内存在4~8G且希望尽量减少垃圾收集的停顿时间 |
Serial(串行收集器)
Serial在JDK1.3.1之前是HotSpot虚拟机新生代收集器唯一的选择。Serial是单线程收集器,在进行垃圾回收时必须暂停其他所有工作线程,直到它收集结束。至今仍是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,Serial收集器采用复制算法暂停所有用户线程进行收集,采用标记-复制算法。
优点:简单而高效(与其他收集器的单线程相比)
- 对于资源受限的环境,它是所有收集器里额外内存消耗(指为保证垃圾收集能顺利高效地进行而存储的额外信息)最小的;
- 对于单核或者处理器核心较少的环境,其没有额外的线程交互开销,可以获得最高的单线程收集效率。
缺点:单线程收集,无法避开STW问题,如果垃圾收集频繁发生,其结果令人无法接受。
ParNew(Serial并行版本)
ParNew实质上是Serial的多线程版本,是一款新生代收集器,除了同时使用多个线程进行垃圾收集,其余与Serial相比在Serial可用的所有控制参数、收集算法、对象分配规则、回收策略、STW等都与Serial收集器完全一致。虽然与Serial相比并无太多创新之处,但是却是不少运行在服务端模式下的HotSpot虚拟机(尤其是JDK7以前的遗留系统)中首选的新生代收集器,其中关键原因是:除了Serial收集器外只有它能和CMS收集器配合工作,其使用的是标记-复制算法。
值得注意的是:(自JDK9开始)
- 随着G1的登场,ParNew+CMS的组合已经不是官方推荐的服务端模式下的收集器解决方案;
- 官方取消了ParNew+SerialOld和Serial+CMS的组合,取消了-XX:+UseParNewGC参数,意味着ParNew从此只能和CMS搭配。
Parallel Scavenge(并行收集器)
Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现,能够并行收集的多线程收集器。其特别之处在于Parallel Scavenge收集器的目标侧重是达到一个可控制的吞吐量。其也经常被称作”吞吐量优先收集器“。
吞吐量 = 运行用户代码时间 / (运行用户代码时间+运行垃圾收集时间)
PS收集器中可以通过-XX:GCTimeRatio参数设置一个正整数,表示用户期望虚拟机消耗在GC上的时间不超过程序运行的1/(1+N)。默认值为99,含义是尽可能保证应用程序执行的时间为收集器执行时间的99倍,也就是收集器的时间消耗不超过总运行时间的1%。
同时PS收集器还有一个参数-XX:+UseAdaptiveSizePolicy,该参数是一个开关参数(此开关默认开启),当这个参数被激活,就不需要人工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数提供最合适的停顿时间或最大吞吐量,这被称为垃圾收集的自适应调节策略。
值得注意的是,当使用PS垃圾收集器时,其内存布局有所不同
通常情况下的堆内存布局如下所示:
Parallel Scavenge 堆内存布局如下如所示:
Serial Old(Serial老年代版本)
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它可能有两种用途:
1)在JDK5及之前的版本中与Parallel Scavenge收集器搭配使用;
2)作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old(Parallel Scavenge老年代版本)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。
CMS收集器
CMS收集器是一种以获得最短回收停顿时间为目标的老年代收集器,使用标记-清除算法实现,整个过程分为四个步骤:
-
初始标记(CMS initial mark),会STW,仅仅标记GC Roots能直接关联到的对象,速度很快;
-
并发标记(CMS concurrent mark),从GC Roots直接关联到的对象开始遍历整个对象图,耗时长但可与用户线程一起并发运行;
-
重新标记(CMS remark),会STW比初始标记阶段稍长,修正并发标记期间因用户程序继续运行而导致标记产生变动的那部分对象的标记记录;
-
并发清除(CMS concurrent sweep),清理删除标记阶段判断已经死亡的对象,不移动存活对象,可以与用户线程并发运行。
优点:并发收集、低停顿;
缺点:
-
对处理器资源敏感。CMS默认启动的垃圾回收线程数是(处理器核心数量+3)/4,处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不少于25%的处理器运算资源。但当处理器少于四个时,并发收集过程可能对用户线程的影响很大。为缓解这种情况,虚拟机提供了一种”增量式并发收集器“的CMS变种,但效果一般,JDK7开始已经被声明为”deprecated“;
-
无法处理”浮动垃圾“,有可能出现”Concurren Mode Failure“失败而导致另一次完全STW的Full GC的产生;
Concurren Mode Failure的出现是由于CMS在进行垃圾收集时并发标记和并发清理阶段用户线程还是在继续运行的,还会有新的垃圾不断产生,如果新的垃圾对象无法在老年代中分配,就会出现此问题。
可以通过-XX:CMSInitiatingOccu-pancyFraction参数控制CMS触发百分比,即老年代空间占用率达到此比例时触发垃圾回收,在JDK5下默认配置为68%,JDK6及以后默认值为92%。该参数设置得太高将会很容易导致大量的并发失败产生,性能反而降低;设置得太低会导致CMS触发更加频繁。
- 基于标记-清除算法实现,会导致产生大量的空间碎片。
-XX:UseCMSCompactAtFullCollection开关参数(默认开启,JDK9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,整理过程需要移动对象,无法并发(在Shenandoah和ZGC出现前);
-XX:CMSFullGCsBeforeCompaction设置参数(默认为0,JDK9开始废弃),这个参数要求CMS收集器在执行过若干次不整理空间的Full GC后,下一次进入Full GC前会先进行碎片整理,0表示每次都整理。
Garbage First收集器
垃圾收集器的部分实现细节
如何解决跨代引用?
JVM使用记忆集与卡表来解决跨代引用(部分收集跨区域引用)所带来的问题,以避免把整个老年代加入GC Roots的扫描范围。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,而卡表是其具体实现。JVM为了减少这种记录对内存的消耗,将指定区域按照大小划分为多个卡页。只要卡页内有一个对象存在跨代引用,就将对应卡表位置的元素置为1。每次GC Roots扫描时就会包含这些区域。