一、GC的基础知识
介绍见垃圾收集器之前,先介绍一下基础知识
1、JAVA VS C++
- java
- GC处理垃圾
- 开发效率高,执行效率低
- C++
- 手工处理垃圾
- 忘记回收—内存泄漏
- 回收多次—非法访问
- 开发效率低,执行效率高
2、定位垃圾(如何确认对象的死活)
1.引用计数法(REferenc Counting),简称RC
-
如何判断:在对象中添加一个引用计数器,有一个地方引用它,计数器+1;当引用失效时,计数器值就-1,任何时刻计数器位0的对象就是不可被使用的。
-
缺点:不能解决对象直接互相循环引用
2.根可达算法(Root Searching )
-
从GC Roots节点(起始点)出发向下搜索,如果没有任何引用链相连(即GC Roots到对象不可达),则证明此对象不可用。
-
什么是引用链?
- 从起始点向下搜索所走过的路径被称为引用链(Reference Chain)。
- 对象Object5、Object6和Object7之间虽然彼此还有联系,但是它们到GC Roots是不可达,所以被判定为可回收对象。可以当做GC roots的对象有以下几种:
- 虚拟机栈中的引用对象(栈帧中局部变量表中引用的对象)
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象(声明为final的常量值)
- 本地方法栈中JNI的引用对象(Navite方法)
- java虚拟机内部引用
- 所有被同步锁(synchronized关键字)持有的对象
-
缺点:
- 耗时
- GC停顿(初始标记和重新标记阶段)
3.引用
-
判断对象是否存活离不开引用,JDK1.2之后对引用进行扩充。
-
4种引用强度依次减弱
-
强引用
-引用赋值,比如Object ob=new Object();
-
软引用
-描述还有用,但非必须的引用。在内存溢出前,会进行二次回收,若内存还不足,就会回收该对象。
-
弱引用
-当垃圾收集开启,无论当前内存是否足够,都会回收被弱引用管关联的对象。
-
虚引用
-无法通过该引用来实例化对象。没什么意义。
-
3、常见的垃圾回收算法
- 找到这个垃圾之后怎么进行清除的算法,GC常用的算法有三种
1.标记-清除算法(Mark-Sweep())
- 首先标记出所需要回收的对象,在标记完成之后,回收所有被标记的对象
- 标记的是可回收的对象,也就是说死亡的对象
-
优点:算法简单,不需要挪动对象
-
缺点:会产生碎片,效率低,不稳定
-
适合场景:适合存活对象比较多的情况(老年代)
2.标记-复制算法(Copying)
- 标记-复制算法是一种“半区复制算法”,将内存按照容量划分为大小相等的两块,每次只使用其中一块,当这块没存用完了,就将存活者的对象复制到另外一块上,然后把已使用过的那块空间清除。
- 优点:没有碎片,效率高
- 缺点:空间消耗大,对象需要移动和复制,需要调整对象引用
- 适合场景:存活对象比较少的情况(新生代)
3.Mark-Compact(标记-整理算法)
- 与 标记-清除算法 区别就是 标记-整理算法 会移动对象
- 优点:不会产生碎片,方便对象的内存分配,不会产生内存减半
- 缺点:,移动对象(造成停顿),效率偏低
- 应用场景:老年代
4.分代收集算法
5.总结:
使用标记-清除算法会影响对象内存分配效率,因为空间碎片太多,当有大对象时,会迫使第二次垃圾回收;
使用标记-复制算法会移动对象,移动对象过程中会迫使所有用户程序停止,也叫做“停顿”;
按常理说,移动对象会比较快,也就是比较划算,但是呢,内存分配频率要比对象移动高得多,所以最后还是选择内存分配的方式,即标记-清除算法,这也是关注延迟的CMS收集器使用标记-清除算法的原因
4、堆内存逻辑分区
- (只是用于分代垃圾收集器)
1.部分垃圾回收器使用的模型
除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型
G1是逻辑分代,物理不分代
除此之外不仅逻辑分代,而且物理分代
2.对象分配过程
对象在内存分配方式
1.指针碰撞
2.空闲列表
- 新生代(young Generation:刚new出来的对象往新生代扔,大量死去,少量存活,采用复制算法
- 老年代(Old Generation):垃圾回收很多次都没回收掉,存活率高,采用MC(标记压缩)或者MS(标记清除)
- eden:正真存放刚new出来的对象,在新生代(年轻代)中
- survivor:eden回收一次后没被回收,就会往该地方跑,会在两个suevivor之间来回跑,年龄够了就往老年代跑(默认15次)
3.新生代 + 老年代 + 永久代(JDK1.7)Perm Generation/ 元数据区(1.8) Metaspace
- 永久代(1.7) 元数据(1.8) - Class
- 永久代必须指定大小限制 ,元数据可以设置,也可以不设置,无上限(受限于物理内存)
- 字符串常量 1.7 - 永久代,1.8 - 堆
- MethodArea逻辑概念 - 永久代、元数据
4.新生代 =8 Eden + 1From suvivor区 +1To suvivor区
- YGC(对新生代进行垃圾回收)回收之后,大多数的对象会被回收,活着的进入s0
- 再次YGC,活着的对象eden + so> s1
- 再次YGC,eden +s1 -> s0
- 年龄足够 -> 老年代 (15 CMS 6)或 s区装不下 -> 老年代
5.老年代
- 很难消亡
- 老年代满了FGC (Full GC):收集整个堆和方法区元数据区域的垃圾(方法区中元数据区域在FullGC中会被回收,而1.7的永久代是不会的)
6.GC Tuning (调优)
- 尽量减少FGC
- 方法区空间增大
- 老年代空间增大
- 新生代空间减小
- 禁止使用System.gc()方法(或者少使用)
- 使用标记-整理算法,尽量让连续空间保持最大
- 排查代码中的无用大对象(内存泄漏)
- Minor/YoungGC = 简称YGC
- MajorGC =简称 FGC
7.动态年龄
- 动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小 。
8.对象分配过程
9.关于JVM命令行的一些指令
1.命令行上执行如下命令,查看所有默认的jvm参数
java -XX:+PrintFlagsFinal -version2.新生代Eden/Survivor空间的初始比例
-XX:InitialSurvivorRatio
3.Old区 和 Yong区 的内存比例
-XX:Newratio
4.配置年龄限定值进入老年代
-XX:MaxTenuringThreshod
5、常见的垃圾收集器
0.在了解垃圾收集器之前,先了解并行与并发
- 并行(Parallel):描述多条垃圾收集器线程之间的关系,说明同一时间,由多条同样的线程在协同工作
- 并发(Concurrent):描述的是垃圾收集器线程与用户线程之间的关系,说名同一时间,垃圾收集器线程和用户线程在同时运行
- JDK诞生 Serial追随 提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前任何一个JDK版本默认是CMS。并发垃圾回收是因为无法忍受STW
1.Serial收集器
图中说到的标记压缩算法就是标记整理算法
- Serial收集器是最基础、历史最悠久的收集器,作用于新生代,是一个单线程工作的收集器,进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。,基于复制算法
- 理解:
-你妈妈在给你打扫房间,肯定会让你老老实实呆着,不能让你扔垃圾,不然怎么扫都扫不完 - 是Hotspot运行虚拟机就运行在客户端模式下默认新生到收集器。
2.ParNew收集器
- ParNew实质上是Serial收集器的多线程并行版,除了同时使用多线程收集垃圾外,其余于Serial完全一致。
- 作用于新生代,是一个单线程工作的收集器,进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。
- 理解:
- 你妈妈和姥姥一起在给你打扫房间,肯定会让你老老实实呆着,不能让你扔垃圾,不然怎么扫都扫不完
3.Parallel Scavenge收集器
-
也是新生代垃圾收集器,同样基于复制算实现,也是一款能够并行收集的多线程收集器,但是Parallel Scavenge收集器目标是达到一个可控制的吞吐量(Throughput)。
吞 吐 量 = 运 行 用 户 代 时 间 / ( 运 行 用 户 代 码 时 间 + 运 行 垃 圾 收 集 器 时 间 ) 吞吐量=运行用户代时间/(运行用户代码时间+运行垃圾收集器时间) 吞吐量=运行用户代时间/(运行用户代码时间+运行垃圾收集器时间)
2.Paraller Scavenge 收集器提供两个参数用于精确空指吞吐量- 控制最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
XX:MaxGCPauseMillis参数允许的一个大于0的毫秒数,收集器会尽力保证内存回收花费时间不超过用户限定时间,但是,垃圾收集停顿时间的缩短是以牺牲吞吐量和新生代空间为代价
- 设置吞吐量大小:-XX:GCTimeRatio:
XX:GCTimeRatio:参数值应是一个大于0小于100的正数, 表示希望在GC花费不超过应用程序执行时间的1/(1+n) ,如果参数为n,则 此参数的值表示运行用户代码时间是GC运行时间的n倍
- 控制最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
4.Serial Old收集器
- Serial Old是Serial老年代的版本,同样是一个单线程收集器,采用标记—整理算法,在进行垃圾收集时,会暂所有用户线程(停顿)。
5.parallel Old收集器
- Parallel Old时Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记—压缩算法实现,这个收集器直到JDK6才被开始提供。
- 在注重吞吐量的时候,可以考虑优先使用Parallel Scavenge和Parallel Old组合。
6.CMS收集器(并发标–记清除)
-
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。基于标记-回收算法实现的。总体上来说,CMS收集器的内存回收过程是与用户线程并发执行的。用于老年代
- 为什么CMS是基于标记清除:因为标记—清除算法和复制算法的区别就是标记—清除算法产生空间碎片,会影响内存分配。复制算法需要移动对象,会暂停所有用户线程(停顿)影响内存收集,并且内存分配频率回避内存收集高,CMS以获得最短回收停顿时间为目标的收集器,最终CMS选择标记—清除算法。
- 由于CMS时基于标记—清除算法,老年代会产生空间碎片,单大对象进入时,如果没有内存可以分配,也就是说老年代满了,那么就会迫使Serial Old收集器使用标记—压缩算法进行垃圾回收
-
垃圾收集过程分为4个过程
-
初始化标记(停顿)——单线程
- 只标记一下GC Roots能直接关联的对象,速度很快
- 只标记一下GC Roots能直接关联的对象,速度很快
-
并发标记——多线程
- GC Roots直接关联的对象开始,遍历真个对象图的过程,并发执行,不会产生停顿
-
重新标记(停顿)
- 修正并发标记期间由于用户线程运作导致标记产生变动的那一部分对象的标记
-
并发清除
- 清除标记阶段判断已经死亡的对象,由于不需要移动存活的对象,因此可以并发执行
-
问题:CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收
-
CMS的问题
-
Memory Fragmentation(内存碎片),CMS基于标记—清除算法,会产生空间碎片
-XX:+UseCMSCompactAtFullCollection:
-XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩 -
Floating Garbage(浮动垃圾),并发会产生浮动垃圾
并发清理阶段,由于用户线程仍在运行,在此期间用户线程制造的垃圾就被称为“浮动垃圾”,浮动垃圾本次GC无法清理,只能留到下次GC时再清理。
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。 CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。 如果CMS预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。
解决方案:降低触发CMS的阈值
PromotionFailed(老年代满)
解决方案类似,保持老年代有足够的空间
–XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,老年代达到这个之后会触发full GC让CMS保持老年代足够的空间。
-
-
7.Garbage Fiest(G1)收集器
- 算法:三色标记 + SATB
-
在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区(Region),每个Region可以根据需要扮演新生代的Eden空间、Survivor空间或老年代空间,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。
-
特点
- 并行与并发: G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 :与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 :G1从整体来看是基于标记-压缩算法实现的收集器,从局部(两个Region之间)上来看是基于标记—复制 算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿: 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
-
可预测的时间模型
- G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
-
如果不计算用户线程运行过程中的动作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking) ——停顿-速度快
- 标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,速度很快
- 并发标记(Concurrent Marking) ——浪费总时间的80%
- GC Roots直接关联的对象开始,遍历真个对象图的过程,并发执行,不会产生停顿
- 最终标记(Final Marking)——停顿,并发执行
- 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation)
- 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。