前言
- java的厉害之处就在于其对内存方面的自动管理。管理无非就是产生、移动或者移除其中的内容。对于内存中再也用不到的东西,当然有必要删除。垃圾收集就是在进行这个“删除”操作
- jvm管理的空间分为五大部分:程序计数器,java栈,本地方法栈,java堆,方法区(java8中去除永久代,方法区中的类的源信息被存放在元空间内,运行时常量池移入java堆,这部分不理解的可以参考我的博文:java内存区域与内存溢出异常),其中的程序计数器,java栈和本地方法栈随着线程产生和消亡,其中存储的内容相对固定,不作为垃圾回收的目标。而相比java堆和方法区内的内容,由所有线程共用,所以这部分的内存需要动态分配与回收,这就是垃圾回收的目标区域
什么该回收?
- 我们要回收垃圾,首先要确定目标是不是垃圾,jvm提出了两种算法,如下
引用计数法
- 垃圾收集的主要目标区域在java堆中,java堆中对的主要内容是对象(指针),引用计数法的大致内容是:在对象中添加一个“引用计数器”,每当有一个地方对它进行了引用操作,就将计数器+1,引用失效就给计数器-1,当计数器为0时,表示这个对象已死,表示可回收。
- 这个方法存在漏洞:无法处理两个或多个对象之间的循环引用问题。假设有两个对象,他们之间互相引用,而外界并没有他们的引用,他们的引用计数器始终不为零,也就无法回收。
可达性分析法
- 这个方法是目前的主流方法,它通过一系列称为GC Root的节点为起始点,从他们开始向下搜索,搜索形成“引用链”,凡是不在引用链上的对象,一律被判定为可回收对象(垃圾),如图所示:对象1, 2, 3, 4均在引用链上,而对象5, 6, 7, 8之间虽然有引用关系,却没有处于任何GC Root下,故:对象5, 6, 7, 8被判定为可回收对象。
- GC Root可以有多个,他们产生于以下四种位置:
虚拟机栈中的引用对象(Reference)
本地方法栈中引用的对象
原来方法区中,类静态属性中引用的对象
方法区中常量引用的对象
什么时候回收?
- 判定是否是垃圾的算法中,可达性分析法是主流。
- 对象一旦退出了GC Root引用链,就变为了可回收对象,等待回收。其实在这中间还有一个对象自我拯救的过程。关键在于下面的finalize()方法:
- 这个finalize()方法是基类Object类中的方法,在对象被标记为可回收对象时,先进性一轮判断,如果对象覆盖了finalize()方法,并且finalize()方法之前没有被执行,那么这个对象将被放入一个叫做F-Queue的队列中,有专门的的线程Finalizer依次执行F-Queue中对象的finalize()方法(不一定执行完方法或遍历完所有元素),当下一次从根节点开始遍历引用链时,如果F-Queue中有对象通过finalize()方法的执行重新回到引用链上,那么这个对象便又“活过来了”。否则,对象基本上真的就被回收了。
怎么回收?
- 在判定的对象之后,就要对对象进行回收了,主流的方法基于一个理论:分代收集法。
- 顾名思义,对于内存不采用单一方法进行管理,根据对象的特点将内存分为新生代和老年代,分别管理。新生代:这里面的对象大都产生不久,就完成了自己的使命,每次垃圾回收都有大批对象死去。老年代:这里的对象大都是存活了很久的对象,因为一直在被某个地方使用,所以不会轻易被清除,每次垃圾回收变动不大
- 具体的回收的算法大致分为三种:
标记清除算法
- 这个方法是最基础的回收算法。主要思想是将内存中的可回收对象标记,然后清除,不对其他内容作出改变。
- 这样做的弊端:效率低下:标记和清除两个过程的效率都不高
空间问题:清除后不对内存做出任何改变,以一段时间之后内存会碎片化,导致后面如果要存放较大对象,可能倒追无法存放。
标记复制算法
- 为了解决上面标记清除法的效率问题,提出复制算法。这个算法的思想:将空间分为两块,只使用一块空间存放对象,当这块空阿需要进行垃圾回收时,将这块空间中有用的对象整齐的放入第二块“空”空间内存中,直接将第一块空间设置为“空”,循环往复。
- 现代商业虚拟机的新生代(后面会解释新生代和老年代)基本采用这种回收算法,后面还提出了这个算法的改进版:因为新生代中的对象大部分都是刚产生不久,就会被回收,如果用原先的复制算法将会降低空间利用率(原因是每次只能用一半空间),所以提出将内存分为一块较大的区域(Eden区)和两块较小的区域(Servior空间),虚拟机默认比例是1:8,如图:
- 整个过程是这样的:第一次先使用Eden空间和一块Servior空间存储对象,当需要进行垃圾回收时,将Eden空间和第一块Servior中可以存活的对象复制到另一块Servior空间中,由于是在新时代发生的垃圾回收,所以只有少部分对象会被复制过去,然后将Eden空间标记为空,继续使用Eden空间和第二块Servior空间存放对象,如此循环往复。
- 在上面的过程中,有一个漏洞,就是:如果复制的过程中发现第二块Servior空间不够大,无法存放前面要复制过来的全部对象,怎么办?于是jvm又提出了分配担保机制:将这部分多出来的对象直接存放到老年代。
标记整理法
- 前面提出的复制算法其实也存在弊端,如果不想浪费50%的空间,就需要有多余出来的空间作为分配担保的空间存在。
- 在老年代的回收算法中提出了标记整理法,这个方法是指:在发生回收时,将内存中的存活对象统一向内存的一端进行移动,也就是整理,全部移动完毕之后,将其他部分标记为可用空间
上面几种算法的具体实现
- 上面算法实现的前提是:已经可以确定那些对象是已经死亡的,但是怎样判定对象是否死亡,上述给出了两种方法:引用计数法和可达性分析法。这里就讨论一下可达性分析法中的一些细节
- 在可达性分析法中,要先找到GC Root,GC Root多存在于全局性引用(常量和静态变量)和执行上下文(栈帧中本地变量表),可是遍历这些地方会消耗大量时间,所以jvm提出了一个名为OopMap的数据结构,这个数据结构中存储有可能是GC Root的引用,当需要进行可达性分析时,通过遍历一组OopMap,可以快速完成GC Root的枚举。
- 虽然OopMap很好用,但也毕竟也占用空间,OopMap过多会导致GC的空间成本增加,所以只在特定位置生成OopMap,这里的特定位置有以下几个:
- 循环的结尾
- 方法返回前/调用方法指令后
- 可能抛出异常的位置
- 程序不执行,当前线程处于sleep或者blocked的状态
- 前三个位置被称为安全点:jvm在GC时,需要让所有线程都停止(Stop The World),如果不停止,有可能会导致引用关系发生变化,GC的准确性无法保证。
- 最后一个被称为安全区域,在这种情况下,当前的引用关系不会发生变化,线程进入该区域后会标识自己进入安全区域,可以开始进行GC Root枚举,当线程要离开安全区域时,如果虚拟机没有完成GC Root枚举,必须等虚拟机完成,才可以继续执行自己的程序。
- 有关让所有线程都停下来的方式,分为两种:
- 抢先试中断:让所有线程全部断掉,如果发现其中某个线程不在安全点处,则恢复该线程,让其运行至安全点处后停止。
- 主动式中断:让所有线程依次访问一个值,当这个值为真,则让自己中断。这个方式是目前的主流方式。
几种垃圾收集器
- 垃圾收集器是jvmGC的主要体现,下面列举出几个垃圾收集器,如图:
图中的连线表示相连的两个垃圾收集器可以协同工作。 - 这里只大概讲一下G1收集器:这是一款面向服务端应用的垃圾收集器。他有如下特点:
- G1是一个多线程的收集器,在多CPU的情况下缩短Stop The World的时间。并且G1还允许用户线程与收集器线程并发,只不过用户线程的速度会响应降低
- 从上图中可以看出,G1实现了分代收集,也就是说,G1可以独自管理java堆等GC区域
- 从整体上来看,G1是基于标记整理算法实现的收集器
- 除了追求更小的停顿时间(Stop The World的时间),他还实现了能够让使用者规定GC发生的时机及频率
- 从内部布局上来讲,G1的内部被分为了一个一个的小块,再将小块分为新生代和老年代。并且它在后台维护了一个优先级列表,列表里将每个小块,根据小块里垃圾堆积的价值大小(如果回收,可以获得多少空间以及回收所要消耗的时长)排列。在回收时,根据优先回收列表,首先回收价值最大的小块(Region)。
内存分配与回收策略
- 对象首先在Eden区分配
- 大对象,直接进入老年代
- 长期存活的对象将进入老年代,jvm给每个对象定义了Age,每进行一次GC就将剩余对象的Age加1,默认加到15时,该对象就会被加入到老年代。
- 动态对象年龄判定:当新生代Servior空间中,有一半以上的对象年龄相同为age,则年龄大于age的对象会被直接移入老年代。