概述
垃圾定义:在运行程序中没有任何指针指向的对象
为什么需要GC:如果不进行垃圾回收,内存迟早会被消耗完。垃圾回收除了释放没用的对象,也会清理内存的记录碎片,随着程序的运行,所占内存会越来越多,没有GC就无法保证程序的正常运行
java的GC:java采用的是自动内存管理,无需开发人员手动参与内存的分配与回收,降低内存泄漏与溢出的风险。java堆湿垃圾收集器的工作重点,当然也会包括方法区,但基本不做收集,一般也没有太多的垃圾
算法
垃圾标记阶段算法
垃圾回收器工作时,首先要明确哪些对象需要进行回收。在理论上,判断对象是否可回收一般有两种方法
引用计数法
定义:
- 对象每被引用一次计数器加1,失去引用就减1,计数器为0就标志对象死亡。
优点:
- 实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟
缺点:
- 需要单独字段存储计数器,增加存储空间开销
- 每次赋值需要更新计数器,增加时间开销
- 无法处理循环引用,这个是致命缺陷,需要额外开销来处理循环应用
java没有采用引用计数法,是因为难以处理循环引用关系。python的垃圾处理机制采用的是引用计数法,没有绝对好与坏的技术,只有场景,只要不怎么出现循环引用,引用计数法的效率一般是优于可达性分析的。
可达性分析算法
定义:
- 顺着GC Roots跟一直向下搜索,这个搜索构成一条引用链,在链上的对象就是可达对象,不可达对象就可以判定为可回收对象
GC Roots(根集合)是一组必须活跃的引用:
- 虚拟机栈中引用的对象(方法参数,局部变量,临时变量等)
- 本地方法栈内JNI的引用对象
- 方法区内类静态属性引用的对象(引用类型对象,常量引用对象)
- java虚拟机内部的引用(异常对象,基本数据类型对象,常驻的异常对象)
- 被同步锁synchronized持有的对象
- 反映java虚拟机内部情况的注册回调、本地代码缓存等
- 还有一种情况就是根据用户选择的垃圾收集器以及当前回收的内存区域不用,可能还有有其他对象临时加入,例如G1收集器的分区域回收,这个区域里面的对象有可能是被其他区域对象引用,因此需要将关联区域对象一并加入GC Roots考虑
优点:
- 不会出现对象循环引用问题
缺点:
- 使用可达性分析算法判断内存是否可回收,分析工作必须在一个能保障一致性的快照中进行,否则会无法保证准确性。这也就是GC过程中STW(stop-the-world)的重要原因。
对象的finalization机制
当垃圾回收期发现没有引用指向一个对象,垃圾会输此对象之前,总会先调用这个独享的finalize()方法。主语不要主动调用这个方法,应当交给垃圾回收机制调用。
基于这个机制,java对象在虚拟机中存在三个状态:
- 可触及的:从根节点开始,可以到达这个对象
- 可复活的:对象不可达,但是对象有可能在finalize()中复活
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及的,finalize()方法只会被调用一次
判断对象是否可回收,至少需要两次标记:
- 对象与GC Roots之间没有引用链进行第一次标记
- 进行筛选,判断此对象是否需要执行finalize()方法,
- 如果对象没有重写finalize()方法或者已经被虚拟机调用过,那么对象判定为不可触及的
- 对象重写了finalize()方法,并且还为执行,那么就会进行一个队列,并有徐建机自动创建的,帝有限的finalizer线程触发
- 执行finalize()方法时,如果对象与引用链上对象建立联系,那么在此次标记过程中,就会被移出即将回收集合
代码如下:
public class Finalization { public static Finalization finalization; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("over write the finalize"); finalization = this; } public static void main(String[] args) throws InterruptedException { finalization = new Finalization(); // release finalization finalization = null; System.out.println("start first gc"); // start GC System.gc(); // stop the thread, wait the finalizer thread run and watch the status Thread.sleep(10000); if(Objects.nonNull(finalization)){ System.out.println("finalization still alive"); }else { System.out.println("finalization has been dead"); } // release finalization finalization = null; System.out.println("start second gc"); System.gc(); Thread.sleep(10000); if(Objects.nonNull(finalization)){ System.out.println("finalization still alive"); }else { System.out.println("finalization has been dead"); } /** * start first gc * over write the finalize * finalization still alive * start second gc * finalization has been dead */ } }
垃圾清除阶段算法
当完成垃圾表完之后,GC接下来就是执行垃圾回收,释放无用的对象所占用的内存空间
标记-清除算法(Mark-Sweep)
一种非常基础和常见的垃圾收集算法。首先标记出所有需要回收的对象,然后对标记的对象进行统一清除,清空对象所占的内存区域
缺点:
- 执行效率不可控,如果对堆大部分对象都是可回首时,收集器要执行大量的标记操作
- 清理出来的空闲内存不是连续的,产生内存碎片,需要维护一个空闲列表
这里的清除,并非是真的质控,只是把需要清楚的对象地址保存到空闲的地址列表中。如果有新的对象需要加载时,判断垃圾的位置空间是否足够,如果够就存放
复制算法(Copying)
针对标记-清除早垃圾收集效率与内存碎片方面的缺陷,提出了复制算法。
将内存空间分为两块(运行区域,预留区域),每次只使用其中一块,在垃圾回收时将运行区域的内存中的活着的对象复制到预留区域的内存块中,之后清除运行区域的所有对象,交换两个内存角色,最后完成垃圾回收
优点:
- 没有标记和清除的过程,实现比较简单没运行高效
- 保证空间的连续性,不会出现内存碎片问题
缺点:
- 需要两倍的内存空间
- 移动对象时,如果被其他对象引用,需要调整引用地址
- 当然也是需要STW
如果系统的中垃圾非常多,复制算法需要复制的对象不会太对,或者很少,那么这个算法就会很有优势,比如新生代中,我们都是到大部分对象是朝生夕死,复制算法非常适用这种情况,进本现在虚拟机的新生代都是用的是复制算法
标记-压缩算法(Mark-Compact)
对于老年代的内存空间,大部分对象存活时间都很长,并且空间比较大,复制算法不太适用,因此又提出这个标记-压缩算法,又称为标记-整理算法。
该算法在第一阶段与标记-清除算法一致,但是在压缩过程就是将存活对象压缩到内存的一端,按顺序排放,之后清除边界外的空间。因此它的效果等同于标记-清除算法完成之后,在进行一次内存碎片压缩。标记存活对象将会被整理,并且按照内存地址一次排列,而未被标记的内存会被清理,这样如果给新对象分配内存的时候,jvm仅需要持有一个内存的起始地址即可,这比维护一个空闲列表少了很多开销。
优点:
- 消除了标记-清除算法中,内存区域分散的缺点,需要给新对象分配内存时,仅需要持有一个内存的起始地址
- 消除了复制算法中,内存减半的高额代价
缺点:
- 效率比标记-整理与复制算法都要低
- 移动对象是需要调整引用对象的引用地址
- 需要STW
分代收集算法
从效率角度考虑,复制算法是最好的,但是浪费一倍的内存。
但是从速度,空间开销,移动对象角度考虑,标记-压缩算法相对比较平滑,但是效率是最低的,比复制算法多了一个标记过程,比标记-清除多了一个整理内存过程。每个算法都有各自的优势以及特点,适用于不同的垃圾回收场景,这个时候按照java堆的分代收集算法应运而生。
目前几乎所有的垃圾收集器都是基于分代收集算法进行回收
年轻代:
区域相对于老年代较少,对象生命周期短,存活率低,回收比较频繁,这种情况下采用复制算法回收整理,速度是最快的。复制算法的效率只跟当前存活对象的大小有关,很适合年轻代回收。而针对内存效率不高,采用两个幸存区(survivor)区来缓解
老年代:
区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。复制算法就不太适合,一般采用标记-清除或者标记-压缩算法。
- 标计(Mark)阶段的开销与存活对象的数量成正比
- 清除(Sweep)阶段的开销与所管理的区域大小成正比
- 压缩(Compact)阶段的开销与存货对象的数据成正比
增量收集算法(Incremental Collecting)
标记清除,复制,标记压缩算法,在来季回收过程中,应用软件都将处于STW状态,而STW会将所有用户线程挂起,暂停一切正常的工作,如果垃圾回收时间过长,将严重影响用户体验或者西永稳定行,这时增量收集算法诞生了
垃圾收集线程每次只收集一小片的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成。增量收集算法基础仍然是上述基础算法。增量收集算法通过对线程之间冲突的处理,允许牢记收集分阶段完成标记,清理或者复制工作
缺点:垃圾回收过程中,间接性的还执行了应用程序代码,所以可以减少系统停顿时间。但是因为会增加线程切换以及上下文转换消耗,会使得垃圾回收总成本上升,造成系统吞吐量下降。
响应时间与吞吐量总是两个比较矛盾的选择,我们需要结合具体业务场景选择
分区算法
在相同条件下,堆空间越大,一次GC所用时间越长。为了更好的控制GC所产生的停顿时间,将一个大的内存区域分割为多个小块,根据设定的停顿时间,每次合理收集若干个小区间,从而减少一次GC产生的停顿。
每一个区间都独立使用,独立回收,这样的好处就是可以控制一次回收多少个小区间。内存空间越大,这个算法优势越明显。
概念
内存溢出
没有空闲内存,并且垃圾收集器也无法提供更多的内存
内存泄漏
对象不会再被程序用到,但是GC又不能回收这些对象,例如单例模式持有外部对象,如果这个外部对象不再使用,但是这个对象不会被回收,或者一些文件或者数据库连接忘记关闭
Stop The World
GC时间发生过程中,会产生应用程序的停顿。例如可达性分析算法中枚举根节点,会导致所有的java线程停顿,因为分析工作必须在一个快照中完成,否则无法保证数据准确性,所有的GC都有这个事件
安全点
程序并非所有的位置都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置成为安全点。一般选取安全点会根据是否有让程序长时间执行的特性为标准,比如方法调用,循环跳转或者异常跳转等。
一般发生GC的时候,会设置一个中断标志,每个线程到达安全点之后会主动轮询这个标志,如果终端标志为真,自己中断挂起。
安全区域
在一段代码块中,对象的引用关系不会发生变化,在这个区域内任何位置开始GC都是安全的。比如线程进入wait或者bolcked状态。
当线程运行到安全区域是,首先会标识已经进入安全区域,如果发生GC,jvm会护士这个线程
当线程即将离开安全区域是,会检查jvm是否已经完成GC,如果完成,继续运行,否则就会一直等到GC完成信号,才可以继续运行
引用
终结器引用是包内可见,用不到
强引用(StrongReference):
程序代码中普遍存在的引用赋值,只要强引用还在,垃圾收集器就永远不可能回收掉被引用的对象
强引用可以直接访问目标对象,虚拟机即使抛出内存溢出异常,也不会回收强引用
软引用(SoftReference):
系统将要发出内存溢出之前,就会对这些对象进行二次回收,如果空间还不够,才会抛出内存溢出异常
软引用一般是用来描述一些还有用,但是非必需的对象,通常用来实现内存敏感的缓存
// 虚拟机参数-Xms10m -Xmx10m public static void main(String[] args) { Object object = new Object(); SoftReference<Object> softReference = new SoftReference<>(object); object = null; // cancel string reference // get object from soft reference System.out.println(softReference.get()); System.out.println("soft reference->" + softReference.get()); System.gc(); System.out.println("soft reference after gc->" + softReference.get()); try { byte[] bytes = new byte[1024 * 1024 * 9]; } catch (Throwable e) { e.printStackTrace(); } finally { // before OOM, the reference will be re-cycle System.out.println("soft reference after make bytes->" + softReference.get()); } /** * java.lang.Object@7c30a502 * soft reference->java.lang.Object@7c30a502 * soft reference after gc->java.lang.Object@7c30a502 * soft reference after make bytes->null * java.lang.OutOfMemoryError: Java heap space * at com.yang.reference.Soft.main(Soft.java:40) */ }
弱引用(WeakReference):
只能生存到下一次垃圾收集之前,无论内存是否足够都会回收
public static void main(String[] args) { Object object = new Object(); WeakReference<Object> weakReference = new WeakReference<>(object); WeakReference<Object> weakReference2 = new WeakReference<>(new Object()); System.out.println("weak reference=> " + weakReference.get()); object = null; System.gc(); System.out.println("weak reference after gc => " + weakReference.get()); System.out.println("weak reference after gc => " + weakReference2.get()); /** * weak reference=> java.lang.Object@1554909b * weak reference after gc => null */ }
虚引用(PhantomReference):
一个对象是否有虚引用,对其生存事件不构成任何影响,也无法通过虚引用获取实例。唯一目的就是能够在对象被收集器回收之前得到一个系统通知
分代回收
新建对象首先会先分配在伊甸园区
年轻代空间不足时,会触发Minor GC,伊甸园和幸存0(from)区的存货对象项使用复制移动到幸存1(to)区,存货对象的年龄加1,
如果对象的年龄满足晋升阈值,最大寿命时,默认是15(4bit),对象会直接从from区移动到老年代,或者幸存区放不下的大对象也会直接进入老年代,还有一种情况就是如果相同年龄的对象占据幸存者1区的一半,name年龄等于或大于这个年龄的对象会直接晋升老年代
Minor GC会触发STW,暂停其他用户新城,等垃圾回收结束之后,用户线程才恢复运行
当老年代空间不足,会先尝试触发Minor GC(取决于垃圾回收器,非绝对),如果空间仍不足,则会触发Full GC或者Major GC,STW的时间会很长。
垃圾回收器
垃圾回收器的分类
垃圾回收器按照线程数分,可以分为串行垃圾回收器和并行垃圾回收器
- 串行垃圾回收器:同一个时间段内只允许一个cpu执行垃圾回收操作,对于单处理器或者较小应用内存,串行垃圾回收器运行i笑傲率更好,因为减少了线程上下文切换的开销
- 并行垃圾回收器:运行多个cpu同时执行垃圾回收,也就是多线程去泡,对于多核cpu来讲,会提升应用的吞吐量。同样也是独占式,使用STW机制
按照工作模式可以分为并发式垃圾处理器与独占式垃圾回收器
- 并发式垃圾回收器:与应用线程交替工作,尽可能减少应用程序停顿时间
- 独占式垃圾回收器:运行时,会停止应用程序中所有的用户进行,知道完成GC
按照碎片处理方式,分为压缩式垃圾回收器和非压缩式垃圾回收器
- 压缩式垃圾回收器:在回收完成之后,对存活对象进行压缩整理,清理回收之后的碎片
- 非压缩式垃圾回收器:回收完成之后,不进行压缩
按照工作内存区间封你为年轻代垃圾回收器和老年代垃圾回收器。
GC的性能指标
- 吞吐量:用户代码运行的时间与总运行时间的比例(总运行时间为用户代码运行时间与垃圾回收时间){垃圾收集开销:吞吐量的补数,即垃圾回收时间与总运行时间的比例}
- 暂停时间:执行垃圾收集是,用户线程被暂停的时间
- 收集频率:对比应用程序的执行,收集操作发生的频率
这三个与CAP理论一样,不能同时做少,只是随着技术的成熟逐步在变好,一般我们主要抓住:吞吐量以及暂停时间
- 高吞吐量一般就会降低内存回收的执行频率,这样会导致GC需要更长的暂停时间执行内存回收,但是减少了上下文切换以及线程切换,完成得任务量肯定越多,一般适用于没有太多交互场景的,比如后台计算任务
- 低暂停时间就是降低每次执行内存回收时的暂停时间,因此就会频繁的执行垃圾回收,这样响应时间锁单名单时会导致吞吐量降低
经典的垃圾回收器
串行回收器:Serial,Serial Old
并行垃圾回收器:ParNew, Paraller Scavenge,Paraller Old
并发垃圾回收器:CMS,G1
年轻代垃圾收集器:Serial, ParaNew, Parallel Scavenge
老年代垃圾收集器:Serial Old, Parallel Old, CMS
整堆收集器:G1
垃圾回收器之间的组合关系:
垃圾收集器
Serial收集器:
一个单线程的收集器(只有一个线程在运行,并不是只有一条线程去完成),在进行垃圾收集时,必须暂停其他所有工作线程。这种单线程的垃圾收集器在目前的java web很少用到,因为现在都不再是单核服务器了。使用-XX:UseSerialGC启用该垃圾回收器,但是根据组合关系,秒人老年代也会启用Serial Old GC
ParNew收集器:
也就是Parallel New收集器,采用的就是并行回收年轻代的垃圾,除了并行其余与Serial GC没有区别。使用 -XX:UseParNewGc手动指定使用ParNewGC收集年轻代,同时可以使用 -XX:ParallelGCThreads限制线程数,默认开启与cpu相同的线程数
Parallel Scavenge收集器:
采用复制算法,并行回收,也是用STW机制,该处理器主要目标是达到一个可控制吞吐量的垃圾处理器,具有自适应调节策略。手动开启 -XX:UseParallelGC,根据上面的组合关系,默认也会开始老年代使用Parallel Old GC
-XX:ParallelGcTHreads设置年轻代并行收集器的线程数,默认时cpu的核数,如果超过,则默认值为 3 + (5 * cup核数/8)
-XX:MaxGcPauseMilles设置垃圾收集器的最大停顿时间,设置之后,Parallel Scavenge收集器将会在工作时调整java对的代销或者其他参数,这个值设置需要谨慎,如果设置过小,堆调整的很小,就会影响吞吐量
-XX:GCTimeRatio设置垃圾收集时间与总时间的占比,公式为(1/(n+1)),默认值时99,也就是垃圾回收时间不超过1%
-XX:UseAdaptiveSizePolicy设置收集器具备自适应调节策略,这种模式下,jvm会自动调节年轻代大小,伊甸园区与幸存去的比例以及今生老年代的年龄值大小。手动调有比较困难场景,建议设置这个参数以及垃圾回收时间与总时间的占比
CMS收集器
第一款真正意义上的并发收集器,实现了用户线程与垃圾收集线程同时工作。
CMS关注点就是尽可能缩短垃圾收集时用户线程停顿时间,适合多交互的场景,采用的时标记清除算法。
主要分为四个节点:
- 初始标记阶段:标记GC Roots可以直接关联到的对象,需要STW,一旦标记完成则回复用户线程。由于直接关联对象小,速度很快。这个过程也就是枚举GC Roots,以及直接关联对象
- 并发标记阶段:从GC Root的直接关联对象开始白能力这个引用链,耗时较长,但是始于用户线程并发运行
- 重新标记阶段:修正并发标记期间因为用户线程运行导致标记发生变动的一部分对象标记,这恶鬼阶段比并发标记时间段。这个过程会暂停用户线程
- 垃圾回收阶段:清理删除标记阶段已判定死亡的对象,释放内存空间,由于使用的是标记-清除,不需要移动对象,因此可以与用户线程并发执行
CMS在比较耗费时间的并发标记以及并发请理阶段并没有挂起用户线程,因此整体的回收是低停顿的。如果早CMS运行期间预留的内存无法满足程序需要,就会执行后备方案,临时启用Serial Old GC进行老年代垃圾收集
优点:
- 并发收集
- 低延迟
缺点:
- 会产生内存碎片:采用标记-清除算法,会产生内存碎片
- 对cpu资源敏感:因为与用户进程一起工作,因为占用一部分线程,总的吞吐量降低
- 无法处理浮动垃圾:并发标记阶段,如果产生新的垃圾,无法对其标记,本次垃圾回收不能及时回收
-XX:+UseConcMarkSweepGC:使用CMS收集器,年轻代就会启用ParNew,备用老年代就是Serial Old
-XX:CMSInitiatingOccupanyFraction:设置堆内存的使用阈值,因为是并发执行,因此默认阈值时68,当老年代使用空间达到68%之后就会执行垃圾回收,如果阈值设置过大,有可能导致垃圾回收时,空间不足,使用Serial Old进行回收
-XX:+UseCMSCompactAtFullCollection:指定垃圾回收完成之后进行碎片整理,但是会导致停顿时间百年城
-XX:CMSFullGCsBeforeCompaction:设置多少此Full GC之后急性压缩整理
-XX:ParallelCMSThreads:设置CMS线程数量,默认是 年轻代垃圾收集器的线程树做运算(ParallelGCThreads+3)/4
如果最小化的使用内存与并行开销使用Serial GC;
如果最大化应用程序吞吐量,使用Parallel GC;
如果最小化GC的停顿时间,使用CMS
G1(Garbage First)收集器
为适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。G1诞生,目标就是在延迟可控的前提下,尽可能获得更高的吞吐量。
G1是一个并行回收器,他采用的就是分区算法,将堆内存分割为很多不相关的区域(Region),使用不同的Region来表示Eden,幸存者0区(from),幸存者1区(to),老年代等
G1 GC会尽量避免在整个java堆中进行全区域垃圾收集,G1会记录每个区域的空间大小,以及垃圾数量,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域
G1是JDK9之后默认的垃圾回收器,主要面对的就是服务端应用,针对配备多核以及大容量内存的机器,以极高概率满足GC停顿时间的同时,兼顾高吞吐量的特征。
优点:
并行与并发:
- 并行性:在G1回收期间,可以多线程同时工作,此时用户线程STW
- 并发性:部分工作可以与用户线程交替执行,整个垃圾回收阶段不会一致阻塞用户线程情况
分代收集:
- 属于分代垃圾回收器,会区分年轻代与老年代,但从堆空间结构来看并不要求整个伊甸园区,幸存者0区,幸存者1区,老年代是连续的,也不再坚持固定大小与固定数量
- 堆空间分为若干区域,包含逻辑上的年轻代以及老年代
- 可以同时兼顾年轻代与老年代
空间整合
- region之间是复制算法,但整体上看可以说是标记-压缩算法
- 将内存划分为若干个region,回收是以region为单位收集,G1新增加一种内存区域Humongous内存区域,主要存储大对象,如果超过1.5个region,就会放入humongous区域。这样就不必把大对象直接放入老年代。虽然G1大多时候都会吧大对象当做拉年代一部分看待。
缺点:
- G1为了垃圾收集产生的内存占用或者程序运行时的额外执行负载都比CMS要高
- 小内存应用一般CMS表现会优于G1,G1内存越大优势越明显,平衡点一般就是6-8G
参数设置:
-XX:+UseG1GC 手动指定使用G1,但JDK9之后就是默认的
-XX:MaxGCPauseMillis 设值期望达到的最大GC停顿时间(默认时200ms),jvm会尽力实现,但是不保证
-XX:ParallelGCThreads 设置STW的工作线程,最多设置8个
-XX:ConcGCThreads 设置并发标记的线程数 设置并发标记的线程数。一般设置为(parallelDCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent 设置复查并发GC周期的java堆占用率预支,超过此值,就会触发GC,默认是45
G1设计原则就是简化jvm的性能调优,一般只需要开启G1垃圾收集器,设置堆的最大内存,设置最大的停顿时间就可以
适用场景:
具有大内存,多处理器的机器(一般需要堆的大小超过6G),G1在GC时,如果GC线程处理缓慢,系统会调用应用程序线程帮忙加速垃圾回收过程,其他垃圾收集器都只会使用内置的JVM线程
垃圾回收模式
在G1中最突出的问题就是一个区间的对象可能被其他区间的对象引用,在进行标记阶段,如果不做任何处理,就需要扫描整个java堆才行,这样会降低GC的效率,其他垃圾回收器也存在这个问题,但是G1更为突出。jvm一般就是采用Remembered Set来比秒全局扫描。每一个Region(G1)或者内存空间(其他垃圾收集器)都有一个Remembered Set来避免全局扫描,每次有一个引用(reference)类型加入region(或者内存区域时)会暂时中断操作,判断将要写入的引用关系的对象是否与当前对象在一个region(内存空间),如果有就将引用信息记录到当前remember Set中,这样在GC Roots枚举的时候就会加入RememberSet,保证不进行全局扫描也不会遗漏。
年轻代GC(Minor GC):
伊甸园区的内存用尽,就会开始年轻代回收过程,是一个并行的独占式收集器,会暂停所有的用户线程,年轻代垃圾回收只会回收伊甸园区与幸存者区域,年轻代回收过程回收集包含全部的伊甸园区与幸存者区
- 扫描GC Roots
- 更新Remember Set:因为在将信息保存到Remember Set并非直接保存,而是通过一个dirty card queue入队一个保存对象信息的card来进行的,因此需要更新
- 处理Remember Set:主要就是识别这里面的引用对象存活
- 复制对象:遍历对象树,伊甸园区存活的对象复制到幸存者区中空的内存分段,并年龄加1,幸存者区中存活的对象年龄未达到阈值也会复制过去,如果达到就是进入老年代,但是如果幸存者区空间不做,则对象会直接进入老年代
- 处理引用:处理Soft,Week,Phoantom,Finalize等引用,最终伊甸园区空间变空,目标内存中对象都是连续的,没有碎片,因为region之间采用的就是复制算法
老年代并发标记过程(Concurrent Marking):
当堆内存使用达到一定的阈值(默认45),开始老年代并发标记过程
- 初始阶段标记:标记直接可达对象,这个阶段是STW
- 根区域扫描:扫描幸存者去直接可达的老年代区域对象,并标记为被引用对象,在Minor GC之前完成
- 并发标记:在整个堆并发标记(与应用程序并发执行)有可能被Minor GC中断。如果发现某个区域全是垃圾,则这个区域会立马被回收。标记过程会计算当前区域对象的存活比例
- 再次标记:修正上一次标记的结果,STW
- 独占清理:计算各个区域的存活对象以及GC的回收比例,进行排序,STW
- 并发清理:识别清理完全空闲的区域
混合回收(Mixed GC):
- 完成老年代并发标记过程就会进行混合回收,老年代区间移动存活对象到空闲区间,这些空闲区间就会变成老年代的一部分,当然并非回收全部的老年代区间,一般只会扫描回收一部分老年代以及回收整个年轻代
- 混合回收阶段,老年代中100%为垃圾的老年代内存区域已经被回收,部分为垃圾的内存区域被计算出来,默认情况下。老年代分为8次回收(可以通过 -XX:G1MixedGCCountTarget=8进行设置)
- 混合回收的回收集包括八分之一的老年代内存分段,伊甸园区内存分段以及幸存者区内存分段。回收算法与年轻代回收算法一致
- 老年代中内存分段默认分为8次回收,G1会优先回收垃圾较多的内存分段
- 混合回收也并不一定会回收8次,有一个阈值(-XX:G1HeapWastePercent=5)默认值是5%,意思就是允许整个堆内存有5%的空间被浪费,如果发现可回收垃圾占比小于5%,则不在进行混合回收
Full GC:
如果上面的方式无法正常工作,就会STW,进行单线程,独占式,高强度的GC。就是一种GC失败之后的保护机制。类似于CMS使用Serial Old GC最补偿是一样的
一般会触发的原因就是没有足够的空间存储晋升对象或者并发处理过程中内存耗尽
总结
在进行选择垃圾回收器是一般原则就是:
- 如果内存小于100M或者单cpu就使用串行收集器
- 如果是多cpu,需要高吞吐量,允许停顿时间超过1s则选用并行收集器
- 如果是多cpu,追求低停顿时间,需要快速响应,选择并发收集器
jvm参数的官方网站:https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html