垃圾收集器与内存分配策略
- 垃圾收集器关注的内存是Java堆和方法区
- 引用计数算法
- 描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的
- 优点:实现简单、判断效率很高
- 缺点:很难解决对象之间相互循环引用的问题
- 可达性分析算法
- 描述:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(从GC Roots到这个对象不可达)时,则证明此对象是可回收的
- 可作为GC Roots的对象如下
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- Java是通过可达性分析来判断对象是否存活的
- 引用
- 强引用 类似
Object obj = new Object()
这类的引用,只要强引用还在,垃圾收集器永远不会回收被引用的对象 - 软引用
- 有用但非必需对象
- 系统将要发生内存溢出异常之前,会对这些软引用对象二次回收
SoftReference
类实现软引用
- 弱引用
- 非必需对象,强度比软引用更弱
- 无论当前内存是否足够,只被弱引用关联的对象都会被回收
WeakReference
类实现弱引用
- 虚引用
- 幽灵引用或幻影引用
- 引用关系最弱
- 不会对对象的生存时间构成影响,无法通过虚引用取得对象实例
- 唯一目的:虚引用关联的对象被收集器回收时收到一个系统通知
PhantomReference
类实现虚引用
- 强引用 类似
- 可达性分析算法之对象死亡 对象死亡,至少要经历两次标记过程
- 可达性分析后,对象没有与
GC Roots
相连接的引用链,第一次标记,并进行一次筛选- 筛选的条件:对象是否有必要执行
finalize()
方法- 对象没有覆盖
finalize()
方法,或者finalize()
方法已经被虚拟机调用过,没必要执行finalize()
方法 - 若判定为有必要执行
finalize()
方法,放入F-Queue
队列中finalize()
方法是对象逃脱死亡命运的最后一次机会- 稍后GC将对
F-Queue
中的对象进行第二次小规模标记 - 对象在
finalize()
方法中拯救自己:重新与引用链上的任何一个对象建立关联即可,在第二次标记时该对象将被移除“即将回收”的集合 - 第二次标记未逃脱,基本上就真的被回收了
- 任何一个对象的
finalize()
方法都只会被系统自动调用一次,如果对象面临下一次回收,finalize()
方法不会被再次执行
- 对象没有覆盖
- 筛选的条件:对象是否有必要执行
- 可达性分析后,对象没有与
- 回收方法区
- 方法区垃圾收集主要回收两部分:废弃常量和无用类
- 废弃常量回收 以String为例,如果没有任何String对象引用常量池中的某一常量,也没有其他地方引用这个常量,此时发生内存回收时,有必要的话,该常量就会被系统清理出常量池
- 无用类回收 判断无用类三个条件,都满足时 可以 被回收:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.Lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- 场景 大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能
- 垃圾收集算法
- 标记-清除算法
- 最基础的收集算法
- 分为“标记”和”清除”两个阶段 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 缺点
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后需要分配较大对象时,无法找到足够的连续内存而不得提前触发另一次垃圾收集动作
- 复制算法
- 描述 将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清洗掉。
- 优点 每次都是对整个半区进行内存回收,内存分配时不用考虑内存碎片等复杂情况,实现简单,运行高效
- 缺点 代价是将内存缩小为原来的一半
- 现代商业虚拟机都采用这种算法回收新生代
- 将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor
- 回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
- HotSpot虚拟机默认Eden和Survivor的大小比例是8:1
- 当Survivor空间不够用时,需要依赖老年代进行分配担保 如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代
- 标记-整理算法
- 针对老年代
- 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法
- 当前商业虚拟机的垃圾收集都采用这种算法
- 将Java堆分为新生代和老年代
- 新生代采用复制算法
- 老年代采用“标记-清理”或者 “标记-整理”算法
- 标记-清除算法
- 垃圾收集器
- Serial收集器
- 新生代收集器
- 单线程收集器
- 新生代采用复制算法,老年代采用标记-整理算法
- 垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,即“Stop The World”
- 适用于client模式下
- ParNew收集器
- 新生代收集器
- Serial收集器的多线程版本,多条线程进行垃圾收集
- 新生代采用复制算法,老年代采用标记-整理算法
- Stop The World
- 除Serial收集器之外,只有它能与CMS收集器配合工作
- 并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
- 并发:用户线程与垃圾收集线程同时执行
- Parallel Scavenge收集器
- 新生代收集器,采用复制算法
- 并行的多线程收集器
- 目标是达到一个可控制的吞吐量 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 提供两个参数精确控制吞吐量
- 控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数
- 大于0的毫秒数,保证内存回收花费的时间不超过设定值
- 值越小,垃圾收集速度越快,但牺牲了吞吐量和新生代空间,新生代空间更小,导致垃圾收集更频繁,吞吐量降低
- 垃圾收集时间占总时间的比例 GCTimeRatio 大于0且小于100的整数
- -XX:+UseAdptiveSizePolicy
- 这个参数打开后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式成为GC自适应的条件策略
- 控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数
- Serial Old收集器
- 老年代收集器
- 单线程收集器
- 标记-整理算法
- 主要意义给Client模式下使用
- Parallel Old收集器
- 老年代收集器
- 多线程收集器
- 标记-整理算法
- 吞吐量优先
- Serial收集器
- CMS收集器
- 以获取最短回收停顿时间为目标的收集器
- 标记-清除算法
- 4个步骤
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- 初始标记、重新标记两个步骤需要Stop The World。并发标记和并发清除过程收集器线程都可以与用户线程以前工作
- 优点:并发收集、低停顿
- 缺点
- 对CPU资源非常敏感 占用CPU资源,吞吐量降低
- 无法处理浮动垃圾,可能出现“Concurrent Mode Failure” 失败而导致另一次Full GC产生
- 浮动垃圾:并发清理阶段用户线程还在运行,还会有新的垃圾不断产生,无法在当次收集中处理,只好留待下一次GC时再清理掉
- 需要预留空间给用户线程使用,使用参数 -XX:CMSInitiatingOccupancyFraction 可以调节老年代已使用空间从而触发收集,但当预留的空间无法满足用户线程时,会发生 “Concurrent Mode Failure” 失败,此时会临时启动 Serial Old收集器
- 由于基于标记-清除算法,因此会产生大量空间碎片,不利于大对象的分配
- G1收集器
- 特点
- 并行与并发
- 分代收集
- 空间整合
- 可测试的停顿
- 整个Java堆划分为多个大小相等的独立区域(Region) G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
- 使用Remembered Set避免全堆扫描 G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
- 除去Remembered Set操作,G1运作步骤如下:
- 初始标记 初始阶段仅仅是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象,这阶段需要停顿线程,但耗时很短
- 并发标记 并发阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
- 最终标记 最终标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set中,这阶段需要停顿线程,但可并行执行
- 筛选回收 筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
- 内存分配与回收策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代 虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对在Survivor区每熬过一次的Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代
- 动态对象年龄判定 如果在Survivor空间中的相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 空间分配担保 在发生Minor GC之前,如果老年代最大可用连续空间小于新生代所有对象的总空间,并且设置了允许担保失败,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,但是可能会担保失败,失败后也只能重新发起一次Full GC;如果小于,就要进行一次Full GC
- 特点