垃圾判断算法
- 引用计数算法(Reference counting)
- 给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能在被使用
- 引用计数算法无法解决对象循环引用的问题
2. 根搜素算法(GC Roots Tracing)
- 在实际的生产语言中,都是使用根搜索算法判定对象是否存活
- 算法基本思路就是通过一系列的称为“GC Roots” 的点作为起始进行向下搜索,当一个对象到GC Roots没有引用链相连,则证明此对象是不可用的
在java 语言中,GC Goots包括:
(1)在VM栈(贞中的本地变量)中的引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等
(2)方法区的静态引用、常量引用的对象
比如:java 类的引用类型静态变量,字符串常量池里的引用
java虚拟机规范表示可以不要求虚拟机在这区实现GC,方法区GC性价比一般比较低
在堆中,尤其是在新生代,常规引用进行一次GC一般可以回收70%-95%的空间,而方法区的GC效率远低于此
当前的商业jvm都有实现方法区的GC,主要回收两部分内容:废弃常量与无用类
类回收需要满足如下3个条件:
该类所有实例都已经被GC,也就是JVM中不存在该Class的任何实例
加载该类的ClassLoader已经被GC
该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
(3)JNI(即一般说的Native方法)中的引用对象
注 :除了这些固定的gc roots集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC ROOTS集合。比如:分代收集和局部回收(partial gc)。如果只针对java堆中国呢的某一块区域进行垃圾回收(比如:典型的只针对新声代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完成有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入gc roots集合中去考虑,才能保证可达性分析的准确性。
对象的finaliztion机制
- java语言提供了对象终止finaliztion机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
- finalize方法允许在子类中被充血,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作
对象生存还是死亡
- 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。但事实上,也并非“非死不可”的,这个时候他们暂时处于“缓行”阶段。一个无法初级的对象有可能在某个条件下“复活”自己。虚拟机中的对象可能的三种状态为:
- 可触及:从根节点开始,可以达到这个对象
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
- 不可触及的:对象的finalize被调用,并且没有复活。不可触及的对象不可能被复活,因为finalize()方法只会被调用一次
- 判定一个对象objA是否可回收,至少要经历两次标记过程:
- 如果对象objA到GC Roots没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行finalize()方法
(1)、如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
(2)、如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
(3)、finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合(可复活的)。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。
GC算法
- 标记-清除算法(mark-sweep)
算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象
缺点:效率问题:标记和清理两个过程效率都不高,需要扫描所有对象,堆越大,gc越慢
空间问题:标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足后的连续内存而提前触发另一次的垃圾搜集动作,gc次数越多,碎片越严重,同时由于空闲内存是不连续的,需要维护一个空闲列表
何为清除:
这里的清除不是真的置空,而是把需要清除的对象地址保存在空闲列表的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
![虚拟机栈、堆的初始状态](https://i-blog.csdnimg.cn/blog_migrate/9ae5c3a58c88c9b5437110304741943b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7907fa475b56df8550a28642876edc43.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b10c875083f445c017dcf41f85395155.png)
- 标记-整理算法(mark-compact)
(1)标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存
(2)没有内存碎片
(3)比标记清除耗费更多的时间进行整理
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可
- 消除了复制算法当中,内存减半的高额代价
缺点:
- 从效率上来说,标记整理算法要低于复制算法
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。
- 复制算法(copying)
(1)将可用内存划分为两块,每块只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉
(2)这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效,只是这种算法的代价是将内存缩小为原来的一半,代价高昂。
(3)现在的商业虚拟机中都是用了这一种收集算法来回收新生代
(4)将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中的一块survivor,当回收时将eden和survivor还存活的对象,一次性拷贝到另外一块survivor空间,然后清理eden和用过的survivor
(5)oracle hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。
(6)只需要扫描存活的对象,效率更高、不会产生碎片、需要浪费额外的内存作为复制区
(7)复制算法非常适合生命周期比较短的对象,因为每次gc总能回收大部分的对象,复制的开销比较小
(8)根据IBM的专门研究,98%的java对象只会存活1个gc周期,对这些对象很适合用复制算法,而且不用1:1的划分工作区和复制区的空间。
优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现碎片问题
缺点:
- 需要两倍内存空间
- 对于g1这种拆分成为大量region的gc,复制而不是移动,意味着gc需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
- 分代算法(generational)
(1)当前商业虚拟机的垃圾收集都是采用分代算法,根据对象不同的存活周期将内存划分为几块
(2)一般把java堆分为新生代和老年代,这样就可以根据各年代的特点采用适当的收集算法,譬如新生代每次gc都有大批量的对象死去,只有少量对象存活,那就选择复制算法只需要付出少量存活对象的复制成本就可以完成收集
(3)年轻代:新生成的对象都放在新生代,年轻代用复制算法进行gc,年轻代分为三个区,一个eden区,两个survivor区,对象在eden区生成,当eden区满时,还存活的对象将复制到一个survivor区,当这个survivor区满时,此区的存活对象将被复制到另外一个survivor区,当第二个survivor区也满了的时候,从第一个survivor区复制过来的并且此时还存活的对象,将被复制到老年代。
(4)老年代:存放了经过一次或多次gc还存活的对象,一般采用标记压缩或者标记整理算法进行gc,有多种垃圾收集器可以选择,每种垃圾收集器可以看作一个gc算法的具体实现,可以根据具体应用的需求选用合适的垃圾收集器
内存回收
- GC要做的是将那些dead的对象所占用的内存回收掉 : hotspot认为没有引用的对象是dead的,hotspot将引用分为四种:Strong、soft、weak、phantom,strong即默认通过 Obeject o = new Object() 这种方式赋值的引用; soft、weak、phantom这三种则都是继承Reference
- 在Full GC 时会对Reference类型的引用进行特殊处理: soft 内存不够时一定会被gc、长期不用也会被gc;weak 一定会被gc,当被mark为dead,会在ReferenceQueue中通知;phantom:本来就没引用,当从jvm heap中释放时会通知
内存泄漏
- 也称作“存储渗漏”,严格来说,只有对象不会在被程序用到了,但是gc又不能回收他们的情况,才叫内存泄漏。
- 尽管内存泄漏不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outofmemory异常
垃圾收集器
1. serial收集器
单线程收集器 ;收集时会被暂停所有工作线程(stw),使用复制算法收集,虚拟机运行在client模式时的默认新生代收集器;最早的收集器,单线程进行gc;new 和 old 都可以使用;在新生代,采用复制算法,在老年代使用标记整理算法;因为是单线程gc,没有多线程切换的额外开销,简单易用
2. parNew收集器
parNew收集器就是serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、stw、对象分配规则、回收策略等都与serial收集器一模一样;对应的这种收集器是虚拟机运行在server模式的默认新生代收集器,在单cpu的环境中,parNew收集器并不会比serial收集器有更好的效果;可以通过-xx:ParallelGCThreads来控制gc线程数的多少
3. Parallel Scavenge收集器
也是一个多线程收集器,也是使用复制算法 ,但它的对象分配规则与回收策略都与parnew收集器有所不同,它是以吞吐量最大化(即gc时间占总运行时间最小)为目标的收集器实现,它允许较长时间的stw换取总吞吐量最大化
4. serial old 收集器
是单线程收集器,使用标记-整理算法,是老年代的收集器
5. parallel old 收集器
老年代版本吞吐量优先收集器,使用多线程和标记整理算法,jvm1.6提供,在此之前,新生代使用了ps 收集器的话,老年代除serial old 外别无选择,因为ps 无法与cms收集器配合工作;注重吞吐量; Parallel Scavenge+parallel old = 高吞吐量,但gc停顿不理想
6. cms 收集器
cms是一种以最短停顿时间为目标的收集器,使用cms并不能达到gc效率最高(总体gc时间最小),但它能尽可能降低gc时服务的停顿时间,cms收集器使用的是标记清除算法;追求最短停顿时间 ,非常适合web应用;只针对老年区,一般结合parnew使用;gc线程和用户线程并发工作,只有在多cpu环境下才有意义;-xx:UseConcMarkSweepGC
cms以牺牲cpu资源的代价来减少用户线程的停顿,当cpu个数少于4的时候,有可能对吞吐量影响非常大;在并发清理过程中,用户线程还在跑,这时候需要预留一部分空间给用户线程;cms 使用标记清除算法,会带来碎片问题,碎片过多容易频繁触发full gc。
他们对应的JVM参数如下
新生代(别名) | 老年代 | JVM 参数 |
---|---|---|
Serial (DefNew) | Serial Old(PSOldGen) | -XX:+UseSerialGC |
Parallel Scavenge (PSYoungGen) | Serial Old(PSOldGen) | -XX:+UseParallelGC |
Parallel Scavenge (PSYoungGen) | Parallel Old (ParOldGen) | -XX:+UseParallelOldGC |
ParNew (ParNew) | Serial Old(PSOldGen) | -XX:-UseParNewGC |
ParNew (ParNew) | CMS+Serial Old(PSOldGen) | -XX:+UseConcMarkSweepGC |
G1 | G1 | -XX:+UseG1GC |
-
串行回收器:serial、serial old
-
并行回收器:pranew、prarllel scavenge、prarllel old
-
并发回收器:cms、g1
GC的时机
- 在分代模型的基础上,gc从时机上分为两种:scavenge GC 和 Full GC
- Scavenge gc 触发时机:新对象生成时,eden空间满了;理论上eden区大多数对象会在scavenge gc回收,复制算法的执行效率会很高,scavenge gc时间比较短
- full gc 对整个jvm 进行整理,包括yuong、old和perm;触发时机:1)old满了 2)perm满了 3)system.gc(); 效率很低,尽量减少full gc。
垃圾收集器的“并行” 和 “并发”
- 并行:指多个收集器线程同时工作,但是用户线程 处于等待状态
- 并发:指收集器在工作的同时,可以允许用户线程工作;并发并不代表解决了gc停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候,但在清除垃圾的时候,用户新线程可以和gc线程并发执行。
枚举根节点
- 当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知那些地方存放着对象引用。在hotspot的实现中,是使用一组成为oopmap的数据结构来达到这个目的。
安全点
程序执行时并非在所有地方都能停顿下来开始gc,只有在特定的位置才能停顿下来开始gc,这些位置成为安全点
在可达性分析算法中查找存活的对象,首先要找到哪些是GC Roots;
有两种查找GC Roots的方法:
一种是遍历方法区和栈区来查找(保守式GC);
一种是通过OopMap的数据结构来记录引用的位置(准确式GC),如在类加载过程中,JIT编译过程中,分别记录下 类成员 和 调用栈 中的引用的调用信息。对应OopMap的位置即可作用一个安全点。线程只有到达安全点时才能暂停下来进行可达性分析。
- 在oopmap的协助下,hotspot可以快速且准确地完成gc root枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者是说oopmap内容变化的指令非常多,如果为每一条指令都生成对应的oopmap,那将会需要大量的额外空间,这样gc的空间成本将会变得更高
- 实际上,hotspot并没有为每条指令都生成oopmap,而只是在“特定位置”记录了这些信息,这些位置成为安全点safepoint,即程序执行时并非在所有地方都能停顿下来开始gc,只有达到安全点时才能停顿。
- safepoint的选定既不能太少以至于让gc等待时间太长,也不能过于频繁以至于过分增大运行时的负载。所以,安全点的选定基本上是以“是否具有让程序长时间执行的特征” 为标准进行选定的--因为每条指令的时间非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转,所以具有这些功能的指令才会产生safepoint。
- 对于safepoint,另一个需要考虑的问题是如何在gc发生时,让所有线程都“跑”到最近的安全点上再停顿下来:抢占式中断和主动式中断
- 抢占式中断:它不需要线程的执行代码主动去配合,在gc发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。
- 主动式中断:当gc需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。现在几乎没有虚拟机采用抢占式中断来暂停线程从而影响gc事件。
安全区域
- 在使用safepoint似乎已经完美地解决了如何进入gc的问题,但实际上情况却并不一定。safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入gc的safepoint。但如果程序在“不执行”的时候呢?所谓不执行就是没有分配cpu时间,典型的例子就是处于sleep状态或者Blocked状态,这时候线程无法相应jvm的中断请求,jvm显然不太可能等待线程重新分配cpu时间。对于这种情况,就需要安全区域safeRegin来解决
- 在线程执行到safe region中的代码时,首先标识自己已经进入了safe region,那样,当在这段时间里jvm要发起gc时,就不用管标识自己为safe region状态的线程了。在线程要离开safe region时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开safe region 的信号为止