一、思考
哪些内存需要回收? (什么是垃圾) 什么时候回收? 如何回收?
二、对象是否存活
1. 引用计数算法
引用计数算法实现简单高效,但主流的java虚拟机并没有使用,主要原因是它很难解决对象直接相互循环引用的问题。
题外话:
python使用了该算法,它是如何解决循环引用的?
- 手动解除:在合适的时机,接触引用关系
- 使用弱引用weakref,weakref是python提供的标准库,旨在解决循环引用
2. 可达性分析算法
在主流的商业程序语言(java、c#、Lisp等)使用可达性分析算法判断对象是否存活。
通过把“GC Roots”作为起始点,开始往下搜索,搜索的路径称为引用链(Refernce Chain),搜索到的对象判定为存活,反之为可回收。
java语言中,可作为“GC Roots”的对象有:
在虚拟机栈(栈帧中的本地变量表)中引用的对象
- 譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
方法区中类静态属性引用的对象
- 譬如Java类的引用类型静态变量
方法区中常量引用的对象
- 譬如字符串常量池(String Table)里的引用。
本地方法栈中JNI(一般说的Native方法)引用的对象
Java虚拟机内部的引用如基本数据类型对应的Class对象,一些常驻的异常对象
- (比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
所有被同步锁(synchronized关键字)持有的对象。
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。
例如:
后文将会提到的分代收集 和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生 代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不 可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引 用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确 性
3. 引用的类型
3.1 强引用(Strong Reference)
类似“Object obj = new Object()”就是强引用,只要强引用还存在,垃圾收集器永远不会回收该对象
3.2 软引用(Soft Reference)
软引用用来描述一些还有用但并非必需的对象,对于软引用关联的对象,在系统要发生内存溢出异常之前,会将这些对象列入回收范围进行二次回收,二次回收后还没有足够的内存,才会抛出内存异常。jdk1.2之后,提供了SoftReference类来实现软引用
3.3 弱引用(Weak Reference)
弱引用也是用来描述非必需对象的,但强度比软引用更弱一些,当垃圾回收器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。jdk1.2之后,提供了WeakReference类来实现弱引用
3.4 虚引用(Phantom Reference)
虚引用也称为幽灵引用或者幻影引用,一个人对象是否有虚引用,完全不影响其生存时间,也无法通过虚引用来获取一个对象实例。设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。jdk1.2之后,提供了PhantomReference类来实现虚引用
4. 对象是否存活
finalize()方法可以理解为对象最后的操作,只能执行一次。
永远不要主动调用finalize()方法,交给垃圾回收机制调用
理由:
- 调用finalize()方法时可能会导致对象复活
- 执行时机没有保障,完全由GC线程决定,极端情况下不发生GC,则没有执行机会
- 一个糟糕的finalize()方法会影响GC性能
5. 方法区回收
方法区的回收效率比较低
主要回收两部分内容:废弃常量和无用的类
- 废弃常量判定:
加入一个字符串常量”abc”进入了常量池,但是没有其他地方引用,如果此时发生了垃圾回收,而且有必要的话,这个”abc”就会被系统清理出常量池
- 无用的类判定,满足三个条件:
- 该类所有实例已经被收回,java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
满足了以上三个条件就可以进行回收,但不是必定回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制
在大量使用反射、动态代理、、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义的ClassLoader场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出
三、垃圾回收算法
1. 标记清除算法
特点:
- 标记和清除的效率低
- 产生大量不连续的空间碎片
2. 复制算法
特点:
- 效率高
- 占用内存大
使用场景:用来回收新生代,新生代中的对象98%是 “朝生夕死”,所以不需要按照1:1划分空间,新生代按8:1:1划分。仅“浪费”10%的内存。但是不能保证每次存活的对象都不多于10%,所以需要借助其他内存
3. 标记整理算法
4. 分代收集算法
一般把java堆分为新生代、老年代。
- 新生代较少对象存活,使用复制算法
- 老年代存活率高、没有额外空间担保,使用标记清除、标记整理算法
四、垃圾收集器
1. 评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间=程序的运行时间+内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用的时间与总运行时间的比例
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
- 收集频率:相对于应用程序的执行,收集操作发生频率
- 内存占用:Java堆区所占的内存大小
- 快速:一个对象从诞生到被回收所经历的时间。
2. 垃圾收集器分类
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS , G1
3. 参数
-XX:+PrintCommandLineFlags
查看命令相关参数,包含使用的垃圾收集器
命令行方式: jinfo -flag 相关垃圾回收器参数 进程ID
-Xloggc:(文件路径/文件名)
打印GC日志
4. 七种经典垃圾收集器
Ⅰ Serial收集器:串行回收
采用复制算法、串行回收和STW机制
优势:
- 单线程下简单高效
Ⅱ Serial Old收集器:串行回收
采用标记整理算法、串行回收和STW机制
客户端模式下:
- HotSpot虚拟机使用。
服务端模式下:
- 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用
- 另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用
Ⅲ ParNew收集器:并行回收
除了并回收机制,和Serial回收器几乎没有区别
-XX:+UseParNewGC
使用ParNew回收器
-XX:+ParallelGCThreads
限制线程数量
Ⅳ Parallel Scavenge收集器:并行回收
吞吐量优先、采用复制算法、并行回收和STW机制
吞吐量是Parallel Scavenge与ParNew的区别
-XX:+UseParallelDC
指定新生代使用Parallel Scavenge收集器
-XX:+UseParlleloldGC
指定老年代使用Parallel Old收集器
上面两个参数开启一个,另一个也会开启
-XX:ParallelGCThreads
设置年轻代并行收集器的线程数,最好与CPU数量相同
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间,即STW的时间
-XX:GCTimeRatio
垃圾收集时间占总时间的比例(= 1 / (N + 1))。
用于衡量吞吐量的大小
- 取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%
- 与前面-XX:MaxGCPauseMillis参数有一定矛盾性,暂停时间越长,Radio参数就容易超过设定的比例
-XX:+UseAdaptiveSizePolicy
开启Parallel Scavenge收集器自适应调节策略
Ⅴ Parallel Old收集器:并行回收
吞吐量优先、标记-压缩算法、STW机制
-XX:+UseParallelDC
指定新生代使用Parallel Scavenge收集器
-XX:+UseParlleloldGC
指定老年代使用Parallel Old收集器
上面两个参数开启一个,另一个也会开启
Ⅵ CMS收集器:并发回收
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器(低延迟),真正意义上的第一款并发收集器。同样会有STW
基于标记-清除算法实现,运作过程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
优点
- 并发收集
- 低延迟
缺点
- 会产生内存碎片,导致提前触发Full GC:因为需要并发清除,不能使用标记-压缩算法
- 对CPU资源非常敏感:使用并发,占用一部分线程,导致吞吐量降低
- 无法处理浮动垃圾:在并发标记到重新标记这个过程,可能有些原来不是垃圾的对象变为垃圾,这部分垃圾无法识别,称为浮点垃圾
版本变化
- JDK9:CMS被标记为过时
- JDK14:删除CMS
Ⅶ GI 收集器:区域化分代回收
G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器。被Oracle官方称为“全功能的垃圾收集器”。
G1收集器是并发与并行的,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区、幸存者1区、老年代等
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。所以G1又称垃圾优先
优势
- 并行与并发
- 分代收集
能够兼顾年轻代和老年代的垃圾回收
- 空间整合
G1的回收时以Region作为基本单位的。Region之间是复制算法,但整体上可以看作是标记-压缩算法。两种算法都可以避免内存碎片,在堆非常大的时候性能更加明显
- 可预测的停顿时间模型(即:软实时)
G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒
缺点
- G1为了垃圾收集产生的内存占用和额外执行负载都要比CMS高
- 在内存小的情况CMS的表现更优,G1则在内存大的时候优势更大,平衡点在6-8GB之间
使用场景
- 面向服务应用端,具有大内存、多处理器的机器
- 超过50%的java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间长(长于0.5至1秒)
① 分区Region:化整为零
使用G1收集器是,它将整个堆空间划分为2048个大小相同的独立的Region块,大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。所有的Region大小相同,且在JVM生命周期内不会改变。
G1还新增了一种新的内存区域叫Humongous内存区域,主要用于存储大对象,如果超过1.5个Region,就放到Humongous
虽然还保留新生代、老年代的概念,但是在物理上不是隔离的了,通过动态分配的方法式实现逻辑上的连续。
设置Humongous区域的原因:
- 短期存在的大对象会对垃圾收集器造成负面影响,所以新增Humongous区域
- 如果一个Humongous装不下大对象,那么G1会寻找更多的Humongous来存储,为了找到更多的Humongous,有时候不得不调用Full GC。G1大多数行为都把Humongous区当作老年代得一部分来看待
② G1收集器执行过程
年轻代GC —> 年轻代GC + 并发标记过程 —> 混合回收,接着循环
或者:年轻代GC —> 年轻代GC + 并发标记过程 —> 混合回收 —> Full GC,接着循环
年轻代GC过程
- 扫描根
- 更新 Rememnered Set
处理dirty card queue中的card,更新 Rememnered Set。此阶段后, Rememnered Set可以准确的反应老年代对象所在的内存分段中对象的引用
- 处理 Rememnered Set
- 复制对象
- 处理引用
并发标记过程
- 初始化标记阶段
标记从根节点直接可达的对象。这个阶段是STW的,并且触发一次年轻代GC
- 根区域扫描
扫描Survivor区直接可达的老年代区域对象,并标记被引用对象。这一过程必须在young GC之前完成
- 并发标记
在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
- 再次标记
由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)
- 独占清理
计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下一阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集
- 并发清理阶段
识别并清理完全空闲的区域
混合回收
Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收部分Old Region
Full GC(可选的)
当上面三个方式不能正常工作,就会使用单线程的内存回收算法,性能会非常差,程序停顿时间会很长
③ Rememnered Set 记忆集
垃圾回收时,Region不同代之间的相互引用,需要扫描整个堆?
- 每个Region都有一个对应的Rememnered Set
- 每次引用类型数据写操作时,都会产生一个Write Barrier(写屏障)暂时中断操作
- 然后检查将要写入的引用指向的对象是否和该引用类型数据在不同的Region(其他收集器:检查老年代对象是否引用新生代对象)
- 不同:通过CardTable把相关引用信息记录到指向对象所在的Region对应的Rememnered Set 中。相同不做任何操作
- 当进行垃圾回收时,在GC根节点的枚举范围加入Rememnered Set ,就可以保证不进行全局扫描,也不会有遗漏
上图中Rset是记忆集,本质上的结构式哈希表。key是Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)
5. 低延迟垃圾收集器
五、垃圾回收的相关概念
1. System.gc()的理解
- 在默认情况下,通过System.gc()的调用,会显示触发Full GC, 同时对老年代和新生代进行回收,尝试释放被丢弃的对象占用的内存
- 无法保证一定调用垃圾收集器, 只是我们向虚拟机表达想要调用一下
- 开发中不要调用, 会导致STW的发生
2. 内存溢出(OOM)
没有空闲空间,并且垃圾收集器也无法提供更多内存
一般抛出OOM之前都会执行一次GC, 但是我们区分配一个超大对象, 超过堆的最大值, 就不会触发GC, 直接抛出OOM
3. 内存泄漏(Memory Leak)
严格来说,只有在对象不会被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
宽泛来说, 对象周期过长甚至导致(OOM), 也可称为”内存泄漏”
内存泄漏例子:
- 单例模式
单例模式的周期和应用程序一样长, 如果单例程序中持有外部对象的引用, 那么这个对象是不能被回收的, 则会导致内存泄漏的产生
- 资源未关闭
数据连接, 网络连接, io连接, 这些都是必须手动调用close, 否则是不能被回收的
4. 垃圾回收的并行与并发
程序的并行与并发
- 并行
多个CPU执行多个任务
- 并发
一个CPU来回切换执行多个任务
垃圾回收的并行与并发
- 并行
指多条垃圾收集线程并行工作, 用户线程等待
- 串行
相较于并行概念,单线程执行, 用户线程等待
- 并发
5. 安全点和安全区域
安全点:
安全点的数量不可过少, 也不可过多. 安全的选择一般选执行时间较长的指令, 例如方法调用、循环跳转和异常跳转等
发生GC时如何保证所有线程都到安全点停顿下来呢?
- 抢先式中断:(目前没有虚拟机采用了)
首先中断所有线程,再判断哪些没有到达安全点,再恢复该线程,让它跑到安全点
- 主动式中断:
设置一个中断标志, 各个线程运行到安全点的时候主动轮询这个标志,标志为真,则将自己进行中断挂起
安全区域:
考虑到优先线程可能处于Sleep状态或Blocked状态,这时候无法到安全的响应中断请求,这时候需要安全区域来解决
安全区域式指在一段代码中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。
六、内存分配与回收策略
1. 对象优先在eden分配
大多数情况在eden区中分配,当eden内存不够分配时,虚拟机发起一次Minor GC
当Survivor的空间无法放入对象时,通过分配担保机制提前转移到老年代
2. 大对象直接进入老年代
大对象是指需要连续内存空间的java对象,例如很长的字符串或数组。尽量避免写寿命短的大对象
3. 长期存活的对象将进入老年代
对象在Survivor区域存活过一次Minor GC,“年龄”+1,到达一定年龄则进入老年代
4. 动态对象年龄判断
如果Survivor区域中相同年龄所有对象大小的总和超过Survivor空间的一半时,年龄大于等于该年龄的对象进入老年代