3.1 概述
垃圾收集(Garbage Collection, GC) 当需要排查各种内存 溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术是是必要的监控和调节;
- 程序计数器、虚拟机栈、本地方法栈随线程而生、而灭;内存分配和回收都具备确定性;当方法结束或者线程结束时,内存自然就跟随着回收了;
- Java堆和方法区很不确定性:一个接口的多个实现类需要的内存可能会不一样;一个方法所执行的不同条件分支所需要的内存也可能不一样;只有运行时才知道;这部分内存的分配和回收是动态的;垃圾收集器所关注的也是这部分内存;
3.2 对象已死?
3.2.1 引用计数算法
- 对象中添加一个引用计数器
- 每当有一个地方引用他时,计数器值加一;
- 当引用失效时,计数器值减一;
- 难以解决对象之间相互循环引用的问题;
可达性分析算法
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活;
- 基本思路:通过一系列称为“GC Roots”的跟对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”;从某个对象到GC Roots间没有任何引用链相连,或从GC Roots到这个对象不可达,此对象不可能再被使用;
Java里,固定可作为GC Roots的对象包括以下集中: - 在虚拟机栈中引用的对象;各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量;
- 在方法区中类静态属性引用的对象;如Java类的引用类型静态变量;
- 在方法区中常量引用的对象;如字符串常量池里的引用;
- 在本地方法栈中JNI引用的对象;
- Java虚拟机内部的引用;如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器;
- 所有被同步锁(synchronized关键字)持有的对象;
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
- 其他的:分代收集、局部回收;如果只针对Java堆中某一块区域发起垃圾收集时,必须考虑到内存区域是虚拟机自己的实现细节 – 某个区域里的对象可能被位于堆中其他区域的对象所引用;
3.2.3 再谈引用
引用(传统定义):如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,称该reference数据是代表某块内存、某个对象的引用; 引用分为强引用、软引用、弱引用和虚引用;4种引用强度一次逐渐减弱
- 强引用: 最传统的 “引用”定义,指在程序代码之中普片存在的引用赋值;类似“Object obj = new Object()”这种引用关系; 只要强引用关系还存在,垃圾收集器就永远不会收掉被引用的对象;
- 软引用: 用来描述一些还有用,但非必须的对象;只被软引用关联的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常;
- 弱引用: 描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存道下一次垃圾收集发生为止;当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象;
- 虚引用:幽灵引用、幻影引用;最弱的一种引用关系;一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用关联的唯一目的:为了能在这个对象被收集器回收时收到一个系统通知;
3.2.4 生存还是死亡?
可达性分析算法种判定为 不可达的对象,也不是“非死不可”; 之后有两次标记:
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
- 随后进行一次筛选,条件是此对象是否有必要执行finalize()方法;
- 如果对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,那么没必要执行finalize方法;
- 如果对象被判定为确有必要执行finalize方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后有一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行他们的finalize()方法。
- **: 执行指 虚拟机会触发这个方法执行,并不一定会等待他运行结束
- 如果finalize方法执行缓慢、极端死循环,将导致F-Queue队列中的其他对象永久处于等待,升值导致整个内存回收子系统崩溃。
finalize方法是对象逃脱死亡命运的最后一次机会(在finalize里 给对象 加个引用;但finalize只能执行一次,只能逃脱一次):
- 稍后收集器将对F-Queue中的对象进行第二次小规模的标记;
- 如果对象要在finalize中拯救自己-- 只要重新与引用链上任何一个对象建立关联即可;如把自己赋值给某个类变量或者对象的成员变量;在第二次标记时它将被移出“即将回收”的集合;
- 如果对象这时候还没有逃脱,那基本上他就真的要被回收了;
3.2.5 回收方法区
- Java堆中,尤其是新生代中,对常规应用进行一次垃圾收集通常可以回收70%到99%的内存空间;
- 相比之下,方法区回收囿于苛刻的判定条件,回收成果往往远低于此;
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
- 废弃的常量:与Java堆中的对象非常类似;例子:“java”曾经进入常量池中,但当前系统没有任何一个字符串对象的值是“java” – 已经没有任何字符串对象引用常量池中的“java”常量,而且虚拟机中也没有其他地方引用这个字面量;这时候发生内存回收,判定有必要的话,他就会被清理出常量池; 常量池中其他类(接口)、方法、字段的符号引用也与此类似;
- 判定一个常量是否“废弃” 还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻,需要同时满足三个条件:
- 1、该类所有的实例都已经被回收;不存在该类以及任何派生子类的实例;
- 2、加载该类的类加载器已经被回收;除非是精心设计的可替换类加载器的场景,否则很难达成;
- 3、该类对应的java.lang.Class对象没有在任何地方被引用;无法在任何地方通过反射访问该类的方法;
满足三个条件,被允许回收; 不是像对象一样 必须回收;
3.3 垃圾回收算法
介绍分代收集理论和集中算法思想及其发展过程; 垃圾收集算法可以划分为“引用计数式垃圾收集(Reference Counting GC)”和 “追踪式垃圾收集”(Tracing GC)两大类; 这两类常被称为“直接垃圾收集”和“间接垃圾收集”; 下面介绍的算法 都属于 追踪式垃圾收集的范畴;
3.3.1 分代收集理论
3.3.2 标记-清除算法
3.3.3 标记-复制算法
3.3.4 标记-整理算法
3.4 HotSpot算法细节实现
3.4.1 根节点枚举
3.4.2 安全点
3.4.3 安全区域
3.4.4 记忆集与卡表
3.4.5 写屏障
3.4.6 并发的可达性分析
3.5 经典垃圾收集器
3.5.1 Serial收集器
3.5.2 ParNew收集器
3.5.3 Parallel Scavenge收集器
3.5.4 Serial Old收集器
3.5.5 Parallel Old收集器
3.5.6 CMS 收集器
3.5.7 Garbage First收集器
3.6 低延迟垃圾收集器
3.6.1 Shenandoah收集器
3.6.2 ZGC收集器
3.7 选择合适的垃圾收集器
3.7.1 Epsilon收集器
3.7.2 收集器的权衡
3.7.3 虚拟机及垃圾收集器日志
3.7.4 垃圾收集器参数总结
3.8 实战: 内存分配与回收策略
3.9小结
|