垃圾回收是JVM垃圾回收器提供的一种用在空闲时间不定时回收无任何对象引用的对象所占据的内存空间的一种机制。
垃圾收集器在一个Java程序汇总的执行是自动的,不能强制执行,我们能做的就是通过调用System.gc方法来建议执行垃圾收集器,但是到底是否可执行,什么时候执行都是不可知的,虽然这是它的缺点,但瑕不掩瑜。
如果你了解过C++,你肯定明白对象所占的内存在程序结束前一直被占用,在明确释放之前不能分配给其他对象;
在Java中,垃圾回收能自动释放内存空间,减轻编程负担,JVM的一个系统级线程会自动释放该内存块。
这使得C++程序员最头疼的内存管理问题迎刃而解,而且因为垃圾回收机制,Java中的对象也不再有“对象域”的概念,只有对象的引用才有“对象域”。
此外,垃圾回收可以有效防止内存泄漏,有效的使用空间内存。
小段子,在餐厅里吃饭,吃完把餐盘端走清理,是C++程序员,而吃完直接走的,是Java程序员。
垃圾,这里指的是可以销毁的对象,其占有的空间是可以回收。
根据JVM架构划分,几乎所有的对象实例都在堆中存放,所以垃圾回收主要是针对堆来进行的。
JVM的眼中,垃圾就是指在堆中存在,但是已经“死亡”的对象。
而对于死亡的定义,我们简单理解为“不可能再被任何途径使用的对象”。
问题来了,你怎么证明对象是死是活呢?
JVM没有明确说使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:
1:找到所有存活对象
2:回收被无用对象占用的内存空间,使该空间可被程序再次使用
给对象添加一个引用计数器:
当对象增加一个引用时,计数器+1;
当对象失效一个引用时,计数器-1;
两个对象如果出现循环引用的情况,此时引用计数器永不为0,导致无法对它们进行回收。
所以因为有循环引用存在,JVM不使用引用计数算法(经典白雪)。
代码如下:
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
现代虚拟机基本都是采用这种算法来判断对象的存活。
通过叫做GC Roots的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个节点(这样通过GC Root串成的一条线就叫引用链),直到所有节点都遍历完毕,如果相关对象不在任意一个以GC Root为起点的引用链中,则这些对象都会被判断为垃圾,被GC回收。
那么有哪些对象有机会可以作为GC Roots,分为以下几种:
1.虚拟机栈(栈帧中本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等
2.在方法区中类静态变量引用的对象
3.在方法区中常量引用的对象
4.在本地方法中JNI(Native方法)引用的对象
5.在JVM内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象及系统类加载器
6.所有被同步锁持有的对象
7.反应 Java 虚拟机内部情况的 JMXBean,JVMTI 中注册的回调,本地代码缓存等。
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可能会有其他对象“临时性”加入,共同构成完整的GC Roots集合。
可达性分析是基于引用链进行判断的,在JDK1.2后,Java将引用关系分为以下四类:
要宣告一个对象死亡,需要经过至少两次标记过程:
1.如果对象进行可达性分析后发现GC Roots不可达,将会进行第一次标记;
2.随后进行一次筛选,条件是——此对象是否有必要执行finalized()方法。
如果对象没有覆盖finalized()方法,或者finalized方法以及被JVM调用过了,那么都会被视为没有必要执行。
如果判断结果是有必要执行,此时对象会被放入名为F-Queue的队列,收集器会进行第二次小规模的标记,如果对象在finalized()方法中重新将自己与引用链上的任何一个对象进行了关联,那么它完成了自我拯救,第二次标记会将其移除“即将回收”的集合,否则该对象就将被真正回收,走向死亡。
在Java堆上进行对象回收的性价比通常比较高,因为大多数对象都是朝生夕灭。
而方法区由于回收条件比较苛刻,对应的回收性价比通常比较低,主要回收两部分内容:
废弃常量和无用的类。
与堆中对象回收类似,以常量池字面量回收为例,如果字符串“abc”进入了常量池,但是没有任何一个String对象引用它,也没有任何地方引用了这个字面量,如果发生内存回收,有必要的话,这个常量会被系统清理出常量池。
当前大多数虚拟机都遵循“分代收集”的理论进行设计,它建立在强弱两个分代假说下:
强弱分代假说奠定了垃圾收集器的设计原则:
收集器应该将Java堆划分不同的区域,然后将回收对象依据年龄(对象经历垃圾收集的次数)分配到不同的区域中进行存储。
如果一个区域中的对象都是朝生夕灭的,那么收集器只需要关注少量对象的存活而不是去标记那些大量要回收的对象,此时就能以较小代价获取较大空间。
最后再将难以消亡的对象集中到一起,根据强分代假说,它们很难消亡,所以JVM可以使用较低频率进行回收,兼顾了时间和内存空间的开销。
根据分代收集理论,收集范围可以分为以下几种类型:
我们通过了可达性算法来识别哪些数据是垃圾,那么如何高效的对这些垃圾回收呢?
由于JVM规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里主要有下面几种方式。
标记清除算法是基础垃圾回收算法,分为两部分:
1.先把内存区域中的这些对象标记(哪些属于可回收标记出来)
2.把这些垃圾拎出来清理掉。
缺点:
1.虽然逻辑十分清晰,但是会产生大量内存碎片,从而可能导致无法为大对象分配足够的连续内存。
2.执行效率不稳定,如果Java堆上包含大量需要回收的对象,则需要进行大量标记和清除动作。
标记-复制算法基于“半区复制”算法:
1.它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。
2.当这一块的内存使用完了,就将还存活着的对象复制到另外一块上面。
3.将使用过的那块内存空间一次性清理掉。
优点:避免了内存空间碎片化的问题
缺点:
1.如果内存中多数对象都是存活的,这种算法将产生大量的复制开销;
2.浪费内存空间,内存空间变为了原有的一半。
### 标记-整理算法
在标记完成之后,让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。
优点:可以避免内存空间碎片化,也可以充分利用内存空间;
缺点:
1.它对内存变动的更频繁,需要整理所有存活对象的引用地址,效率上比复制算法要差很多。
2.在移动存活对象可能要全程暂停用户程序。
商业虚拟机采用分代手机算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法,严格来说并不是一种思想或者理论,而是融合三种基础的算法思想,针对不同情况采用不同算法的一套组合拳。
一般将堆分为新生代和老年代。
并行和并发是并发编程中的专有名词,在谈论垃圾收集器的上下文语境中,它们的含义如下:
最基础,历史最悠久的收集器,进行垃圾回收时,必需暂停其他所有的工作线程,直到收集结束。
缺点:要暂停其他所有的工作线程,直到收集结束。
优点:单线程避免了多线程复杂的上下文切换,因此在单线程环境下收集效率非常高。
它是Serial收集器的老年代版本,同样是一个单线程收集器,采用标记-整理算法,主要用于给客户端模式下的HotSpot虚拟机使用:
Serial收集器的多线程版本,可以使用多条线程进行垃圾回收:
它是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,采用标记-整理算法实现:
与ParNew一样是多线程收集器
其他收集器的关注点在于尽可能缩短垃圾收集时用户现场的停顿时间,而它的目标是达到一个可控制的吞吐量,被称为“吞吐量优先”收集器。这里的吞吐量是指CPU用于运行用户代码的时间占总时间的比值
(吞吐量 = 运行用户代码时间 \ (运行用户代码时间 + 运行垃圾收集时间))
。
停顿时间越短就越适合需要与用户相互的程序,良好的响应速度能提升用户体验。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间越小,垃圾回收越频繁,导致吞吐量下降。
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
它是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,整个收集过程分为以下四个阶段:
1.初始标记(inital mark):标记GC Roots能直接关联到的对象,耗时短但需要暂停用户线程;
2.并发标记(concurrent mark):从GC Roots能直接关联到对象开始遍历整个对象图,耗时长但不需要暂停用户线程。
3.重新标记(remark):采用增量更新算法,对并发标记阶段因为用户线程运行而产生变动的那部分对象进行重新标记,耗时比初始标记稍长且需要暂停用户线程;
4.并发清除(inital sweep):并发清楚掉已经死亡的对象,耗时长但不需要暂停用户线程。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
缺点:
面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。
堆被分为新生代和老年代,其他收集器进行手机的范围是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆划分为多个大小相等的独立区域,新生代和老年代不再物理隔离。
(H代表了大对象)
通过引入Region的概念,从而将原来的一整块内存空间划分为多个小空间,使得每个小空间可以进行单独的垃圾回收。
每个Region都可以根据不同的需求来扮演不同的新老年代空间。
这种划分带来了很大的灵活性,使得可预测的停顿事件模型成为可能。
通过记录每个Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。
通过使用Remembered Set ,在做可达性分析的时候就可以避免全堆扫描。
在不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
具备的特点:
空间整合:整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)来看是基于复制算法实现,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在Gc上的时间不得超过N毫秒。