您的位置:首页 > 其它

垃圾收集器和内存分配策略

2017-12-02 00:00 190 查看
摘要: 垃圾收集器就是用来收集垃圾的“机器(程序)

什么是垃圾收集器

顾名思义,垃圾收集器就是用来收集垃圾的“机器(程序)”。之前我也很疑惑,既然垃圾回收都进入了自动化的时代了,都鸟枪换炮了,为什么我们还要去了解垃圾收集器呢。答案其实很简单,就是垃圾回收的算法还不够智能,它不能够满足所有的需求,所以有的的时候我们需要根据实际中的需求手动调教一下垃圾收集器,让他更好的配合我们的工作。但是由于垃圾收集器种类也很多,每个种类的垃圾收集器的特点各有不同。这叫像是在训狗,你要充分了解不同狗种之间的习性特点,才能因材施教,对症下药,让狗狗听你的话。否则,在不了狗狗的情况下胡乱的训练,可能导致训狗不成反被狗咬的悲惨下场。

所以,我们想要熟练地使用垃圾收集器,并且想要他们能够更好更高效的工作,首先要做就是了解各种垃圾收集器的特点、主要配置参数和使用场景。

垃圾收集器和内存区域

垃圾收集器和内存区域是紧密相连的,因为垃圾收集器的主要作用简单来说就是在内存区域内回收已经不再使用的内存。而对于java语言来说,这毫无疑问是把“矛头”指向了java对象实例,而java中大部分的对象实例都是分配在java堆上的。所以,可以说,垃圾收集器就是建立在java堆上的。当然,这是一种笼统的说法,其实在方法区也有垃圾回收的。

判断对象的死亡

如何判断对象的死亡,前提是你得先有一个对象。要进行垃圾回收,必须要先判断哪些对象是可以回收的,哪些对象是不能回收的。你不能乱来,对于现实世界来说,乱世可能出英雄;但是,对于程序世界来说,乱世只能出悲剧。所以,垃圾回收不能胡来。可以把垃圾回收想象成一个火葬场,对于尸体的焚烧,首先要判断一下尸体是否已经死亡。所以,这里要讲的就是如何判断尸体已经死亡,是没有呼吸了?还是没有心跳了?还是没有脑电波?

引用计数法

引用计数法比较简单。就是给每一个对象保存一个计数器,当有地方引用这个对象的时候,这个计数器就加1;反之,当有一个引用失效的时候就减1。接着上面的比喻,假如火葬场判断尸体有没有死亡,是通过观看有没有人拉着尸体的手来决定的。如果有人拉着尸体的手,表示这个尸体还有用,不能立马焚烧。如果没有人拉着尸体手,表示这个尸体已经毫无用处,在这个世界上已经没有任何留恋了,可以安心的离开了这个世界了,所以可以垃圾焚烧。这本来也没有什么问题,直到有一天,送来两具手拉着手的实体。

所以说,尽管引用计数法在判断对象是否死亡的问题上简单粗暴,但是也正是由于它简单粗暴的原因。它无法解决对象互相引用的问题,也就是说两个都已经死亡的对象互相引用,导致计算器始终无法是0,以至于这两个对象一直无法被回收。这就是我们常说的,对于引用计算法导致的循环引用的问题。

2.可达性分析算法

由于引用计算法存在循环引用的问题,所以主流的商用语言都是毅然决然地抛弃上面的这种方法,而选择了可达性分析来判断对象是否存活。这个算法名字听起来挺唬人的, 其实基本的思想就是通过一系列被称之为“GC Roots”的对象作为起始点,向下搜索,搜索所走过的路径称之为引用链(reference Chain),当一个对象没有在任何一个GC Roots的引用链上时,我们就说这个对象是不可用的,也就是死亡的。

什么意思呢?还是用上面的例子,引用计算法是看所有的尸体手有没有被人拉着。可达性分析算法是看活着的人有没有拉着尸体。也就是说,找一些肯定是活人的人,通过这些活人去证明其他人是不是活着。就好像六度空间理论一样,这个世界上所有活着人可以组成一个关系网,就像你和你老婆有关系,你老婆又和隔壁老王有关系,那间接的你和隔壁老王就产生了一个关系。而那些通过这种我认识你你认识他也无法找到的人,也就是说,不在这个世界上任何一个关系网的人,我们就可以当他已经是一个死人了。

两种判断对象是否死亡的方法其实最大的不同是:引用计数从所有对象查找有没有被引用,这里并没有区分活着的还是死亡的对象,所以它无法区别两个死亡的对象相互引用的问题;而可达性分析算法是从一定活着的对象分析,只要通过活着的对象能够到达的对象,我们就可以当做的活着的对象。那些不可达的,就可以判断他寿终正寝了。这种可以避免循环引用的问题。

所以,现在主流的算法都是使用的可达性分析算法,在java语言中可以被当做GC Roots的对象包括以下几种:

虚拟机栈中引用的对象(栈中的本地变量表)

方法区中类静态属性引用的对象

方法区中常用引用的对象

本地方法栈中JNI应用的对象(即一般说的Native对象)



