- 第一门使用内存动态分配和垃圾收集技术的语言—Lisp
- C语言、C++等需要程序员手动释放开辟的内存。
- 如果程序员忘记释放某些内存,就会产生内存泄漏,随着程序一直运行,内存泄漏越多,可能出现内存溢出(OOM)错误。
- 内存泄漏: 对象不会再被程序使用了,垃圾回收机制又回收不掉,称为内存泄漏
- 内存溢出OOM:产生对象的速度 > 垃圾回收的速度 ; 内存泄漏也是产生OOM的原因之一
- 垃圾回收机制是Java的招牌能力,极大地提高了开发效率,使得程序员无序关心内存的释放,专注于业务代码。
概述
什么是垃圾?
运行的程序中没有任何引用指向的对象
若不及时回收清理,垃圾对象所占空间一直得不到释放,可能最终导致OOM内存溢出
垃圾回收机制的意义?
- 如果不及时的清理垃圾,垃圾所占内存空间越来越多,最后没有足够的空间来产生其他新对象,程序OOM
- 清除内存中的碎片空间,碎片整理将所占用的堆内存移动到堆的一端,以便JVM将整理出的内存分配给新的对象
- 随着应用程序的业务越来越庞大、复杂,用户越来越多,没有垃圾回收就不能保证程序的正常运行
Java垃圾回收机制
自动内存管理
- 不需要程序员去手动释放内存,降低内存泄漏和内存溢出的风险
- 程序员只需要专注业务代码,不再关心内存的管理
自动内存管理对于程序员的影响
- 程序员过度依赖"自动内存管理",会弱化程序员在程序出现OOM时定位问题和解决问题的能力
- 了解JVM的自动内存分配和回收原理,才能在OOM时,快速根据日志信息定位问题和解决问题
- 需要排查内存泄漏和内存溢出时 | 当垃圾收集成员并发的瓶颈时
必须对垃圾回收机制做出必要的监控和调节
那些内存需要回收?
- 堆 是重点回收区域
- 频繁收集Yong区
- 较少收集Old区
- 方法区 基本不回收
垃圾标记阶段算法
- 标记阶段就是为了判断对象是否存活
- 需要从堆中区分存活对象与死亡对象
- 如何判断:一个对象不再被其他存活对象引用时,判定为垃圾
引用计数算法(JVM不使用)
引用计数算法(Reference Counting)
对每个对象保存一个整形的引用计数器,记录对象被引用的情况
对于obj对象
- 有其他对象引用了obj,计数器就 + 1 ; 引用断开,计数器 - 1
- 引用计数器为0时,表示对象不再被使用了,标记为垃圾,可被会回收
优点:
- 实现简单、判定效率高、回收器没有延迟
缺点:
- 需要每个对象使用一个计数器的属性,增加了空间开销
- 每次建立引用或引用断开,需要更新计数器,增加了时间开销
- 无法处理循环引用的情况
可达性分析算法(JVM使用)
也叫根可达算法、根搜索算法
可达性分析算法和引用计数算法相比较:
- 可达性分析算法解决了循环引用的问题
- 同样简单、高效
可达性分析算法的思路
先建立一个GCRoots的概念
思路:
- 从根对象GCRoots为起点,从上到下搜索被GCRoots所连接的对象是否可达
- 使用可达性分析算法,内存中存活的对象都会被GCRoots直接or间接连通,走过的路径称为引用链(Reference Chain)
- GCRoots和目标对象没有引用链,是不可达的,该对象可标记为垃圾对象
GCRoots可以是哪些对象?
- 栈中引用的对象(本地方法栈/虚拟机栈)
- 方法区中Java类的引用类型静态变量
- 方法区中常量的引用(如:字符串常量池里的引用)
总结:
虚拟机栈、本地方法栈、方法区、字符串常量池等对堆空间进行引用的都可以作为GCRoots进行可达性分析
垃圾回收阶段算法
复制算法
- 使用两块内存空间(对应新生代中的幸存者
s0
和s1
) - 将s0中存活的对象复制到s1中,再清空s0,下一次复制s1到s0,清空s1
- s0和s1 交替执行
优点:效率高,不产生内存碎片
缺点:需要2倍的空间
在新生代中使用复制算法
清除算法
- 从根节点开始标记所有被引用的对象(可达性分析算法)
- 并不是直接清除垃圾对象 ;记录垃圾对象,放到一个空闲列表中
- 有新对象需要分配空间,在空闲列表中判断空间是否够用,够用的话就用新对象替换垃圾对象
优点:不需要移动(对比复制算法),实现简单
缺点:
- 清理后空闲内存不连续,会产生内存碎片
- 效率不高
压缩算法
- 从根节点开始标记所有被引用的对象(可达性分析算法)
- 针对清除算法的不足(内存碎片),整理存活的对象(压缩对象到内存一端),清理垃圾(边界外的空间),不会产生碎片
优点:
- 对比复制算法:不使用额外空间(2倍内存)
- 对比清除算法:算法执行后,内存区域不分散,不产生碎片空间
缺点:
- 对比复制算法:比复制算法效率低
- 移动对象时,如果对象被其他对象引用了,还要调整引用的地址
- 需要暂停所有用户线程,STW
清除算法 VS 压缩算法
压缩算法的效果 = 清除算法 + 一次内存碎片整理
清除算法是一种不移动对象的回收算法(使用空闲列表记录位置),会产生内存碎片
压缩算法是一种会移动式对象的,不会产生内存碎片
垃圾回收算法总结
- 复制算法效率最高,但是多浪费一倍空间
- 清除算法不移动对象,会产生内存碎片
- 压缩算法比复制算法多了标记阶段
- 压缩算法比清除算法多了内存整理的阶段
分代收集
没有完美应用所有场景的垃圾回收算法,在不同的分区采用不同的回收算法,将利用率达到最大
分代收集思想:不同对象生命周期不同,根据回收的频率来选择合适的垃圾回收算法
- Http请求中的Session对象、线程、Socket连接…与业务直接相关,生命周期较长
- String对象,经常改变,产生大量对象,可能用一次就不再用了
.
- 年轻代
空间较小(年轻代:老年代= 1:2)对象生命周期短,存活率较低,回收频繁
使用复制算法实现,复制算法的效率只与当前存活对象的大小有关
复制算法内存利用率低的问题,可以通过hotspot中的两个survior设计缓解 - 老年代
对象的生命周期长,存活率较高,回收没有年轻代频繁
使用清除算法+压缩算法实现- 标记(Mark)阶段的开销与存活对象数量成正比
- 清除(Sweep)阶段开销与管理区域的大小成正比
- 压缩(Compact)阶段开销与存活对象的数据成正比
垃圾回收器
垃圾回收器是对垃圾回收算法的实现
垃圾回收器分类
按线程数分类
-
单线程垃圾回收器(Serial)–串行:只有一个GC线程,GC线程工作时其他用户线程暂停
-
多线程垃圾回收器(Parallel)–并行:多个线程同时工作,多核CPU下效率大大提升,也会在暂停用户线程
按工作模式分类
- 独占式:垃圾回收线程工作时,用户线程全部暂停(STW)
- 并发式:垃圾回收线程与用户线程一次执行,不用暂停(从CMS这款垃圾回收器开始引入并发执行)
按内存工作区域分类
- 年轻代垃圾回收器:Serial、ParNew、Parallell
- 老年代垃圾回收器:Serial Old、CMS、Parallel Old
HotSpot垃圾回收器
HotSpot提供了6种垃圾回收器
两个回收器连线代表这两个可以搭配使用
CMS垃圾回收器
概述
CMS追求低停顿(降低STW的时间),采用了用户线程与垃圾回收线程并发执行
CMS之前 单线程or多线程垃圾回收器都是独占式
回收过程
- 初始标记:
STW,单线程独占, 标记所有与GCRoots直接关联的对象 - 并发标记:
垃圾回收线程与用户线程并发执行(用户线程不用暂停),使用可达性分析算法标记处所有对象。 - 重新标记:
STW,多线程独占,标记出新的垃圾对象 - 并发清除:
垃圾回收线程与用户线程并发执行(清除垃圾对象,使用标记-清除算法)
优点:与用户线程并发执行,降低了STW的时间,不会明显的停顿
缺点:
- 基于标记-清除算法,会产生内存碎片
- 与用户线程并发,占用了线程导致程序变慢,吞吐量降低
- 无法处理浮动垃圾(垃圾回收线程并发标记时,用户线程不暂停,标记完后随时可能产生新的垃圾对象,只能等待下次标记处理)
三色标记算法
将对象分为了三种状态
- 黑色:该对象和它的属性全部被标记过了(如GCRoots对象)
- 灰色:该对象被垃圾回收器扫描过,还有没被扫描过的引用
- 白色:没有被扫描过,表示不可达
三色标记算法过程
- 刚开始确定的GCRoots为黑色
- 将与黑色对象关联的对象置为灰色
- 从灰色对象遍历,将灰色对象置为黑色,将新的黑色对象关联的对象置为灰色
- 重复第3步,遍历到没有灰色对象结束
- 清除白色对象(垃圾对象)
三色标记可能出现的问题
漏标
A 关联了 B ,B 又关联了D,E;
当前A是黑色(GCRoots),B是灰色
此时A与B的联系断开,B、D、E都可以被当做垃圾回收掉
但是,B已经是灰色,黑色对象只会遍历一次,下次从灰色对象开始遍历,将灰色对象置为黑色(三色标记算法第3步)
等待下一轮的垃圾回收,B、D、E就是浮动垃圾
本该作为垃圾回收掉的对象,又侥幸活了下来,这就是漏标
错标
A关联了B,B关联了C
此时断掉B、D的联系;但A与D又建立了联系;
黑色对象A只会遍历一次,因此 D 被标记为白色(垃圾对象),回收掉,但A又引用了D,这时程序就会运行错误
解决错标
打破错标产生的两个必要条件之一,就可以解决错标的问题
原始快照:
- 记录 灰色指向白色引用断开 的关系
- 本次扫描结束后,再从灰色对象开始扫描一遍,重新进行标记
增量更新:
- 记录 黑色与白色建立引用 的关系
- 本次扫描结束后,从记录中的黑色对象开始,重新标记
总结:
CMS与其他回收器区别在于:GC标记对象的同时,用户线程不会暂停,可能修改了对象的引用关系,导致标记垃圾,回收垃圾出问题
引入了三色算法来解决:对象可以有3种标记状态(黑、灰、白),将用户线程修改的对象引用关系记录下来,在重新标记阶段修正对象引用关系
G1垃圾回收器
G1 也是并发标记-清除
- G1将堆中的每个区域分为更小不相关的区域(Region)
使用不同的Region表示Eden、survivor0、survivor1、Old - G1避免整个堆进行垃圾收集,维护一个优先列表,根据区间的优先级(垃圾数量)来回收垃圾数量最多的空间Region
- 由于侧重于回收最多垃圾的区域,所以叫垃圾优先(Garbage First)
G1垃圾回收器的回收过程
- 初始标记:单线程,标记出GCRoots直接关联的对象,用户线程暂停(该阶段速度较快)
- 并发标记:从GCRoots开始可达性分析,找出存活对象,和用户线程并发执行(该阶段较耗时)
- 最终标记:修正标记阶段用户线程修改的对象引用
- 筛选回收:对各个区域Region的回收价值(垃圾最多)和成本(STW时间最短)进行排序,回收时会暂停用户线程
G1的使用场景
- 控制GC的停顿时间
- 内存占用较大的引用
查看JVM垃圾回收器和设置垃圾回收器
打印默认垃圾回收器
//打印默认垃圾回收器
-XX:+PrintCommandLineFlags -version
//JDK 8 默认的垃圾回收器
//年轻代使用
Parallel Scavenge GC
//老年代使用
Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails -version
设置默认垃圾回收器
Serial 回收器
-XX:+UseSerialGC
//年轻代使用 Serial GC, 老年代使用 Serial Old GC
ParNew 回收器
-XX:+UseParNewGC
//年轻代使用 ParNew GC,不影响老年代。
CMS 回收器
-XX:+UseConcMarkSweepGC //老年代使用 CMS GC。
G1 回收器
-XX:+UseG1GC //手动指定使用 G1 收集器执行内存回收任务
-XX:G1HeapRegionSize //设置每个 Region 的大小。
对象的 finalization 机制
Java允许开发人员编写 对象被销毁之前 的自定义处理逻辑
finalize() 方法
对象在销毁前,会调用finalize()方法
- 垃圾回收器回收一个垃圾对象时,会先调用这个对象的finalize()方法,且finalize()方法只会被调用一次
- finalize()方法可以重写,常用于进行一些资源的释放、清理(关闭文件/套接字/数据库连接…)
为什么不能主动去调用finalize()方法?
- 调用finalize()可能导致对象复活
- finalize()执行时间是不确定的,完全有GC线程决定
- 如果finalize()是死循环or循环次数特别多,会严重影响GC的性能
虚拟机中对象分为3种状态
-
可触及的:从根节点开始可以到达该对象
-
可复活的:对象的所有引用都被切断,但对象可能在finalize()中复活
-
不可触及的:对象调用了finalize()方法,且没有复活,进入不可触及状态(不能复活,finalize()只会调用一次)
对象只有不可触及时,才能被回收
回收过程
判断一个对象A是否可以回收,至少经历两次标记过程
- 如果从GCRoots不能到达A,进行第一次标记
- 如果A没有重写finalize()或者已经调用过一次finalize(),A被判定为不可触及的
- 如果A重写了finalize() 并且 还没有调用过,A对象会被放入一个队列中,由一个Finalizer线程触发队列中对象的finalize()方法
- finalize()是对象复活的机会,GC会对队列中的对象进行第二次标记,如果A和存活对象建立联系,第二次标记时A会被取消回收。如果之后再次没有引用指向A,对象会直接编程不可触及状态(一个对象的finalize()只能调用一次)
代码演示finalize()复活对象
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}