本文参照尚硅谷-宋红康老师的视屏进行整理,仅供大家学习使用,如果侵权,请联系
上篇:https://blog.csdn.net/u010323860/article/details/107782859
1、垃圾回收相关简单介绍
(1)什么是垃圾?在运行程序当中没有任何指针指向的对象。
(2)为什么需要GC?如果不进行垃圾回收,内存迟早会消耗完。
(3)JAVA有自动内存管理机制,也就是自动分配内存和回收,降低了内存泄漏和内存溢出的风险。
(4)System.gc(),显示调用,会触发Full Gc,该方法也无法保证一定会调用垃圾回收器进行回收
(5)内存泄漏和内存溢出:
内存溢出就是OOM;为新对象分配内存时,没有空闲内存,并且垃圾回收器也无法提供更多内存
内存泄漏:对象不再被程序用到了,但是垃圾回收器又回收不了{HotSpot虚拟机中可能存在的情况就是对象不使用了,但 是引用忘记断开了,也就是GCRoots还是可达的,但是确实不使用了;比如单例模式,它的声明周期跟应用 程序一样长,如果单例模式关联了某个对象,就可能造成内存泄漏;一些外部资源,如IO没有close}。
(6)并行是一个时间点上多个程序在执行(只有多个CPU、或者一个CPU多核才可能发生并行);
并发是一段时间内多个程序在执行,一个时间点一定只有一个程序在执行【只不过CPU切换效率比较高】
在垃圾回收器中:并行(parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(可能并行;也不一定是并行的,可能会交替 进行),用户程序在继续镜像,而垃圾收集程序运行于另一个CPU上。
(7)强引用、软引用、弱引用、虚引用
强:如果这个对象是可达的,即使包OOM,这类对象也不会被回收{程序中百分之九十都是强引用 Object a = new Object()}
软:java.lang.ref.SoftReference表示,当内存不足时会回收这类对象{即使可达}
弱:java.lang.ref.WeakReference表示,当JVM垃圾回收时,无论内存是否充足,此类对象都被回收
虚:java.lang.ref.PhantomReference表示,在任何时候都可能被垃圾回收器回收。
2、垃圾回收相关算法
2.1 标记阶段{标记对象是垃圾的过程}
(1) 引用计数法【JAVA中没有使用这个算法】
原理:对每个对象都保存一个整型的引用计数器属性,用于记录对象被引用的情况。引用加1,失效减1,当为0的时 候回收
优点:实现简单,判定效率高
缺点:无法处理循环引用的问题
Python中使用了这个算法,它是通过手动解除{在合适的时候解除引用关系}、弱引用{使用weakref}的方式解决循环引 用的问题
(2) 可达性分析算法
原理:可达性分析算法就是以根对象(GC Roots)集合为起始点,按照从上到下的方式搜索根对象到所连接的对象是否 可达
可作为GC Roots的:
(1)虚拟机栈中引用的对象{各个线程被调用的方法中使用到的参数、局部变量}
(2)本地方法栈内的对象{Native方法}
(3)类的静态属性引用的对象、字符串常量池里的引用[方法区JDK1.7之前]
(4)同步锁的对象【以上四种记忆的小技巧就是堆周边的运行时数据区都可能作为GC Roots】
(5)还有一种特殊情况,就是当只对新生代进行垃圾回收时,老年代中还可能存在对新生代的引用,老年代也可做 为GC Roots【也就是分代收集】
对象的finalization机制:
调用时机:当垃圾回收器发现一个对象没有被引用时,总会先调用对象的finalize方法(对象存活一生中只调一次)
finalize()是object类中的方法,允许子类覆写,尽量不要主动的去掉该方法
由于该方法的存在,对象可能存在三种状态
(1)可触及的:也就是GC Roots可达,是被引用的
(2)可复活的:就是第一次标记GC Roots不可达,但是调用finalize()时复活了
(3)不可触及的:不可能在被引用了,不可能被复活,一定会被回收
判断对象是否可回收过程:
(1)如果该对象到GC Roots没有引用链,进行第一次标记
(2)判断对象是否有必要执行finalize方法(子类覆写了才执行,因为父类中是空的),如果没有必要执行,则直接 标记该对象不可触及;如果需要执行finalize方法,则该对象会插入到F-Queue队列中,等待低优先级线程 Finalizer去触发finalize方法,如果finalize方法中该对象到GC Roots又有引用链了,则该对象变为可触及,否 则变为不可触及
JProfiler工具
2.2 清除阶段
(1) 标记-清除算法(Mark-Sweep)
过程:会停止整个程序Stop The Word,然后进行两项工作标记-清除;标记是从GC Roots开始遍历,标记所有引 用的对象,也就是可达对象,然后将这个标志位放在对象的Header中;清除是对堆内存从头到尾进行线性 遍历所有对象,如果某个对象的Header中没有被标记为可达对象,那么就将它回收
优点:基础、常见
缺点:效率不算高(两次堆内存对象全遍历),进行GC的时候需要停止整个应用程序,用户体验差,容易产生内存碎 片,内存空间不连续{需要维护一个空闲列表}
(2) 复制算法
过程:将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用 的的内存块中,之后清除正在使用的内存块中的所有对象,然后交换两个内存的角色 {survivor0、survivor1}
优点:没有标记-清除过程,实现简单,运行高效;不会出现内存碎片,内存是连续的{再次分配内存用的指针碰撞}
缺点:每次只有一半的内存空间可用;对象迁移后,其它地方的引用还需要变换内存地址,STW(Stop the word)
注意:此算法只适用垃圾对象比较少,而且朝生夕死的那种,适用新生代。【如果对象多,存活时间长,相当于都 是可达的,平白无故的复制了迁移了一次】
(3) 标记-压缩算法{标记-整理}(Mark-Compact)
过程:标记是从GC Roots开始遍历,标记所有引用的对象,也就是可达对象,然后将这个标志位放在对象的 Header中;第二步是将所有存活对象压缩到内存的一端,按顺序排放,清除边界外所有空间
优点:整理了内存碎片,内存更连续(指针碰撞分配新内存);内存也不用减半了
缺点:效率上要低于复制算法;移动对象了,需要该引用地址;STW(Stop the word)
(4) 以上三种算法比对
没有最优的算法,只有最合适的算法。所以有分代收集算法,也就是不同的生命周期的对象可以采用不同的收集算 法,也就是不同的代(新时代、老年代)使用不同的算法【新生代-复制算法;老年代-标记清除或者标记整理】
(5) 增量收集算法
为了解决STW(Stop the word)的时间,体验差的问题,该算法是让垃圾线程和用户线程交替执行,每次垃圾线程只 收集一小片区域的内存空间,就切换到用户线程,依次反复
缺点:来回切换线程,造成额外的消耗,回收垃圾的总成本上升,系统吞吐量下降
(6) 分区算法
为了解决STW(Stop the word)的时间,体验差的问题,该算法是将一块大的内存区域分割成多个小块{region},根 据目标停顿时间,每次合理的回收若干个小块区间
3、垃圾回收器
3.1 性能指标
吞吐量:运行用户代码所占的时间占总运行时间{用户代码执行时间+垃圾收集时间}的比例
暂停时间:执行垃圾收集时,程序的工作线程被占用的时间{这个指标日益重要}
内存占用:Java堆内存的大小
现在GC的基本标准:在最大吞吐量优先的情况下,降低暂停时间。
3.2 GC回收器
分类:新生代-----Serial、ParNew、Parallel Scavenge
老年代-----Serial Old、Parallel Old、CMS
整堆回收器:G1
查看参数:-XX:PrintCommandLineFlags[+代表使用,-代表不使用]
Serial回收器(串行回收):
原理:采用复制算法+串行回收+STW【新生代用这个算法, 默认老年代用Serial Old,这两个关联】
优点:单线程下简单而高效【限定单个CPU,单核,没有线程间切换】
关联回收器:Serial Old回收器:与Serial回收器类似,不过这个是老年代的回收器,采用标记-整理算法。【不能使 用参数设置】
参数:-XX:UseSerialGC 指定使用Serial回收器,表明新生代用Serial回收器,老年代用Serial Old回收器
收集过程图示:
ParNew回收器(并行回收,多CPU多核)【除了是多线程进行收集外,与Serial并无差别】
这个回收器在JDK9以后就不建议使用了
原理:采用复制算法+并行回收+STW【新生代用这个算法, JDK9以前默认老年代用Serial Old,也可以使用CMS; JDK14以后CMS回收器被移除了,所以这个回收器基本不用了在最新的JDK上】
优点:多核下新生代的收集会更快速,多个线程并行收集,STW时间短了
关联回收器:Serial Old回收器、CMS收集
参数:-XX:UseParNewGC 指定新生代使用ParNew回收器;-XX:ParallelGCThreads指定并行线程数{一般小于等于 CPU总核数}
收集过程图示:
Parallel回收器:吞吐量优先,并行,采用复制算法+并行+STW【JDK8的默认回收器】
原理:高效利用CPU时间,快速完成程序,老年代会默认采用Parallel Old回收器
关联回收器: Parallel Old回收器,采用并行+标记-压缩+STW+高吞吐量
参数:-XX:UseParallelGC,代表新生代是Paralel;
-XX:UseParallelOldGC 老年代采用Parallel Old;【二者可以相互激活】
-XX:ParallelGCThreads指定并行线程数{一般小于等于CPU总核数}
收集过程图示:
CMS回收器{低延迟}Concurrent Mark Sweep,老年代回收器【JDK9标记弃用,JDK14之后该回收器已经被干掉了】
是HotSpot虚拟机中第一个真正意义上的并发收集器,垃圾线程、用户线程可以同时工作
原理:初始标记-会出现STW,但时间比较短,主要任务是只标记出GC Roots能直接关联到的对象
并发标记-从GC Roots的直接关联对象{初始标记的那些对象}开始遍历整个对象图的过程,耗时较长但不需 要暂停用户线程
重新标记-由于在并发标记阶段,用户线程也在运行,所以这个阶段是修改并发标记期间,因用户程序继续 运作而导致标记产生变动的那一部分对象的标记记录{当然这些对象还是第一步那些对象,不会对 新产生的对象进行标记}STW
并发清除-清除删除掉标记阶段判断的已经死亡的对象,释放内存空间{并发,用户线程和清除线程同时运 行,因为是标记清除算法,不需要移动对象}
优点:并发收集,并发清除,低延迟。
缺点:会产生内存碎片;对CPU资源敏感,并发阶段,会抢占CPU,造成吞吐量降低;无法处理浮动垃圾,浮动 垃圾就是在并发标记阶段,用户线程所产生的新垃圾;{在并发标记阶段,如果内存不够用的话,会报一次 Concurrent Mode Failure,然后开启备用收集器Serial Old收集器开始单线程收集老年代};
参数:-XX:UseConcMarkSweepGC,手动指定使用CMS垃圾回收器,会自动加UsePaNewGC打开
-XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率,也就是阈值,到了这个值就开始触发CMS,因为 在CMS存在并发标记阶段,不可能等内存要满了才回收,否则在并发标记阶段可能就会内存溢出,所 以设置这个值{JDK6以后的版本默认是92%}
-XX:UseCMSCompactAtFullCollection 指定在执行完Full GC后对内存进行压缩整理{STW}
-XX:CMSFullGCsBeforeCompaction 指定多少次Full GC后对内存进行压缩整理
收集过程图示:
G1回收器:区域分代化,region, 在延迟可控的情况下获得尽可能高的吞吐量。【JDK9以后默认的回收器】
侧重点在于优先回收垃圾最大量的区间,所以称为Gaarbage First。
Region:每个region内部是通过指针碰撞进行内存分配的,通过TLAB进行控制。
RememberedSet:一个region中的对象可能被其他任意region中对象引用,判断对象是否存活时,是否需要扫描 整个堆的region一个个判断是否引用该对象吗?
每个region都有一个对应的RememberedSet,这里记录了引用这个区域的其他region{除它自己}
原理:当年轻代的Eden区用尽时开始年轻代的回收过程;当堆内存使用达到一定值(默认是45%),开始老年代的 标记过程,标记完成后开始混合回收过程,G1的老年代回收器是一次只需扫描/回收一部分老年代的region
初始标记-这个阶段是STW的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。
并发标记-从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。若发现某个region 中的所有对象都是垃圾,那个这个区域会被立即回收。
最终标记-标记那些在并发标记阶段发生变化的对象,将被回收
筛选回收-首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回 收一部分Region{STW}
优点:并行与并发;分代收集,它将堆分成若干个eden、survivor、old区,兼顾整个堆的垃圾收集;空间整合, G1将堆分成一个一个的region,region之间采用赋值算法,整体上采用的是标记-整理算法,无碎片化;可 预测的停顿时间,可以指定STW时间,G1也会尽可能的满足在收集垃圾上停顿的时间不大于指定的值,是 因为G1可以只收集部分region,每次根据允许的收集时间,优先回收垃圾占比高的region区域;
缺点: 会多占用内存{rset}【内存大于8G时G1的效率比较高】
参数:-XX:+UseG1GC,使用G1回收器,JDK9之后是默认。
-XX:+G1HeapRegionSize,设置每个Region的大小,值是2的次幂,范围是1-32MB。
-XX:+MaxGCPauseMillis,设置最大的GC停顿指标,默认是200ms【并不是越小越好】
3.3 总结