GC算法是作为一个java程序猿所必须了解的东西,不仅在日常生活中有助于我们更深入的理解代码,也是在面试中必定会考的东西。
一:标记-清除算法
标记-清除算法作为最基础的收集算法,其分为“标记”与“清除”两个阶段,当触发GC时,jvm会暂停所有工作,第一阶段阶段会通过标记算法标记出所有需回收的对象,在之后的第二阶段回收所有被标记的对象。
市场上主流的标记算法都是可达性分析算法
。这个算法的基本思路就是通过一些称之为“GC Roots”的根对象作为起始点,从这些节点往下搜索,搜索路径形成一条引用链(如下图),当一个对象到任意“GC Roots”对象都没有引用链可连通的话,则表明这个对象是不可达的,即这个对象会被标记为可回收的。
上图中的5/6/7对象是没有被任何“GC Roots”引用的,就比如我们一些方法中定义的局部对象变量,在堆中是有对象的,但是在方法执行完后引用已经出栈了,所以没有任何引用去指向他,所以这个对象则是不可达的,将会被标记为可回收。
这里补充下java中可以作为“GC Roots”的对象存在:
- 虚拟机栈中引用的对象
- 常量引用的对象
- 静态属性引用的对象
- 本地方法栈中JNI引用的对象
上面已经介绍了标记算法,下面来看清除过程的图解,便于我们理解:
假如当前内存使用状况如图1,红色为未被标记为可回收的对象,黑色为标记为可回收的对象,白色为未使用内存区域,清除算法会回收黑色部分内存,则回收后内存使用状况如图2。这就是标记清除算法。
标记-清除算法的缺点:
- 最致命的缺点:效率不高。在标记-清除两个阶段都需要停止程序的运行,且两个步骤效率都不高。试想一下,假如你的网站应用每运行一个小时都要停止10分钟来做垃圾回收,用户体验是何等的不好。
- 如上图可以看到,清除后会留下很多小片内存,运行久了都是这种碎片内存的话,在有时我们new一个大对象时,可能会找不到连续的内存空间去分配给对象。
二:复制算法
为了解决效率问题,“复制”算法应运而生,它将可用内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完时则停止程序,然后将还存活的对象复制到另一块内存中,再把当前这块内存空间全部清理掉。如下图:
上图灰色区域表示当前未使用的一半内存,图2可以看到存活的对象被拷贝到之前未使用的一块内存区域中。
复制算法的优缺点
- 缺点:牺牲一半内存
- 优点:如果对象存活率低则运行非常高效,且内存连续性高
三:标记-整理算法
上面说了复制算法在对象存活率高时则会效率低下,因为每一次复制都会复制大量对象,且会浪费50%的内存,所以有人提出了“标记-整理”算法。标记-整理算法的思路为当内存不足时停止程序,之后触发标记动作,然后将存活的对象全部向一端移动,形成连续的空间,后续空间则可继续使用。如下图:
如上图所有存活对象全部前移,后续空间都为可使用空间。
标记-整理算法优点:
- 当存活对象多时,效率高于复制算法
- 不会造成空间浪费
四:分代收集算法
首先我们来看对象分类:
- 新生代对象。新生代对象98%以上都是朝生夕死对象,在一次GC中就会被回收,比如方法中的局部变量等
- 年老代对象。年老代往往会存在较长时间,经过很多轮GC都不会被回收,比如数据库连接池对象
- 永久代对象。永久代对象几乎不会不会被回收,比如常量池中的对象。
鉴于对象以及各个算法的优缺点,分代收集算法
应运而生,分代收集算法将内存分为新生代、年老代和永久代,我们都知道新生代都是一些“朝生夕死”的对象,其中百分之98%以上的对象都会在一次GC中被回收,所以我们将内存按8:1:1的比例分为一块较大的Eden空间和两块较小的Survivor空间,每次都使用Eden和一块Survivor空间,当空间满时,采用复制算法
将当前使用的Eden和Survivor对象复制到另一块Survivor空间中,之后清除当前Eden和当前Survivor空间的内存以完成内存回收。针对老年代和永久代这种存活率很高的区域,则采用标记-整理算法`,以获得更快的效率和连续的内存空间。
总结:
- 新生代都是朝生夕死的对象,则采用“复制算法”,高效且只浪费10%的空间
- 年老代对象存活率高,则采用“标记-整理算法”,高效且获得连续空间
欢迎关注我的博客:blog.scarlettbai.com