GC回收
基于正在使用的对象进行遍历,对存活的对象进行标记,其未标记的对象可以认为是垃圾对象,然后基于特定的算法进行回收,这个过程称之为GC(Garbage Collection)。
通过以下几个方面进行实现
- GC判断策略(例如引用计数、对象可达性分析)
- GC收集算法(标记-清除、标记-清除-整理、标记-复制-清楚)
- GC收集器(例如Serial,Parallel,CMS,G1)
GC种类
手动GC
显示地进行内存分配(allocate)和内存释放(free)。如果忘记释放,该内存就不能再次被使用,称之为内存泄漏(memory leak)
手动需要C语言进行,java不能够手动操作内存
自动GC
一般实在JVM系统内存不足时,有JVM系统启动GC对象,自动对内存进行垃圾回收
如果GC的速度小于对象分配内存的时候,也会造成内存溢出
引用计数法
绿色圆圈是内存中的根对象,表示程序正在使用的对象
蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数
灰色圆圈是内存中没有活动对象引用的对象,表示非活动对象(垃圾对象)
存在一个设计缺陷即循环引用,如上图的红色即为垃圾,有计数 无法被回收,引发内存泄漏
通过弱引用、软引用或者其他算法来排除
弱引用weak reference:WeakReference的一个特点是它何时被回收是不可确定的, 因为这是由GC运行的不确定性所确定的. 所以, 一般用weak reference引用的对象是有价值被cache, 而且很容易被重新被构建, 且很消耗内存的对象.
在weak reference指向的对象被回收后, weak reference本身其实也就没有用了. java提供了一个ReferenceQueue来保存这些所 指向的对象已经被回收的reference. 用法是在定义WeakReference的时候将一个ReferenceQueue的对象作为参数传入构造函数.
软引用SoftReference:soft reference和weak reference一样, 但被GC回收的时候需要多一个条件: 当系统内存不足时, soft reference指向的object才会被回收. 正因为有这个特性, soft reference比weak reference更加适合做cache objects的reference. 因为它可以尽可能的retain cached objects, 减少重建他们所需的时间和消耗
标记清除
标记清楚就是对可达对象进行标记,不可达对象即认为是垃圾,进行清楚
通常分为两个步骤
- 标记所有可到达的对象(reachable objects)
- 清除不可到达对象占用的内存空间
可达性行分析:
GC遍历内存中整体的对象关系图确定根对象(根对象的定义)
栈中直接引用的对象
常量池中引用的对象
大致可以理解为正在使用的对象
根对象对所有对象进行依赖查找,所有可以被遍历访问到的对象,对其进行标记,即为存活对象,也称为可达性
解决了循坏依赖的问题,但是存在短时间的线程暂停,这种现象为STW停顿(Stop The World Pause,权限暂停 ),暂停时间和堆内存的大小、对象的总数没有直接关系,而是同存活对象的数量来决定。
算法
标记-清除
即为上述所讲模式,不会对碎片进行整理
标记-清除-整理
上述模式以外,还会进行内存整理,但是会增加GC暂停时间
标记-整理
基于标记-清除-整理算法,创建新的内存用于存储幸存对象,同时复制和标记并发进行,可以减少GC时间,牺牲空间换取时间
碎片整理
系统GC每次执行清除(sweeping)操作,JVN都必须保证“不可达对象”占用的内存被回收然后重用。虽然内存回收了,但会创建大量的内存碎片(类似于磁盘碎片),进而引发两个问题:
- 对象创建时,执行写入操作越来越耗时,因为JVM系统需要花费更多的时间去寻找对应大小的内存空间
- 对象创建需要在一块连续的内存空间中分配内存。如果碎片问题非常严重,直至没有空闲片段能够存放新建的对象,就会发生内存分配错误(alloaction error)
因此JVM启动GC收集垃圾时不仅仅需要标记和清除,还需要执行“内存碎片整理”。整个过程会让所有的可达对象进行依次移动,进而减少或者消除内存碎片
分代设想
垃圾收集需要停止程序的进行,如果收集的过程很长,就会大大影响系统的性能。为了解决这个问题,通过实验发现内存的对象大致分为两大类
- 存活时间较长(这类对象比较少)
- 存活时间较短(这类对象比较多)
因此将VM中将内存分为年轻代(后者)和老年代(前者),采用不同的算法用来提高GC的性能。
缺点:不同分代的对象依旧会存在相互引用,难以回收
对象分配
基于默认的JVM内存架构,对象创建时内存的分配过程如下
编译器通过逃逸分析(JDK8默认开启),确认对象在堆或栈上分配
如果在堆上分配,则首先检测是否可以在TLAB(Thread Local Alloaction Buffer)上直接分配
如TLAB不能分配,则分到Eden加锁区分配(线程共享区)
如果Eden区无法分配对象,则执行Yong GC(Minor Collection)
如果Yong GC之后Eden仍不能分配存储对象,则直接分配到老年代
TLAB:线程本地应用缓存,在伊甸园区的私有区,空间小没有锁,效率快。如果不能分配,线程会再次申请,还是失败则会分配到伊甸园区
Young GC
- GC触发时所有可达对象会被复制到一个幸存区(例如S1),S1如果无法存储这些对象会被直接复制到老年代
- GC再次触发时Eden和S1的可达对象会被复制到S2,同时清空S1和Eden
- GC再次触发时Eden和S2的可达对象会被复制到S1,同时清空S2和Eden,以次类推
- 每次GC幸存对象都会年龄+1,到达一定阈值直接分配到老年代(可用参数 -XX:+MaxTenuringThreshold 来指定上线,默认15)
s1和s2至少有一个是空的,用来存放下次GC未被收集的对象
GC模式
垃圾收集事件通常分为:
- Minor GC(小型GC):年轻代GC,新对象分配频率越高,GC越多
- Major GC(大型GC):老年代GC
- Full GC(完全GC):整个堆的GC事件
GC收集器
下文出现的吞吐量= 运行用户代码的时间/( 运行用户代码的时间+垃圾收集时间)
算法搭配
- 年轻代和老年代串行收集器:Serial GC
- 年轻代和老年代并行收集器:Parallel Gc
- 年轻代并行和老年代并发收集器:Parallel Gc New 和 CMS-Concurrent Mark and Sweep
- G1收集器
Serial GC串行收集器
参数配置: -XX:+UseSerialGC
应用特点
- 内部仅一个线程执行垃圾回收
- GC时Stop the world 时间较长
场景引用
- JVM的客户端模式,实时性要求不高
- 适用于CPU个数或者核数较少且内存空间少的环境
算法运用
- 新生代标记-复制(存活对象少)
- 老年代标记-清除-整理(回收少,碎片多)
CMS收集器
Mostly Concurrent Mark and Sweep Garbage Collector
设计目标是追求更快的响应时间
参数
使用CMS配置 -XX:+UseConMarSweepGC, 默认开启 -XX:++UseParNewGC
- -XX:UseCMSCompactAtFullCollection 执行full GC后,进行一次碎片整理,整理的过程是独占的,会引起停顿的时间变长
- -XX:+CMSFullGCsBeforeCompaction 设置进行几次full GC之后,进行一次碎片整理
- -XX:ParallelCMSThreads 设定CMS线程数量(一般情况可约等于CPU数量)
应用特点
空闲列表(free-lists)管理内存的回收,不对老年代进行碎片整理
在标记-清除阶段大部分工作和用户线程一起并发执行
优点是性能高,每次GC的时间都很短
缺点是CPU资源较少时,GC平均占用着,CMS会比并行GC的吞吐量小一些
老年代的碎片无法处理,特别是在堆内存设置非常大的情况下,GC几次full GC后碎片清理会进行不可预测甚至长时间的暂停
场景应用
- 适用于多个或者多核处理器,响应时间优先
- CPU受限下,同用户竞争CPU,吞吐量少
算法运用
- 新生代并行的标记-复制
- 老年代并发标记-清除
Parallel收集器
并行收集器,利用多个或者多核CPU优势实现多线程并行GC操作
配置参数
使用Parallel GC配置 -XX:+UseParallelGC, 默认开启 -XX:++UseParallelOldaGC
- -XX:ParallelGCThread=20 设置并行收集器并行数,最好和处理器数目相等
- -XX:MaxGCPauseMills=100 设置每次年轻代垃圾回收最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
- -XX:+UseAdaptiveSizePolicy 设置并行收集器自动选择年轻代大小和幸存区的比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用该收集器时一直打开
- -XX:GCTimeRatio=99 设置吞吐量大小,默认值为99,就是将垃圾回收的时间设置成总时间的1%,它的值是0-100的整数值。假设值为n,则垃圾收集时间不得超过1/(1+n)
应用特点
- 利用CPU的多核特性执行多线程下的并行化GC操作
- GC期间,所有CPU内核都在并行清理垃圾,暂停时间短
- 实现可控吞吐量和停顿时间(可配置)
场景运用
- GC操作仍需暂停应用程序,GC不能打断,不适合低延迟要求场景,适用于需要高吞吐量而对暂停时间不敏感的场合
- 后台计算、后台处理的弱交互场景而非web交互场景
算法应用
- 年轻代标记-复制,对应Parallel Scavenge收集器
- 老年代标记-清除-整理,对应Parallel old收集器
G1收集器
在JDK8中属于准产品
一种工作于服务端模式的垃圾回收器,面向多核、大内存的服务器,在实现高吞吐量时,也满足了GC的停顿时间可控。未来G1计划全面取代CMS。
- 可以像CMS收集器一样能够同时和应用线程一起并发执行
- 减少内存停顿时间
- 满足可预测的停顿需求
- 属于压缩型收集器,可以实现有效的空间压缩。消除大部分内存碎片问题
实现方式
在G1中堆不再区分年轻代和老年代,二十划分为多个(通常是2048个)存放对象的小堆区(small heap regions)。每个小堆区可以是eden、survivor、old,请两者合起来为年轻代。
- 每次GC只处理一部分小堆区,陈为此次的回收集(collection set),收集所有年轻代和部分老年代的小堆区
- 类似于CMS,在并发阶段会事先估计每个小堆区存活对象的数量,垃圾最多的被优先收集。
- 基于停顿时间目标来选择需要回收、压缩的小堆区数量
- G1基于标记、清理对应的region时,会将对象从一个或者多个region中复制到另一个region,在这个过程中伴随释放和压缩内存
- 基于多核并行执行,满足其他收集器所不具备的(CMS不能压碎碎片、ParallelOld只能较长时间的整堆压缩)
G1不是一个实时的收集器,它只是近最大可能来满足设定停顿时间。基于以往的收集数据和指定的停顿时间来确定收集几个regions
region:1-2m,最多200个,最大支持内存64G
配置参数
-XX:+UseG1GC 启用G1收集器
-XX:MaxGCPauseMillis=200
设置最大停顿时间指标,是一个软指标,JVM会尽力区完成这个指标,默认值为200毫秒
-XX:InitiatingHeapOccupancyPercent=45
启动GC时整个堆内存的占比,默认45即45%
特点
- 内存运用比较弹性
- 支持并行和并发
运用场景
FullGC发生相对比较频繁或总时长比较长
对象分配或对象进入老年代比例波动大
较长内存停顿
如果其他收集器运用良好,不建议更换G1
算法分析
- 年轻代标记复制
- 老年代标记复制整理
关键步骤
- 初始标记:属于YoungGC,具有STW,对持有老年代引用的survivor(root)进行标记
- 根区扫描:并发执行,对root进行扫描
- 并发标记:找出整个堆中存活对象,空区域标记为”x“,会被young GC中断
- 再次标记:完全堆堆中存活对象标记,采用比CMS更快的SATB算法
- 清理:并发执行,统计小队去存活对象,并对小堆区进行排序,清理垃圾,释放内存
- 复制/清理:小堆区中未被清理的对象进行复制,然后清理
后记
值得一提的是HotSpot工程师主要精力一直放在不断改进G1上,新版本JDK会带来新的功能和优化