jvm之垃圾回收算法

来源:互联网 发布:tm商标域名 编辑:程序博客网 时间:2024/06/05 21:28

假象,我们把内存想象成一个装库存的仓库,定期清理过期的货物,我们想象一下我们会怎么清理这些货物。

货物是否过期,在前一节已经说过,利用可达性分析。这里不再赘述。

最简单最省事的清理方式,就是直接把过期的货物搬走,不用管其余的货物,但是想一下,这种做法有很个缺点,就是多次清理后,存储的空间会变得凌乱,不连续。

所以我们想另外一种方法,首先也是标记所有过期的货物,此时不再直接清除货物,而是把没过期的获取都搬到仓库的一边上,然后直接把之外的所有的空间清空。

更极端一点,我们直接把仓库分成两半,每次只用一半,需要清理的时候,把没过期的货物从一半移动到另一半,直接清理这一半的空间即可。

如上就是我们生活中清理仓库的基本方法,在内存回收中,基本的原理是一样的。

第一种就是标记-清除算法,首先标记出所有需要回收的对象,然后统一回收,缺点就是内存空间会变得不连续,当有大对象需要分配时,会触发另一次垃圾回收。

基于改进,有了第二种算法,标记-整理算法,首先一样标记出所有的需要回收的对象,然后将存活的对象移动到内存的一端,将端边界外的内存直接清除,但是这种移动中,我们还需要注意把端这边的可回收对象移动出去。

所以,在极端情况下,直接把内存分为两块,每次只使用一块,回收时,把存活的对象复制到另一块内存中,直接清理刚使用的空间,叫做复制算法。代价就是减少了一半的空间使用。但是一般情况下,我们创建的对象都是“朝生夕死”,所以我们不需要把内存按照1:1分为两等快,我们首先分配一大块内存区域eden空间用于创建对象,再将剩余的一小块空间分为两等块survivor空间。创建对象分配在eden空间上,垃圾回收时,将eden和survivor的一块空间的存活对象分配到另一个survivor空间上,然后清理eden和刚使用的survivor空间即可。一般在hotspot虚拟机中,eden和survivor空间的比例式8:1,即eden占总空间的80%,survivor的两个空间各站10%,也说明内存中每次使用的空间是90%,只有10%的浪费。同样也说明了每次回收后都只有10%以下的对象存活,但是这无法保证,所以当surivivor空间不够时,需要分配到其他的空间上。

现在的垃圾回收一般都采用分代算法,根据对象的存货周期,将对象划分为几个区域,分别采用合适的算法进行回收。一般分为新生代和老年代,新生代对象存活较少,采用复制算法,老年代存活对象较多,使用标记清除或者标记整理算法。


HotSpot中算法实现

首先得可达性分析,一般可以作为gc root的节点主要是全局性引用(常量或者类静态属性)和执行上下文(栈帧中的局部变量表),现在的应用一般方法区就要几百兆,如果要全遍历一遍过于耗时。

并且可达性分析还会产生stop the world现象,在进行分析时必须要保证当前的内存中引用不再发生变化,如果变化无法保证分析的准确性。

在hotspot虚拟机中,由于是完全准确式虚拟机,所以不需要扫描所有的全局变量和上下文,使用一组成为OopMap的数据结构来存储对象上什么位置是什么类型的数据,当进行可达性分析时,只需要扫描OopMap即可,这里也就引出了另一个问题,什么是完全准确式虚拟机。

在虚拟机中,代码编译之后是没有类型信息的,只有在栈中的位置,所以早期虚拟机是不存储栈中数据的类型信息的,在GC时,从栈帧中开始扫描,每遇到一个整形,就需要判断是否是指针,这里判断使用堆的界限、长度是否对齐等,但是有的对象就会产生已经死亡,但是好像还有指向他的引用导致无法回收。并且对象无法进行移动,移动对象就需要移动指针,所以采用了句柄的方式,增加了中间层,移动的时候只改变句柄中的指针即可。这种早期的方式效果不好。

所以在新的虚拟机中,会在类加载完成的时候,计算出来对象内哪些位置的偏移量是什么类型的数据保存到OopMap中,在JIT编译过程中,会在特定的位置计算保存。因为导致OopMap变化的指令很多,如果每一条指令都计算并生成OopMap,效率和存储空间都是问题,所以一般只在特定的位置进行计算,这些位置成为安全点(safepoint)。安全点的选取满足“是否具有让程序长时间执行”的特点,因为指令执行时间都很短,所以长时间执行的指令一般有这些位置:

1.循环结束位置

2.方法条用之前或者方法临返回前

3.异常的跳转等

安全点还有一个问题,就是在进行GC时,如何让所有的线程都进入安全点再停顿下来,有两种方式:抢先式中断和主动式中断;

抢先式中断是不需要线程的配合,在进行gc时,停止所有线程,判断线程是否在安全点,把不在安全点的线程恢复执行到安全点再停止,现在几乎没有使用这种方式的。

主动式中断是需要GC时,不主动地中断线程,而是设置一个标志,线程会轮询这个标志,发现标志位真时,中断线程。轮询的地方与安全点重合,再加上对象创建的位置,这样就保证了线程中断时都会进入安全点。

但是还有一种情况,如果在GC发生时,一个线程刚好没有分配CPU时间,处于sleep或者阻塞状态,这时无法响应中断请求,进入到安全点再中断。所以引入了安全区域的概念,安全区域就是一段代码中,引用的关系不会发生变化,这个区域内任何一个地方的GC都是安全的。在线程进入安全区域时,先标识自己进入了安全区域,在gvm进行gc时,不会再管这些进入安全区域的线程了。


以上就是垃圾收集算法,和hotspot中可达性分析过程。

接下来就是垃圾收集器。

简单总结下:

垃圾回收需要判断对象是否存活,一般用可达性分析,一般能作为gc root的有栈帧的局部变量表和全局变量,但是如果遍历所有的gc root会很耗时,所以引入了OopMap存储对象中哪些位置是哪些类型的变量,在可达性分析时遍历OopMap即可。生成OopMap的时间点有类加载完成时,进行计算。还有JIT编译过程中,让程序长时间执行的地方进行计算。这些地方称为安全点,在进行gc时,线程都需要进入安全点,此时遍历OopMap后可达性分析才是准确的。让线程进入安全点的方法是设置一个标志位,线程在安全的地方查询这个标志位,如果为真,则中断,等待GC。

垃圾回收的方法有标记清除法,标记整理法,复制算法。各有优缺点,根据对象的存活周期,一般将内存区域分代,年轻带和老年代,年轻代用复制算法,老年代用标记清除或者标记整理算法。

以上就是最开始三个问题的答案

1.哪些对象需要回收:可达性分析

2.什么时候进行GC:安全点和安全区域

3.怎么进行回收:回收的算法


明天学习垃圾收集器。