生存还是死亡这是一个问题

对于引用这个概念,在实际的过程中,并不是那么纯粹。按照实际的使用中,有些对象虽然被引用了,但是其实他们没有太多的作用,是可以被回收的。这些对象就像是鸡肋,食之无味,但是弃之可惜。所以,对于这些对象,我们希望他在内存紧张的时候可以被回收,但是在内存富裕的情况下,我们姑且暂时不处理他,对于这样的需求,JDK给引用又分了好多的类型如下:

强引用:类似于“Object obj = new Object()”这种,只要引用存在,就不能被回收。

软引用:这种引用的对象是非必需的,在内存紧张的时候,即将要发生内存溢出的之前,进行回收。通过SolfReference类来实现软引用。

弱引用:比软引用还要若,这种引用只能挺过一次垃圾回收,在第二次垃圾回收的时候,回收回收。通过WeakReference类来实现弱引用

虚引用:又称之为幽灵引用或者幻影引用。这种引用对于生存回收完全没有影响,也就是说这种引用在第一次垃圾回收的时候就会被干掉。他的目的就是能在这个对象被回收的时候收到一个系统的通知。通过PhantomReference类来实现。

另外,在Object类中,也就是所有类的父类中,有一个finalize()方法。虚拟机在回收对象的时候,该对象已经没有在任何GC Root中了,虚拟机会查看该对象有没有覆盖和执行过这个方法。如果该对象没有覆盖该方法或者该对象已经覆盖并且执行过该方法,那么这个对象才会被宣告死亡。也就是说,通过这个方法,可以让对象暂且免于一死,因为我们可以在这个方法第一次执行的时候,重新把这个对象放在GC Root中,这样在下次GC的时候,该对象又出现在GC Root中,从而免于一死。等于给自己放了一块免死金牌。

再者,在方法区也会进行回收的,比如说方法区的类,在大量使用了反射、动态代理、GCLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景中,都需要虚拟机具备有类卸载的功能,以保证永久代不会溢出。满足下面三中条件的类才算是无用类:

该类的所有实例已经被回收

加载该类的ClassLoader已经被回收

该类对应的java.lang.Class对象已有在任何地方被引用,无法再任何地方通过反射访问该类。

垃圾收集算法

垃圾回收算法涉及到大量的程序细节,所以我们基本上就是简单了解一下几种算法的思想及其发展过程就行了。下面讲讲几种主流的垃圾回收的算法

标记-清除算法

基本思想就是先标记出需要清除的对象,然后统一回收所有被标记的对象。他是最基本的收集算法,因为后续的手机算法都是基于这种思想并对其不足进行改进而得到的。

这种算法主要有两个缺点“

效率不高

会产生内存碎片,内存碎片过多会导致以后在分配较大对象时,无法找到足够连续的内存单元而不但不提前出发一次垃圾回收动作。

2.复制算法

这种算法出现就是为了防止初上标记-清除算法会产生内存碎片的问题。这种算法的基本思想是把内存分为大小相等的两块内存,每次只使用其中的一块。当这一块内存使用完之后,就将还存活的对象复制到另一块上面,然后再把这块内存空间清除掉。这样每次都对整个半区进行内存回收,不会产生内存碎片。这种算法的代价上就是将内存缩小到原来的一半,代价是相当的高。

现在商业虚拟机都是采用这种收集算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%都是朝生夕死,所以并不需要按照1:1的比例来分配内存空间。可以将内存划分为一块较大的Eden空间和两块较小的survive空间,每次使用Eden和Survive中的一块。(HotSpot就是这种策略)当回收时,将Eden和Survive中还存活的对象,一次性地复制到另一块Survive空间上,最后清理掉Eden和刚才使用过的Survive空间。有的时候,当survive内存空间不足时,我们还可以依赖其他内存比如说老年代的内存空间进行担保。这样做的目的是不用额发出发一次GC。

注意:复制算法现在有两个内存区域,一个是Eden,两个是survive,这三者之间有什么关系呢?在这两个survive中,一个名字叫做to,而另一个名字叫做from。在GC开始的之前,对象只在eden和from两个内存空间中存在,而to是空的;在GC开始的时候,Eden中还存活的对象会复制到to Survive中,而from中还存活的对象按照年龄(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)来区分,达到年龄阈值的会进入到老年区;而尚不足以达到年龄阈值的对象会进入到to Survive中。这时候原来to survive 和 from survive的角色互换,也就是说之前的to survive变成了 from survive,而之前的from survive变成了to survive。GC完成的时候,Eden和现在的to survive已经清空,存在的对象都在from survive中

3.标记-整理的算法

复制算法对于存活率较低的新生代内存空间比较有效,但是对于对象存活率比较高的老年代来说,就不是很乐观了。因为复制算法要进行大量的复制操作,效率会变得非常低。所以我们在老年代使用另一种算法,就是这里要讲的标记-整理的算法。这种算法的思想就是,让已经存活的对象都向一端移动,移动完成后,直接清理掉边界之外的内存区域就好。这种算法不需要大量的复制操作,所以对象存活率比较高的老年代老说,比较合适。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jvm