Java 和C++之间有一堵由内存动态分配和垃圾收集收集技术围成的“高墙”,墙外面的人想进去,墙里面的人想出来。
----周志明《深入理解Java虚拟机》
说起垃圾回收机制(Garbage Collection ,GC),大部分人都以为这项技术是伴随着Java语言产生的。
事实上,GC 比Java的历史久远的多,早在1960年MIT的Lisp是第一门使用内存动态分配和垃圾回收技术的语言。
当时的GC技术,人们就在思考三件事情:
1、哪些内存需要回收?
2、什么时候回收?
3、如何回收?
一、内存回收的区域?
- 程序计数器、本地方法栈、虚拟机栈:这几块内存都是随线程而生,随线程而灭。栈中的栈帧,随着方法的调用入栈和出栈,栈帧的内存大小在编译器就已经可知了,因此内存的分配和回收是确定的,故不需要参加GC。
- 堆:内存的回收的主力区域,对象和数组都保存在此区域。
- 方法区 :无用的类型信息以及常量都是回收的目标,但是一般进行GC的概率不大,主要是性价比不高。
二、哪些内存需要回收?
要确定哪些内存需要回收,目前有两种常用的方案
-
1、引用计数法:
给对象添加一个引用计数器,当对象被引用时,计数器的值加1;当对象引用失效时,计数器的值减1, 当计数器的值为0时,就可以回收了。 这种方法实现简单,判定效率也很高。是一个不错的算法,微软的Com技术、Python、 一些脚本语言都用此方法管理内存。 但主流的Java语言却没有使用这种方式,主要原因是它无法解决对象的相互循环引用问题。
如下图所示:
循环引用.png-
2、可达性分析:
(1)从GC Roots 作为起点,从这些节点开始向下搜索,搜索经过的链路称为引用链。不在引用链上的(图论中称为可达)都是无用的对象。 (2)可作为GC Roots 的对象: JVM栈中引用的对象(栈帧的局部变量表的对象) 类中的static引用、final引用 本地方法栈中引用的对象
可达性分析对象是否可用,如下图:
可达性分析-
3、引用类型介绍:
i、强引用:JVM即使抛出OOM也不会回收强引用指向的对象 ii、软引用:描述一些还有用但是并非必须的对象,对于软引用,JVM会在内存溢出前进行第二次回收,如果这次回收还没有足够内存,才会抛出OOM。 iii、弱引用:同样描述还有用但是非必须的对象,强度比软引用还弱,该引用指向的对象,只能存活到下次GC之前,只要发生GC,就会被回收。 iv、虚引用:最弱的引用关系,没啥用,为对象设置虚引用关联的唯一目的就是,该对象被回收的时候会收到一个系统通知。
-
4、finalize方法:
对象自救的方法,建议不要使用,在《effective java》等书籍中都不推荐使用,具体原因,读者可以自行查阅。
三、如何进行回收?
-
3.1 标记-清除算法
原理:首先将无用的内存标记出来,然后统一释放。
缺点:效率问题,标记和清除的两个过程效率都不高;同时回收后,将会产生大量不连续的内存,空间碎片太多甚至无法满足一个大对象的申请,可能导致提前进行下一次GC。
效果图如下:
-
3.2 复制算法
原理:将内存分为相等的两块,然后每次只使用其中的一块,当发生GC的时候,将有效的对象复制到另一块空白内存,然后把已使用的全部清理。
优点:简单易行,运行高效,解决了内存碎片的问题。
缺点:浪费了一半的内存空间
PS:这种回收算法目前一般应用在堆的新生代回收,当然内存比例也不是1:1,后续在介绍新生代、老年代的时候,我们会再细讲。
效果图如下:
-
3.3 标记整理算法
和标记-清除算法类似,只是在标记之后,不是清除无用对象,而是将所有存活对象,移动到一端,然后清理掉边界意外的内存区域。
优缺点:这种算法避免内存空间的浪费,同时又解决了内存碎片问题。但是由于移动对象的效率问题,所以该回收算法,一般放在老年代进行。
示意图如下:
JVM中的分代回收算法:
现代的JVM虚拟机都采用了分代回收,即分为新生代和老年代,新生代采用复制回收算法,老年代采用标记整理算法。
新生代:
- new出来的对象一般都放在新生代
- hotspot虚拟机新生代分为三块,Eden区、From Survivor、To Survivor区(其中两者相等),大小比例为8:1:1.
- 之所以新生代采用复制算法,并且这样设计大小,是有依据的,IBM研究过新生代的对象98%的对象都熬不到下一次GC,即“朝生夕死”,所以这样设计效率很高。
- 新生代发生GC(minor GC)的时候,会将Eden区存活的对象和Survivor中的对象,一起复制到另外一个Survivor中,然后清空Eden和Survivor区。
- 新生代的对象可能在多次Minor GC后移到老年代。
老年代:
老年代的对象,一般存活时间较长,因此回收效率不高,性价比不高。
老年代的对象一般来自下面几种情况:
- 1、经历多次minor GC未被回收的(一般是15次,在虚拟机中对象有个age属性,通常每经历一次minorGC,对象的age属性+1,当age大于15即放到老年第中)
- 2、new的对象过大,而新生代经历了minor GC之后,依然放不下这个对象,那么这个对象将直接被放入到老年代。
- 3、可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
- 4、如1中所述,新生代的某个age的对象,达到新生代Survivor空间一半以上时,大于或等于这个年里的这些对象将会被移到老年代中去。
新生代和老年代的内存图如下:
新生代和老年代JVM中的GC类型:
上面讲到了新生代和老年代:
这两块区域对应的GC分别为Minor GC和Major GC,这两个GC一起触发叫做Full GC(当然也有资料称Major GC 就是Full GC)
- Minor GC:新生代的GC,由于采用复制回收算法,所以速度很快。
- Major GC:老年代的GC,一般发生时都会伴随一次Minor GC,由于采用标记整理算法,而且Major GC 一般是Minor GC的10倍以上。同时,由于老年代的对象,存活时间一般较长,因此回收的性价比不高。
一般的,我们要尽量避免Major GC的发生,比如不要频繁申请大块内存,这样新生代放不下就放到了老年代,老年代内存回收,速度慢,就会导致系统卡顿,影响体验。
四、何时出发内存回收(GC)?
- GC有JVM发起,不受应用控制。即使调用system.gc()
方法也只是建议JVM进行一次GC,而不会立即进行系统的GC操作。 - GC时机:
新生代:Eden区放不下会进行一次新生代GC,即Minor GC。
老年代:老年代的内存不够的时候,会触发一次老年代GC,即Major GC,一般都会伴随一次Minor GC。 - JVM进行GC的时候,也不是随时随刻都可以进行的,一般要等到安全节点(safe point)或者安全区域(safe region)才会进行,以在特定的位置记录一些栈的信息,这一块比较复杂,就不在这里展开了。
五、垃圾回收器?
目前所有的垃圾回收器 都不能做到完全并行。至少在对象标记节点,都要暂停所有用户线程(“stop the world”)。
目前新生代的垃圾回收器有Serial、ParNew、Parallel Scavenge
,老年代的回收器有Serial old、ParNew Old、CMS
等。其中Serial 是单线程的,效率较低,后面的几个在一定程度上做到了并行,效率较高。垃圾回收器一般关注两个点吞吐量、暂停时间
。
- 吞吐量:在一定时间内,非暂停时间占的比例,吞吐量高,说明暂停时间端,处理时间长,一般服务器比较关注这些。
- 暂停时间:暂停的时间,就是GC导致的暂停时间,一般客户端应用比较关注这个。
六、JVM内存分配策略?
JVM提倡自动内存管理,最终归结为两个问题:自动分配内存以及自动回收内存。关于回收,上面我们已经将的很细致了,下面我们讲一下内存分配,这将对我们写更高效的代码提供帮助。
- 1、对象优先分配在Eden区:这一点很容易理解
- 2、大对象直接进入老年代:这个对象多大有具体参数设置,我们应该避免“朝生夕死”的大对象,比如大的数组,最糟糕的是一群大对象。当我们申请大对象的时候,即使虚拟机还有空间,也不得不提前GC以申请连续的空间来存放它们。
- 3、长期存活的对象将进入老年代:这个我们前面讲过,默认age超过15 对象将进入老年代,所以如果对象无用了,我们应该及时释放其引用。
- 4、新生代某一个年龄的对象,达到Survivor空间的一半时候,则年龄大于等于该年龄的对象将进入老年代。
- 5、空间分配担保:老年代为了减少Major GC会进行空间分配担保,以节约时间,但是这个有一定风险,具体不在此展开讲了。
参考资料:
1、JVM垃圾回收
2、jvm:停止复制、标记清除、标记整理算法
3、JVM 垃圾回收 Minor gc vs Major gc vs Full gc
4、JVM学习03-内存管理和垃圾回收04(之GC算法 垃圾收集器)
5、Major GC和Full GC的区别是什么?触发条件呢?
网友评论