目录
前言
-
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要回收的垃圾
-
为什么要有GC:如果不进行垃圾回收,内存迟早会被消耗完;为了整理出内存分配给新对象;没有GC就不能保证程序正常进行
-
内存泄漏:简单的说就是一个对象没用了,没有任何指针指向它,但是却无法回收;比如一个对象已经没有用了,但他还是被GC Roots直接或间接的引用,那么GC就无法对他进行回收了
-
内存溢出:简单的说就是对象撑满了内存,比如死循环,导致堆中有数不清的方法(栈帧)堆满了就内存溢出了
-
垃圾回收的并行与并发:在程序中并行是指同一时间点有多个线程在执行,并发是指同一时间段有多个线程在执行但同一时间点是有一个线程在执行;
-
而垃圾回收中的并行是指多条垃圾回收线程同时工作,此时用户线程处于STW阶段;串行是指只有一条垃圾回收线程在执行;并发是指在同一时刻用户线程与垃圾回收线程同时执行,用户线程不会停止,例如CMS和G1
-
垃圾回收相关算法
垃圾标记阶段的算法
-
这个阶段主要是去判断哪些对象是没有任何指针引用的垃圾
引用计数算法
-
对每个对象保存一个整形的引用计数属性,用于记录对象的引用情况
-
当对象被引用时计数器加一当引用失效时计数器减一,如果计数器时0则代表没有任何引用,可以回收
-
-
优点:实现简单,无延迟,判定效率高
-
缺点:增加了空间放计数器,增加了时间算加减;最重要的是无法判定循环引用
-
例如→A→B→C→A;这个时候引用计数器中a是2,b是1,c是1;当指向A的指针断掉时,A不再有用,那么BC也就没用,此时我们认为ABC都是无用的可以被回收,然而事实是,指针断掉后,引用计数器中A是1,B是1,C是1从而导致GC无法对其进行回收,导致内存泄漏,这也是为什么Java放弃引用计数法的原因;好像python在用,他们是只要弱引用就回收来规避这个弱点
-
可达性分析算法
-
也叫根搜索算法,追踪性垃圾收集,可达性分析算法可以很好的解决循环引用的问题
-
可达性分析算法是以根对象集合(GC Roots)为起点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
-
GC Roots包含栈中的引用的对象;本地方法栈中引用的对象;方法区静态属性引用的对象;方法区常量引用的对象。。。
-
还可以有一些临时的对象可以加入GC Root,例如将分代收集,局部回收(当只针对新生代时,老年代的对象也可以加入)
-
也就是说如果一个指针保存了堆内存里面的对象,而自己又不保存在堆内存中就是一个根对象了
-
-
内存中的对象被根对象集合直接或者简直连接着才是存活的,这个搜索路径就叫引用链
对象的finalization机制
-
当垃圾回收器发现有一个对象没有任何引用的时候:即在垃圾回收前都会调用finalize()方法
-
该方法可以被重写,一般用于在对象回收前对资源进行释放
-
不要主动调用这个方法,首先这个方法可能会导致对象复活;其次执行时间无法保证,因为只有GC的时候才会调用它,若不GC就永远不会执行;最后要是你写的垃圾会严重影响性能
-
-
由于有了finalize()方法,那么虚拟机中的对象就由三种状态
-
可触及的:从根节点对象是可达的
-
可复活的:对象所有的引用都被释放了,但是对象可能在finalize中被复活
-
不可触及的:对象的finalize被调用并且没有复活,就是不可触及状态;也就是finalize只能被调用一次
-
-
判断对象可回收的两次标记过程
-
先对对象进行可达性分析,如果没有任何引用了,对其进行第一次标记(准备回收)
-
进行筛选判断该对象也没有必要执行finalize方法;
-
如果对象没有重写finalize方法,或者已经执行过finalize方法了,那么就直接回收了
-
如果该对象重写了finalize方法,那么这是对象唯一的逃脱机会了,只要在该方法中于任何GCRoots直接间接的建立了链接,那么就可以移除准备回收的集合了,但之后如果又没有任何引用了,那么该对象会被直接回收,因为finalize方法只会执行一次
-
补充:finalize方法执行会被插入到一个F-Queue队列中,由虚拟机自己创建的优先级较低的Finalizer执行
也就是说第一次是标记它还有没有引用链,第二次标记到底执不执行finalize方法
-
垃圾清除阶段的算法
-
当成功的标记好哪些是可以回收的对象后,GC就要对这些对象进行回收,释放掉无用对象占用的空间,以便有空间分配给新的对象
标记-清除算法(Mark-Sweep)
-
当堆中内存被耗尽的时候,我们会进行垃圾回收,首先STW停止整个程序,然后进行标记和清除
-
标记:标记阶段是从根节点开始遍历,标记所有被引用的对象(也就是可达的那些非垃圾的对象),记录在对象头中
-
清除:清除阶段将堆中所有对象进行遍历,如果对象头(Header)中没有被标记,那么就是不可达对象,将其回收
-
这里的清除是指将对象的地址放入空闲列表中,如果有新对象来了直接覆盖掉,而不是直接就删掉了
-
-
缺点:效率不高;GC的时候要STW,用户体验不好;会产生内存碎片,要维护一个空闲列表
复制算法
-
核心:将存活的内存空间一分为二当要发生GC的时候将正在使用的内存中存活的对象放入另一份内存中,然后将之前正在使用的内存全部清空,将两个内存角色互换;例如新生代中的S1区和S0区的算法
-
优点:实现简单,运行高效;复制后的空间连续,不存在碎片化问题;新对象存储时用指针碰撞的方式就可以,不需要维护空闲列表
-
缺点:需要两倍空间;对于G1这种GC来说需要维护指针之间对象引用关系,内存占用和时间都是很大的消耗
-
最后:复制算法只有在存活的对象比较少的情况下,才会发挥到最大效果,不然的话万一都是存活对象,不仅没有清除垃圾还复制了一次很浪费;所以在新生代用最好老年代就不好用了
标记-压缩(整理)算法
-
由于标记清除算法会产生大量的碎片,如果老年代有大量的内存碎片,那么需要存储大对象的时候就会因为空间不够而导致OOM的问题;对于复制算法,老年代空间大,砍掉一半实在是得不偿失;所以有了标记压缩算法
-
在第一阶段和标记清除算法一样,先标记可达的对象,之后将标记出来的存活的对象压缩到内存的一端,最后清理别界外的所有对象
-
优点:消除了标记消除算法的内存碎片化问题;消除了复制算法内存减半的弊端
-
缺点:效率上比复制和标志清除都低;移到对象的时候,如果对象被其他对象引用,要修改的地址较多;移动过程中需要STW时间较长
三种算法比较
mark-sweep(标记-清除) | mark-Comp(标记-压缩) | Copying(复制) | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(产生大量内存碎片) | 少(无碎片) | 需要普通对象的两倍(无碎片) |
移动对象 | 否 | 是 | 是 |
分代收集算法
-
不同生命周期的对象采用不同的收集方式以变整体提高回收效率
-
虚拟机基于年轻代和老年代的特点进行不同的垃圾回收算法
-
年轻代:生命周期短,存活率低,回收频繁所以适合使用复制算法
-
老年代:生命周期长,存活率高,回收不频繁;所以不适合复制算法,一般都采用标记清除和标记整理混合的方式进行;例如CMS中先使用标记清除,当回收效果不理想的时候采用标记整理再来一下
-
增量收集算法
-
由于我们执行收集的时候都需要STW所以如果要收集的对象较多,就会导致STW的时间过长从而导致用户体验较差,那么我们可以将用户线程和垃圾收集线程交替进行;即:每次只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到收集完成
-
缺点:由于执行过程中不断的进行切换,而切换也是需要消耗的,所以使得垃圾回收的成本变高,造成系统吞吐量的下降
分区算法
-
把堆空间分成一块块的小区域,每个小区域独立使用,独立回收;以目标时间为准,进行回收,回收多少小块儿
关于垃圾回收相关概念
System.gc()
-
会显示的调用Full GC;但是无法保证对垃圾收集器的调用
-
提醒了jvm执行GC,但是不一定会执行
内存溢出和内存泄漏
内存溢出(OOM)
-
GC会进行各种年龄段的垃圾回收,如果还不够就Full GC操作来释放一下内存,如果还是不行就会报OOM了;一般来说OOM之前都会调用一下GC,也有特殊情况,比如来了一个超大的数组对象比堆内存还大 直接OOM
-
javadoc对内存溢出的解释是没有空闲的内存,并且垃圾收集器也无法提供更多的内存
-
原因:堆空间内存设置不够;代码中创建了大量大对象,并且长时间无法回收
内存泄漏
-
只有当对象不会再被程序使用了,并且GC还无法对其进行回收,这就叫做内存泄漏了
-
内存泄漏不会立刻引起程序崩溃,但如果内存泄露的越来越多,会最终导致OOM;但两者并没有什么必然关系
-
比如:各种链接我们需要close,而我们没有及时close就会导致内存泄漏
Stop The World
-
GC事件发生的过程中,会产生应用程序的停顿,停顿的时候应用程序全部暂停,没有任何响应就叫STW
-
由于我们可达性分析的时候需要保证分析过程中对象的引用关系不可以发生变化(否则无法保证分析结果的准确性),所以需要再分析的时候像是被冻结在了某个时刻
-
被中断的线程会在GC结束后恢复;所有的GC都会发生STW
-
当然不是随时都可以进行STW的,我们需要选取安全点和安全区域,只有在安全点的时候才能停止,而安全点不能天少否则GC等待时间太长,不能太多会影响用户线程性能;安全区域是指在一段代码片段中,对象引用不会发生变化,整个区域任何位置发生GC都是安全的(个人理解就是在线程执行的时候走到某个点就进入安全区域了,这个时候就可以进行GC了,如果中途醒了,就看你GC完成了没有,如果完成了就走出安全区域了,如果没有就等着,等GC完成)
引用类型
-
一共有四种引用,强软弱虚,强度依次递减;软弱虚都是Reference的子类
强引用
-
只要强引用关系还在,垃圾回收器就永远不会回收,即使OOM也不回收(所以强引用也是OOM的主要原因是之一)
-
99%情况下都是强引用,只要是new对象都是强引用(new了一个对象并将其复制给一个变量,这个变量就是强引用)
软引用
-
若发生内存不足就回收,哪怕软应用关系还在也会回收,如果回收了还不够就会OOM
-
使用 SoftReference 类来创建软引用。
弱引用
-
只能活到下一次回收之前,只要发现不管内存够不够都回收
-
WeakReference 类来实现
虚引用
-
一个对象有没有虚引用 对生命周期毫无影响,唯一目的就是在对象被回收前收到一个系统通知
-
对象回收跟踪;使用 PhantomReference 来实现虚引用,需要指定一个虚引用队列,在这个队列中可以看到是否被回收;使用get方法获取的时候是无法获取的(得到null)
关于垃圾回收器相关
-
吞吐量:用户线程的时间占总时间(用户线程+GC线程)的占比
-
暂停时间:STW的时间也就是一个时间段内用户线程暂停让GC来执行;这两个指标是衡量GC的重要指标
7种垃圾回收器
-
串行:Serial、Serial Old
-
并行:ParNew、Parallel Scavenge、Paraller Old
-
并发:G1、CMS
-
jdk8用的是parallel和parallelOld
Serial回收器
-
最基本的,串行回收器;针对新生代收集,采用复制算法串行回收
-
也提供了Serial Old老年代回收器,同样是串行回收
-
是单线程收集器,只会使用一个线程进行垃圾回收;优点就是简单高效;它是 Client 模式下的默认新生代收集器
ParNew回收器
-
是Serial的多线程版本,并行回收;除了多线程和Serial没什么区别
-
是 Server 模式下的虚拟机首选新生代收集器;老年代使用SerialOld因为老年代执行比较少或者CMS也行
-
默认开启与cpu相同数量的线程数
Parallel回收器
-
吞吐量优先,也是多线程的和ParNew的区别是它更注意吞吐量,想要达到一个可控制的吞吐量以及自适应调整策略
-
适合后台运行,不需要太多交互例如订单处理
-
老年代提供了Parallel Old收集器,采用标记-压缩并行回收
-
是Java8的默认收集器组合
CMS回收器
-
低延迟的垃圾回收器;因为只有初始标记和重新标记阶段需要STW,而这两个阶段耗时都比较低,并且其他阶段都是并发执行的不会发生STW,所以说它是低延迟的
-
第一款真正意义上的并发收集器,实现了用户线程和垃圾回收线程同时工作
-
尽可能减少STW时间;适合与用户的交互;使用的是标记清除算法;
-
初始标记:STW,只做一件事就是标记GC Roots可以直接关联到的对象,耗时很短
-
并发标记:从GC Roots直接关联的对象开始遍历整个对象图,耗时较长,不需要STW
-
重新标记:为了修正并发标记期间因为用户线程的执行而导致发生变化的对象;也需要STW比初始标记耗时长一点
-
并发清除:清理(标记-清除)掉标记阶段判断为死亡的对象,并发执行,不需要STW
-
标记清除会产生内存碎片;而我们不使用标记-压缩的原因也很简单,因为CMS在并发清除阶段用户线程还在执行,那么此时如果使用并发-压缩就会导致对象地址值的改变,程序就崩了
-
-
在CMS回收过程中不能等到老年代快满了才进行清除,因为它在收集的时候用户线程是没有停止的,所以如果内存不够了会报错,回收就失败了,所以应该当堆空间达到某个阈值(jdk6之后是92%就触发)后就进行回收
-
如果线程执行的速度远大于垃圾回收的速度,或者预留的空间不够而导致CMS回收失败了,那么就需要报错并且使用后背方案Serial Old来进行回收了
-
优点:并发收集,低延迟
-
缺点:会产生内存碎片,导致大对象放不下,需要提前触发Full GC;对CPU资源敏感由于并发阶段会占用部分线程导致吞吐量降低;无法处理浮动垃圾(如果在并发标记阶段如果产生新的垃圾对象,CMS无法对这些垃圾进行及时标记,最终会导致垃圾无法被回收,只能下一次再回收)
小结
-
最小化的内存开销和并行开销:Serial+Serial Old
-
最大化吞吐量:Parallel+Parallel Old
-
最小化STW时间低延迟:ParNew+CMS
G1回收器(Garbage-First)
-
区域化分代式;G1的目标:在可控的延迟(200ms)下尽可能高的提升吞吐量
-
G1是一个并行回收器,他将堆划分为若干区域(Region)来表示伊甸园,S1,S0,和老年代;会对各个区进行跟踪;在后台维护了一个优先列表,在可控的时间内优先回收价值大(简单的说就是回收后会释放大量空间价值就大,如果回收后发现都是可达对象几乎没释放空间就是价值小)的Region
-
面向服务端的回收器,主要针对多核CPU及大容量内存的机器,jdk9中的默认回收期,8中想用就使用 -XX: +UseG1GC来打开
优势
-
并行并发:并行,在回收阶段有多个线程同时工作,有效利用计算机多核的优势;并发,部分工作可以和应用程序同时进行,不会在整个阶段都处于STW的阶段
-
分代收集:他不在要求伊甸园区,幸存者区,老年代是连续的(物理),也不再固定大小和数量,因为可以动态的进行调节;可以工作在年轻代或者老年代
-
空间整合:各个Region之间是复制算法,而整体上G1是标记-压缩算法,这两种算法都可以有效的避免碎片化
-
可预测的停顿时间模型:由于分区的缘故,所以缩小了回收的范围,但对全局停顿做到了较好的把控;跟踪各个Region每次允许的时间内回收最大的价值的Region
缺点
-
需要消耗额外的内存来支撑G1的运行
-
在小内存的堆上性能不如CMS
参数设置
Region:化整为零
-
使用G1收集器,将整个Java堆划分成2048个大小相同的Region,每个Region的大小根据堆空间实际大小而定,整体可控制在1M-32M之间,是2的次幂(1,2,4,816,32);在整个jvm生命周期内都不会被改变
-
虽然依旧保留新生代老年代的概念,但物理上已经不是连续的了
-
一个Region只能是一个角色,但是当它被回收完后,下次一就不一定是什么了
-
专门有一个H区来放大对象,大对象的标准是他的大小是1.5个Region,如果一个H都放不下就找个连续的继续放,如果找不到就只能Full GC了
Remembered Set(记忆集)
如果在回收新生代的时候,有老年代的对象正在引用他,就会发生错误,而如果扫描整个老年代就会耗时
-
无论G1还是其他的收集器都是通过RSet来避免全局扫描
-
每一个Region都有一个对应的RSet来记录当有引用在其他Region的时候就记录在自己的RSet中,这样在回收本Region的时候就把RSet也加上既不会漏了也不会全局扫描了
回收过程
-
Young GC:当Eden区用尽的时候进行Young GC,是并行的独占式的(STW然后多线程同时回收)
-
扫描根;更新RSet;处理RSet(old指向eden的就是存活的);复制对象(新生代的复制算法);处理引用(强软弱虚)
-
-
老年代并发标记过程:当堆内存使用率达到45%并发标记老年代
-
初始标记(直接可达对象);根区域扫描(扫描幸存者区直接可达的老年代的对象,并且标记被引用对象);并发标记(如果该区域全部是垃圾就立刻回收);再次标记;独占清理(计算谁的价值高,不会回收);并发清理(清理空闲区域)
-
-
混合回收(Mixed GC):上一步标记完成后会马上进行混合回收,G1会从老年代将存活对象移入空闲Region,这些空闲的Region就变成了老年代的一部分;但不需要回收整个老年代,只需要回收一部分价值高的就可以了,同时年轻代的Region也会被回收
-
为了防止越来越多的对象进入老年代导致堆被占满,于是会触发混合回收,回收全部新生代和部分老年代(并不是FullGC)
-
最后
才学疏浅,现在直到这个程度了,以后有时间再深入学习把