GC
在程序运行期间,不再被引用的对象需要被回收(也就是删除),这样可以让空间有效的循环被利用,java支持的是垃圾自动回收,程序员也可以手动的在代码中编写System.gc()来强制进行一次立即的内存清理。
GC的区域
GC主要回收的区域是堆区,极少出现在方法区里。如果出现在方法区内,主要是对常量池的回收和类型的卸载,回收的内存比较少;
哪些对象不会GC
- 栈中引用的对象。
- 方法区中的静态成员和常量
- Native方法中引用的对象。
GC的工作方式—回收算法
引用计数器:A对象每次被引用的时候,该对象的计数器就会加1,当不被引用的时候就会-1,当这个对象的计数器值为0的时候,也就是它要被GC的时候。下面利用这个简单粗暴的图帮助你更清晰的了解一下。目前都是python在用,java并没有发现用到过
标记—清除算法:将所有存活的对象打上标记,在GC阶段没有被打上标记的就示为要被回收的对象。优点:实现简单。
缺点:1.碎片空间不连续,举例GC后总剩余空间10KB,有个A对象5KB,但由于剩余的空间都是碎片的空间组成的,没有连续空间为5KB的空间,就会导致空间够却无法将A对象放入到内存中
2.对象分配到内存的速度:每次在分配对象内存时,需要遍历空闲的分块,找到适合该对象的内存块放进去,比较浪费时间
概念理解如下图:
复制算法:一般用于青年代。将内存对半分,一半作为空闲区,一半作为工作区。当对象在本次工作区装不下的时候,那么把本次工作区的所有存活的对象移动堆顶指针,按顺序复制到空闲区,之后将工作区一次清除掉变成下次空闲区,而本次空闲区变成了下次工作区
优点:1.避免碎片过多导致剩余空间明明足够存储对象却无法存储的问题,碎片化
2.分配速度提升:移动指针分配,非遍历空闲链表分配
3.吞吐量较高,兼容缓存
概念理解如下图:
标记—整理算法:一般用于老年代。每一次GC将所有存活下来的对象移动到内存另一端,之后清除移动好的边界以外的内存空间。根据复制算法,内存对半分,只有一半空间工作,可能会存在频繁复制的操作。
概念理解如下图:
分带收集:将堆空间分为新生代和老年代,老年代特点:对象存活率高,不怎么被GC,采用的是标记—整理算法。新生代对象存活率低,每次GC的时候存活下来的对象较少,采用的是复制算法
首先介绍一下堆区的内存结构:
首先介绍一下新生代的GC流程:
- 当对象因为内存原因无法放入到Eden区中,会发生一次Minor GC,GC后可能会存活下来一部分对象,把这部分对象放入到From幸存者区中,清空Eden区。
- 下一次对象因为内存原因无法放入到Eden区中,会发生一次Minor GC,GC后仍然可能会存活下来一部分对象,这时把这部分对象放入到To幸存者区中,并且将From区仍然存活下来的对象年龄加1移动到To区中,清空Eden区,From存活的移动了,既此时也为空,作为下一次要移动存活对象的内存空间。
- 下一次对象因为内存原因无法放入到Eden区中,会发生一次Minor GC,GC后仍然可能会存活下来一部分对象,这时把这部分对象存入From幸存者区中,并且将To区仍然存活下来的对象年龄加1移动到To区中,清空Eden区,To存活的移动了,既此时也为空,作为下一次要移动存活对象的内存空间。
综上所述可以总结规律:Eden是存放新生对象的区域,From和To区是每一次GC后存放存活对象的区域。每当Eden区内存不够存储即将要存储的对象时,都会出发一次Minor GC,首先会将幸存者From区作为移动存活对象的内存空间,接下里每一次满足GC条件,并且GC之后都会将Eden区存活下来的对象放入到另一个空闲的幸存区中,并且也会把当前非空闲的幸存区中的对象年龄+1放入到空闲的幸存区中。就这样每一次GC,都是将Eden区和非空闲的幸存区中的对象放入到空闲的幸存区中。不同的是非空闲的幸存区中的对象是年龄+1的。因为JVM中配置,当某个对象年龄在幸存区中达到了15的时候会晋升到老年代中存储。
下面再结合以上的文字描述来理解流程图:
什么时候老年代会发生GC?
- 当Minor中的对象年龄到达了配置晋升老年代的标准时,默认是15,老年代的剩余空间无法满足该对象存储
- Minor GC 之前,JVM进行分配空间担保,如果老年代的连续空间小于新生对象,则触发
- System.gc()
通常FullGC 会伴随至少一次的 MinorGC,但不是绝对的。
老年代的GC一般采用标记—清除 | 标记—整理
堆区GC大体流程如下图:
上图是我对堆区GC的大体流程的了解,哪里有疏忽的不吝赐教。以下通过文字针对上图进行步骤详解
-
首先当Eden区new满后,需要触发一次MinorGC
-
在Minor GC之前会进入JVM的分配担保机制
-
检测老年代的连续空间是否大于所有的新生对象空间总和
3.1: 如果大于则判断幸存区内存空间是否满足某对象存储,
3.1.1 :满足的话则直接存储,并且在幸存者区移动到另一个幸存者区的时候判断年龄加1的对象最终是否满足晋升老年代的标准
3.1.1.1:如果满足晋升老年代的标准则判断老年代的连续空间是否能存储此次晋升的对象
3.1.1.1.1:满足直接存储
3.1.1.1.2:不满足触发Full GC ,如果Full GC 之后老年代仍无足够连续空间存储,则抛出OOM异常。如果Full GC 之后老年代仍有足够连续空间存储,则直接存储
3.1.2:不满足的话则判断老年代的连续空间是否能存储此次晋升的对象
3.1.2.1:满足直接存储
3.1.2.2:不满足触发Full GC ,如果Full GC 之后老年代仍无足够连续空间存储,则抛出OOM异常。如果Full GC 之后老年代仍有足够连续空间存储,则直接存储3.2:如果小于则判断HandlePromotionFailure=true 或者 false
3.2.1:如果是true:检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
3.2.1.1:小于的话,直接Full GC,如果Full GC 之后老年代仍无足够连续空间存储,则抛出OOM异常。
3.2.1.2:大于的话,低风险的Minor GC
3.2.2:如果是false:直接Full GC,如果Full GC 之后老年代仍无足够连续空间存储,则抛出OOM异常。
JVM垃圾回收器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:CMS、Serial Old、Parallel Old
- 整堆收集器: G1
几个相关概念:
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
-
Serial 收集器:新生代的单线程收集器,复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程暂停,收集完之后用户线程继续。使用单核服务器。
-XX:+UserSerialGC 来选择 Serial 作为新生代收集器。 -
ParNew 收集器:Serial 的多线程版本,默认开启的收集线程数和 CPU 数量一致.
-XX:ParallelGCThreads 来设置垃圾收集的线程数 -
Parallel Scavenge 收集器:新生代的多线程收集器,能做到一个可控制的吞吐量。注重吞吐量,高效利用 CPU,需要高效运算且不需要太多交互
-XX:MaxGCPauseMillis 设置收集器尽可能在多长时间内完成内存回收
-XX:GCTimeRatio 精确控制吞吐量
-XX:+UseParallelGC :选择 Parallel Scavenge 作为新生代收集器,jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器。 -
Serial Old 收集器:老年代单线程收集器,适用单核服务器,作为 CMS 收集器的后备预案
-
CMS(Concurrent Mark Sweep) 收集器:最短用户线程停顿时间为核心思想,垃圾收集过程分以下4步:
5.1.1 初始标记:标记一下 GC Roots 能直接关联到的对象,速度较快
5.1.2 并发标记:进行 GC Roots Tracing,标记出全部的垃圾对象,耗时较长。
5.1.3 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短。
5.1.4 并发清除:用标记-清除算法清除垃圾对象,耗时较长。整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS 收集器垃圾收集可以看做是和用户线程并发执行的。
缺点:
随着CPU数量下降,占用CPU资源越多,吞吐量越小。
无法处理与本次清理工作并行的用户线程所产生的新垃圾,因为在与用户线程并发工作,用户线程会不断产生垃圾,CMS无法清理本次与他并行的用户线程所产生的垃圾。
垃圾收集阶段用户线程并发执行:此时CMS不能像其他垃圾回收器等老年代被填满时GC,他是立即收集老年代,不管满没满。所以需要预留出一部分空间提供用户线程运行并且使用。所以当CMS正在工作时,预留出的那部分内存空间不够用了,CMS就会报出Concurrent Mode Failure的错误,这是会启动CMS的后备预案 Serial Old临时的对老年代的垃圾重新收集
CMS 是基于标记-清除算法,垃圾回收会产生内存碎片, -XX:UserCMSCompactAtFullCollection 开启碎片整理(默认开启),CMS 进行 Full GC 之前,会进行内存碎片的整理。可以用 -XX:CMSFullGCsBeforeCompaction 设置执行多少次不进行碎片整理的 Full GC 之后,之后再来一次带碎片整理的 Full GC。
适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用 -XX:+UserConMarkSweepGC 来选择 CMS 作为老年代收集器
-
Parallel Old 收集器:Parallel Scavenge 的老年代版本,多线程收集器,采用标记-整理算法。充分利用多核 CPU 的计算能力。jdk7、jdk8 默认使用该收集器作为老年代收集器
-XX:+UseParallelOldGC 来指定使用 Paralle Old 收集器。
-
G1 收集器: jdk1.7正式引用,jdk1.9默认的收集器。垃圾收集范围是整个堆内存。化整为零 的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念,它们分别都是一部分 Region
上图中的每一圆圈都代表一个区域,或Eden,或SUR,或老年代区。每种类型的区域数量也不一定,每个区域大小必须是2的次幂,1-32M之间。最多可以设置2048个区域,对内存最大支持32M*2048=64G,假如设置-Xmx8g -Xms8g,则每个区域大小为 8g/2048=4M。
为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系,为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系
G1 收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小,这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率
回收器分一下几步:
-
初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
-
并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
-
最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
-
筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。
-
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器
收集器参数:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:+UseG1GC:启用G1垃圾回收器
针对垃圾回收器这部分内容,我是参考/88934939该地址记录,更加详细的内容请点击该地址查看
JVM调优参数: