GC垃圾判定
GC判定
对象什么时候会被垃圾回收器回收?
如果一个或多个对象没有任何引用指向它,那么这个对象就是垃圾,可以被垃圾回收器回收。
引用计数法
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数法缺点:
-
很难处理循环引用,相互引用的两个对象则无法释放,导致内存泄漏。
因此目前主流的Java虚拟机都摒弃掉了这种算法。
可达性分析算法
现在的java虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
可达性分析:解决了循环引用的问题,防止内存泄漏的发生。
这个算法的实质在于将一系列GC Roots作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该合集引用到的对象,并将其加入到该合集中,这个过程称之为标记。 最终,未被探索到的对象便是死亡的,是可以回收的。
只要你是存活的对象,你都应该直接或者间接的被GC Roots所连接,没连接到的就是垃圾。
GC Roots
在java语言中,GC Roots可以是哪些具体的元素呢?
虚拟机栈(栈帧中的本地变量表)中的引用对象。(最为常见)
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(Native修饰的方法)的引用对象
所有被同步锁synchronized持有的对象
技巧:
所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root(由于Root采用栈方式存放变量和指针)
对象的finalize机制
对象在回收之前,涉及到一个方法finalize()的调用
该方法是Object类的方法,可以去重写,通常目的是在对象不可撤销的丢弃之前执行清理操作
public class Object { protected void finalize() throws Throwable { } }
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
- 一个糟糕的finalize()会严重影响GC的性能。
虚拟机中对象一般处于3种可能的状态
- 可触及:人家就不是垃圾
- 可复活:可能在finalize()中复活
- 不可触及:真的该死了。。
注意:finalize()只会被调用一次
2次标记
没重写是不可能自救的。。
由优先级低的线程帮我们调用这个方法
跟现有引用链搭上关系了,就是可复活的了
4种引用
强引用
类似于
Object obj = new Object();//一般下我们创建的普通对象都是强引用的对象
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象,就算是OOM也不会对强引用的对象回收,死都不收。
对于一个普通的对象(强引用),显式地将相应(强)引用赋值为null, 一般认为就是可以被垃圾收集的了。
软引用
SoftReference 类实现软引用。在系统要发生内存溢出异常之前,才会将这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。
//创建软应用对象:在对象构造器中传入的数据是我们想要使用的数据
SoftReference softReference = new SoftReference<byte[]>(new byte[1024*1024*5]);
System.out.println(softReference.get());
弱引用
WeakReference 类实现弱引用。只被弱引用关联的对象只能生存到下一次垃圾收集发生为止;
在系统GC时,只要发现弱引用,无论内存是否足够都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
弱引用对象相比于软引用对象更容易、更快被GC回收
虚引用
PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
垃圾回收算法
Stop-the-World
在介绍JVM垃圾回收算法前,先介绍一个概念:Stop-the-World
Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。
复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
优点:
清理效率高且实现简单
不产生内存碎片
缺点:
将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)。
标记清除
标记-清除算法是几种GC算法中最基础的算法,分为标记和清除俩个阶段。
-
标记阶段:标记出需要回收的对象,使用的标记算法均为可达性分析算法。
-
清除阶段:清除被标记的对象并释放其占用的内存空间
Mark-Sweep缺点:内存碎片化
标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续可用的内存空间会不太好找。
标记整理
标记-整理法是标记-清除法的一个改进版。
同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;
不同的是,在清除阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。
标记整理优点:
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
标记整理缺点:
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
上述三种垃圾回收算法对比
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法>标记清除算法>复制算法。
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
分代回收
分代回收算法实际上是把复制算法和标记压缩的结合,并不是真正一个新的算法
一般分为:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。堆结构划分如下:
堆区:新生代:老年代 = 1:2
新生代区:Eden:From:To = 8:1:1
新生代(Young Gen)
新生代特点
新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关(存活的较少),因而很适用于新生代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen)
老年代的特点
老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
垃圾收集器
1、垃圾收集器图
如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
垃圾收集器
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
JDK 1.8默认垃圾收集器使用 Parallel(年轻代和老年代都是)
JDK 1.9默认垃圾收集器使用 G1[G One,不要读错了]
代码中查看使用的垃圾收集器
import java.lang.management.GarbageCollectorMXBean; //查看使用的垃圾收集器 List<GarbageCollectorMXBean> l = ManagementFactory.getGarbageCollectorMXBeans(); for(GarbageCollectorMXBean b : l) { System.out.println(b.getName()); }
控制台打印如下:
2、Serial/Serial Old收集器
Serial ˈsɪəriəl 直译串行
可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;
新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
3、ParNew 收集器
ParNew收集器收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为完全一一样
ParNew特点是:新生代并行(有多条垃圾回收线程),老年代串行;新生代复制算法、老年代标记-压缩
4、(jdk8)Parallel / Parallel Old 收集器
Scavenge直译清扫 Parallel直译并行
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
参数控制: -XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供参数控制: -XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
5、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)
=======
优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量
6、(jdk9)G1收集器
-XX:+UseG1GC 使用G1垃圾回收器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多核处理器及大容量内存的机器. 以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征.
G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。
与CMS收集器相比G1收集器有以下特点:
-
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
-
分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
-
空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
-
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
=====
Region
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
Remembered Set
为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。
7、ZGC收集器
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC是Azul System公司开发的 C4(Concurrent Continuously Compacting Collector)收集器
-XX:+UseZGC 使用ZGC垃圾回收器(jdk11以后支持)
8、垃圾回收器比较
9、垃圾回收器选择策略
- 如果你想要最小化地使用内存和并行开销,请选serial GC
- 如果你想要最大化应用程序的吞吐量,请选 Parallel Scavenge + Parallel Old(jdk8默认)
- 如果你想要最小化GC的中断或停顿时间,请选CMS GC+ParNew
cms并发回收养老区,parnew并行回收新生区
但遗憾的是,在jdk9中被标记为过时,在jdk14中,已经将CMS废弃