先引出一些简单的概念,不想看直接往下翻
一、 判定对象死亡算法
1.引用计数法
- 在内存中为每个对象创建一个引用计数器,记录对象被引用的次数
- 互相引用的对象无法被回收
- 计数器会占用额外的内存空间
- java中主流的gc回收器没有使用该算法的
2.可达性分析
- 根据一系列GC root作为根节点,从这些节点开始,根据引用关系向下搜索,搜索过程走的路程叫引用链,如果一个对象到GC root们没有任何相连,就可以判定为该对象可以被回收(可以被回收不代表下次一定会被回收)
哪些变量适合做GC root
- 虚拟机栈中引用的对象(栈帧中本地变量表),比如各个线程被调用的方法堆栈中使用的参数、局部变量表、临时变量等
- 方法区类的静态属性引用的变量
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的class对象,系统类加载器
- 所有被同步锁(synchronized)持有的对象
二、引用的类型
- 强引用 ,平时我们用的引用就是强引用
- 软引用,当一个对象只被软引用对象关联时,在系统即将内存溢出之前,会进行一次回收,并将其化为回收的范围内。
- 弱引用,当一个对象只被弱引用关联时,当发生GC时,就会被回收掉
- 虚引用,只要GC时就会被回收
三、被标记的对象一定会被回收么?
对象被回收之前会经过两次标记
- 第一次标记:当对象不可达时,被标记一次,然后将有必要执行 finalize方法的对象放入F-queue中,所谓有必要是指:
- 该对象重写了finalize方法
- 在1的前提下,finalize方法之前没有被jvm调用过。
也就是说如果对象没有重写finalize方法或者之前被jvm调用过一次,那么这个对象基本就死定了。
- jvm会创建一个低优先级的线程(finalizer)去执行F-queue中每个对象的finalize方法,finalize是对象唯一可能逃离死亡的办法,就是在finalize中强引用自己。但是这个finalizer并不一定保证finalize方法执行完,因为一旦该方法阻塞了,会耽误其他对象执行finalize方法,影响了第二次标记。所以如果finalize中使用某种手段逃离了第二次标记,就会将该对象从准备回收的集合中移除,反之则打上第二次标记。
打上两次标记的对象就基本要被回收了
在《深入了解java虚拟机》这本书中指出 官方已经不建议我们使用finalize方法了
四、方法区的回收
- 有叫它 永久代或者元空间的
- 方法区的回收效率较低
- 方法区的回收主要是废弃的常量和不再使用的类型,类型卸载的条件包括:
- 该类型所有实例已全部回收
- 该类型的类加载已被回收
- 该类型的 java.lang.Class类没有在任何地方被引用(比如没有在别处反射使用)
- 方法区回收的必要性: 使用反射,jdk的动态代理,cglib等技术会产生很多新的类,经过加载,就会将类的信息加载到方法区,系统长久运行后,必然造成方法区的积攒,所以虚拟机具备卸载类的能力还是有必要的,jdk11时期的ZGC不支持类卸载
五、垃圾回收算法
1. 分代收集理论
这是一种收集的理论
根据对象存活时间的特性,分成三种:
- 对象朝生夕死
- 经过了好几轮了后还没死
- 基本不太容易死
根据对象存活时间的不同,有针对性的去选择回收哪一部分对象,能够提高对象回收的效率。
那么java堆划分出不同的区域,垃圾回收期每次只回收一个或几个区域,因此有了minorGC,MajorGC,MixedGC,fullGC这样的回收类型的划分。
上方说的这种对象所在区域 被描述为:年轻代、老年代、永久代(元空间)(方法区)
而minorGC针对年轻代进行回收、MajorGC针对老年代,MixedGC针对整个年轻代+部分老年代(只有G1收集器是这样),fullGC针对年轻代+老年代+方法区(又叫做整堆收集)
跨代引用假说
- 对象相对于同代引用来说,跨代引用的情况极少
- 年轻代上会维护一个数据结构:记忆集,它将老年代分成了若干的部分,并记录了哪部分存在跨代引用,当发生youngGC时,只需要根据记忆集把需要处理跨代引用的GC root拿出来,而不是把整个老年代的gcroot拿出来 做可达判断。
2. 标记-清除
顾名思义,先标记,再把标记的对象清除了,缺点是会造成大量的内存碎片,当需要请求大的连续空间时,明明有足够的内存空间,但是不够连续,造成提前GC。在新生代中对象的创建和回收都比较频繁,所以该算法用于新生代不太合适,老年代还行,毕竟对象不容易被回收。
3. 标记-复制(简称复制)
有两块内存,每次只使用一块,比如当前使用的是A内存,先标记,然后把未标记的(存活)对象复制到另一块划分好的内存B上,然后把A内存整个清空。循环往复,
缺点是:
- 使用有一块内存被浪费掉了
- 复制时,对象的内存地址变了,引用需要变化,属于额外的开销。
- 当存活的对象比较多时,假设99%的对象都存活了,那复制的意义不大,浪费性能。所以用在永久代不太合适
备注一下:上方说有两块内存只是举例子,毕竟G1中可不止两块。而其他采用复制算法的收集器,比如serial、parNew、parallel scavenge都是三块:eden、s0 、s1 (又被称为from survivor 、to survivor)
优点是:
和标记-清除相比,不会造成内存碎片
4. 标记-整理
与标记-清除类型,但是标记后,不直接清除掉,而是把需要清除的对象都整理到一起(移动到一起),然后按边界清除。但是缺点也是当存活对象多时 移动对象有点浪费。所以用于永久代不太合适
六、垃圾回收器
常看到有很多的回收器,不知道用哪个。不知道他们的区别是啥,适应的场景。再展示一下开篇的图
先不看G1,剩余的6种收集器 被称为经典的收集器。
- 单个收集器之间的区别,可以从表中看出可以从
收集的年代
、使用的回收算法
,回收线程的个数
,回收过程中用户线程是否停下来等待回收完成
来做区分. - 类似的收集器 也是各有自己的特点,比如更注重吞吐量,或者注重最短的垃圾回收时间,或者根据设置的回收暂停时间,选择更高效的部分进行局部回收等,稍后会展开说
- 还有一点就是 多个回收器的组合、配合使用。
以上三点是一个通俗的概括,现在根据不同的回收器说一下各自的特点,其实从表中也可以看出。那我们就根据 上述第一点中说要的四个维度对比一下各个收集器。
先不聊G1、ZGC收集器
1.新生代收集器
- serial
- parNew
- parallel scavenge
总:
三者的区别
这三个收集器处理朝生夕死的对象,所以都采用标记-复制算法。
serial
只有一个线程进行垃圾回收,parNew
属于serial的升级版
,可以多线程回收。
parallel scavenge
是jdk1.8中新生代的默认收集器,他也是多线程回收,那么从以上来看它与parNew就非常相似了,但是ps有自己特点,就是它的诞生更关注于一个可接受的吞吐量,这种处理器可以设置 回收暂停最大时长、直接设置吞吐量、自适应调节策略
。
分1:
serial和parNew的选择
都知道,同一时刻,一个CPU的核心只能执行一个线程的代码
答:serial是单线程回收,所以可以从机器的配置上考虑,如果是单核或者核心数少的处理器上,即使有多个线程一起进行回收,效率也高不起来,反而因为上下文切换降低效率。所以这时用serial就比parNew好,反之,就选parNew 或者其他多线程回收的。
parNew和ps(parallel scavenge)的选择
答:
- parNew收集器,如果我们选择使用了CMS处理器(老年代收集),新生代就会默认选择选择parNew,jdk1.9之后,parNew和CMS已经成了一个固定的搭配了,CMS是追求最少垃圾回收时长的处理器。
- jdk1.8中新生代默认使用parallel scavenge,与之配合的老年代默认使用的收集器是 PS markSweep,因为PS markSweep和serial old几乎一样,所以有些资料里也有说使用serial old。
- 以上是从经典搭配和jdk默认配置上来讲两者的区别
- 上方说过ps处理器更专注于一个我们应用可以接受的、或者说是合适的吞吐量,何为吞吐量:
用户代码执行时间
吞吐量= ——————————————————————————————
用户代码执行时间 +GC时间
- 可以从公式中看出,GC时间越短,吞吐量更高,这时候ps处理器给我们提供了两个参数可以设置,分别是
-XX:MaxGCPauseMillis=1000 (1s) 最大垃圾回收时间
-XX:GCTimeRatio= (0-100之间的整数) 对PS收集器直接设置GC占总时间,有点设置吞吐量
的意思
比如:-XX:GCTimeRatio=19
则算出 GC时间占总时间=1/(1+19)=20%,所以公式是:1/(1+GCTimeRatio)
- 先看第一个可设参数:
最大垃圾回收时间
,我们可能会设置一个比较极端的值,比如正常来说 垃圾回收的时间200ms上下浮动是很正常的,而我们偏要设置最大垃圾回收时间 =20ms,那就必要要减少新生代的大小,设想如果新生代一共10G,可能标记花费的时间就不只是20ms,如果能让整个垃圾回收过程少于20ms呢,并且回收器可能会提前进行垃圾回收,可能内存占用1G时就进行回收了。这样反而增加了垃圾回收的次数。 - 再看第二个可设参数,如上方给的例子,设置
-XX:GCTimeRatio=19
,我们的应用不追求最短的垃圾回收时长,比如不太在乎用户响应的话(比如此时刚好进行了一次gc,用时超长,花了2s,那么由于stop the world,用户请求在至少2s内是没有响应的) - 其实ps和parNew最大的区别还是 ps具有自适应调节功能,就是:
-XX:+UseAdaptiveSizePolicy
垃圾收集的自适应策略,PS收集器可以根据系统的运行情况动态的调整
新生代大小(-Xmn)、
eden与survivor的比例(-XX:SurvivorRatio)、
晋升老年代的对象大小(-XX:PretenureSizeThreshold)
- 如果你手动调节jvm困难的话,那就开启这个参数吧,让垃圾回收器自己去帮你调节。哈哈
10.如果应用对回收暂停时间要求不是特别高,且注重吞吐量的情况下可以选择ps收集器,
2.老年代收集器
1.serial-old
2.parallel-old
3.CMS
serial-old 与paralllel-old
1.serial-old是单线程收集的,与之配合最好的就是serial了,ps markSweep实现和serial-old基本一致,它也是parallel-old没出现之前,parallel-scavenge默认搭配的老年代处理器,但是由于是单线程的,ps和它搭配起来不是很好,后来有了parallel-old。他们都是标记-整理的。
CMS(Concurrent Mark Sweep)
它与parNew结合使用(也是默认搭配,jdk1.9后只能这么搭配),与上两款不同的是 它是采用标记-清除的算法,可以允许在回收垃圾的过程中与用户线程并行进行,但不是说就没有STW了。
A.回收的四个阶段
CMS垃圾回收过程分为四个阶段:其中两个阶段依然需要STW.
- 初始标记 (stw):仅仅标记GCROOT能直接关联到的对象,很快
- 并发标记 :从直接关联对象遍历整个对象图。耗时较长但不需要停顿用户线程
- 重新标记 (stw)修正并发标记期间,因用户线程继续运行而引发的标记变动
- 并发清除:清除掉标记阶段可以回收的对象,因为不需要移动存活的对象,所以可以和用户线程一起执行。
B.缺点
对核心数要求比较高
,进行垃圾回收使用的线程数为:(核心数+3)/4,当CPU为4核时,不超过25%的资源去进行垃圾回收,而且核心数越多,占用资源比例越低concurrent-mode-failure
,有一部分垃圾是标记之后 产生的,这部分垃圾不能立即被本次gc回收掉,被称为‘浮动垃圾’,CMS收集老年代垃圾时 不能像其他gc 收集器一样可以回收整个老年代,因为CMS回收时还要留一部分内存给用户线程并发使用的,当预留的内存不够用户线程使用时,就会触发concurrent-mode-failure,进而暂停所有用户线程,临时使用serial-old,对整个老年代进行垃圾回收。设置CMS回收比例的参数是:-XX:CMSlnitiatingOccupanyFraction
C.优点
- 标记清除肯定要比标记-整理要快
- gc时采用多个线程(相对于serial-old这种单线程来说要快)
- gc线程可以与用户线程并发执行(短暂的stw),与(parallel-old相比)
以上小总结
可以看出 当jdk版本为1.8(包含)及以前版本时
,可以根据 Cpu核心数
、对响应时间的敏感度
、吞吐量
做出选择
3.G1收集器
首先人家叫Garbage First 收集器,jdk9之后,代替了parallel scavenge+parallel-old的默认组合,G1称为了默认的收集器。而且CMS在jdk9之后已经不推荐使用了。
G1特别之处
- 前面说过的垃圾回收器,要么对新生代进行回收、要么就是老年代、要么就是整堆回收(fullGC),但是G1就不是这样,他是看
哪地方垃圾最多
,回收效益最高
,就对哪一个或多个部分进行回收,并且可以参照你设置的回收时最大暂停时间结合以上计算排序进行回收,尽可能的满足你设置的最大暂停时间。这就是G1的MixedGC
- G1把内存按1M-32M 为一个Region,将整个内存分为了很多份,可以通过
-XX:G1HeapRegionSize
来设定,大小应为2的整次幂
. - Region中还有一类特殊的Humongous区域,专用来用储存大对象的,G1认为只要大小超过了一个Region的一半的对象,即可认做是大对象,而对于超过了Region自身大小的超大对象将会被存在n个连续的humongous Region中。G1中将这种region视为老年代的对象
- G1仍然保留了新生代、老年代的概念,但是Region作为G1回收的最小单元,每次收集回收都是region的整数倍,region可能作为新生代,也可能作为老年代。回收前,G1会去计算每个Region的可回收价值(包括可回收的空间和回收需要的时间),然后行成一个优先级的列表,每次根据用户设定允许的收集停顿时间(
-XX:MaxGCPauseMills
,默认为200ms),优先回收价值收益最大的那个Region。保证了在有限时间内获得最大的收益,所以叫做Garbage First。 - 内存如果大一些,比如6G以上,可以考虑使用G1,反之用CMS,这只是作为一个参考,可以自己测试看看最终的效果
G1的四个回收阶段
- 初始标记
- 并发标记
- 最终标记
- 筛选回收 (
并没有像CMS那样可以与用户线程并行执行,需要STW
)