标签(空格分隔): JVM
概述
- GC需要完成的事情
- 哪些内存需要进行回收?
- 什么时候回收?
- 怎么回收?
- GC关注的部分
- Java堆
- 方法区
- 解释:一个接口中的多个实现类所需要的内存可能不一样,一个方法中的多个分支所需的内存也可能不一样,我们只有在程序处于运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的
对象的存在
判定对象存活的算法
- 引用计数算法(Reference Counting)
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象都是不可能再被使用的
- 但是:主流Java虚拟机里面没有选用引用计数算法来管理内存,因为它没有办法解决对象之间相互循环引用的问题
- 可达性分析算法(Reachability Analysis)
- 基本思路:通过一系列成为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots 到这个对象不可达)时,则证明此对象是不可用的
- 可作为GC Roots 的对象
- 虚拟机栈(局部变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般来说的Native方法)引用的对象
对象引用
- 四种对象引用概述
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
- 四种引用强度依次减弱
- 四种引用对象详解
- 强引用:就是指程序代码中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用:用来描述一些还有用但并非必需的对象;对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存的话,才会抛出内存溢出异常
- 弱引用:也是用来描述非必需对象的,但是比软引用的作用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用:也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例,唯一目的就是能在这个对象被收集器回收时受到一个系统通知
对象的生存或死亡
- 判断一个对象真正死亡要至少经过两次标记
- 第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行第一次筛选
- 筛选判定的条件是此对象有没有必要执行finalize方法,当对象没有覆盖此方法或者此方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
- 如果这个对象被判定有必要执行finalize方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并由一个有虚拟机自动建立的低优先级的Finalizer线程去执行它
- 第二次标记:finalize方法是对象逃脱死亡命运的最后一次机会稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize方法中成功拯救自己--只要重新与引用链上的任何一个对象建立联系即可,譬如把自己(this关键字)赋值给某个变量或者对象的成员变量,那在第二次标记时,它将会被移除“即将回收”的集合
- 第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行第一次筛选
回收方法区
- 永久代的垃圾收集
- 废弃常量:
- 无用的类:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
- 整体概述
- 标记-清除算法(Mark-Sweep)
- 复制算法(Copying)
- 标记整理算法(Mark-Compact)
- 分代收集算法(Generational Collection)
- 模块详解
-
标记-清除算法:首先标记要回收的对象,在标记完成之后统一回收所有被标记的对象
- 效率问题:
- 空间问题:会产生大量的内存碎片
-
复制算法:将内存划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上去,然后再把已经使用过的内存空间一次清理掉
- 商用虚拟机实现:IBM公司的专门研究表明,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor;当回收时,将Eden空间和刚才用过的Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
- HotSpot:默认Eden空间和Survivo空间的大小比例8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%,只有10%会被浪费;当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)
- 标记-整理算法:根据老年代的特点,先标记要清楚的对象,然后将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
-
分代收集算法:把Java堆分为新生代和老年代
- 新生代:每次垃圾收集都发现会有大批对象死去,只有少量存活,那就选用复制算法
- 老年代:对象存活率较高、没有额外的空间对它进行分配担保,就必须使用”标记-清除“或”标记-整理“算法
-
标记-清除算法:首先标记要回收的对象,在标记完成之后统一回收所有被标记的对象
HotSpot的算法实现
枚举根节点
在HotSpot虚拟机的实现中,虚拟机有办法达到当执行系统停顿下来(即Stop-The-World)并不需要一个不漏的检查完所有执行上下文和全局的引用位置,而直接得到哪些地方存放着对象的引用的目的,即使用一组OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用
安全点
- 概述
- 实际上,HotSpot也的确没有为每一条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置成为安全点(Safepoint)
- 方法调用、循环跳转、异常跳转等具有让程序长时间执行的特征的指令才会产生Safepoint
- 在GC发生时,让所有线程跑到安全点
- 抢先式中断(Preemptive):
- 主动式中断(Voluntary Suspension):轮询标志
安全区域
安全区域(Safe Region)是指在一段代码之中,引用关系不会发生变化;在这个区域内的任何地方开始GC都是安全的
垃圾收集器
整体概述
- 各种收集器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
模块详解
-
Serial收集器
- 单线程
- “Stop-The-World”
- Client模式下简单而高效,新生代收集器
-
ParNew收集器
- 多线程
- “Stop-The-World”
- Server模式下,首选新生代收集器
-
Parallel Scavenge收集器
- 并行多线程
- 新生代收集器
- 复制算法
- 目的:达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
-
Serial Old收集器
- 单线程
- 标记整理算法
- 老年代收集器
- 主要作用是在Client模式下
-
Parallel Old收集器
- 多线程
- 标记整理算法
- 老年代收集器
-
CMS收集器(Concurrent Mark Sweep)
- 主要目的:获取最短回收停顿时间
- 标记清除算法
- 运作过程
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- 缺点
- 对CPU资源敏感
- 无法处理浮动垃圾
- 大量空间碎片产生
-
G1收集器(Garbage-First)
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
- 运作过程
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
GC日志的理解
- 各部分详解
- 第一块(红色):表示GC发生的时间,从Java虚拟机启动以来经过的秒数
- 第二块(紫色):表示GC的停顿类型,如果是Full,则这次GC是发生了Stop-The-World的
- 第三块(青色):表示GC发生的区域,名称与GC收集器相关联
- 第四块(绿色):表示GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
- 第五块(橙色):表示GC所占用的时间,单位为秒
- 第六块(黄色):表示GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)
内存分配与回收策略
对象优先在Eden(新生代)分配
- 当Eden区域没有足够的空间进行分配时,虚拟机将发起一次Minor GC
- Minor GC和Major GC
- Minor GC(新生代GC):非常频繁,而且速度较快
- Major GC(老年代GC):一般比Minor GC慢上10倍以上
大对象直接进入老年代
大对象:需要大量连续的内存空间的Java对象,例如很长的字符串以及数组
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor区中每熬过一次Minor GC年龄就增加1岁,当它的年龄增加到一定程度(默认是15岁),就会被晋升到老年代
动态年龄判定
如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代的所有对象空间,如果这个条件成立,那么Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续查找老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的:如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC