个人认为在学习垃圾回收算法及垃圾回收器之前应该对jvm的内存模型有足够的了解!
一,垃圾回收概述
在java中,我们不需要手动释放对象的内存,由jvm的垃圾回收线程自动对没有引用的对象进行回收—>自动的回收垃圾
创建对象时,gc开始监控这个对象的地址、大小以及使用情况。
gc采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当gc确定一些对象为"不可达"时,gc就有责任回收这些内存空间 —>可达性分析算法
对垃圾回收的剖析从这几个问题来开始:
哪些内存需要回收或者哪些是垃圾需要被回收?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
为什么要收回这些垃圾?
不进行垃圾回收,内存会很快消耗完。进行垃圾回收,释放内存空间,还会对内存碎片进行整理。
如果不进行垃圾回收就无法保证用户线程的正常运行
java的垃圾回收机制?
java自动内存分配,自动内存管理机制,无需像c 那样需要手动分配内存和手动内存回收
垃圾回收的具体区域?
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收,其中,java堆是垃圾收集器的工作重点
口诀是: 频繁回收新生代,偶尔收集老年代,基本不收集方法区(metaspace)
二,垃圾回收算法
标记阶段
在堆中几乎存放在所有的java对象实例,在gc执行垃圾回收之前,首先需要区分哪些对象是存活的,哪些对象是已经死了的.所以就有了垃圾回收的标记阶段:
简单一句话,标记的目的是判断对象是否存活; 一般常用的俩种方式 引用计数算法和可达性分析算法
1.标记阶段:引用计数算法
每个对象保存整形的引用计数属性,有引用指向对象 引用计数器加一,引用指向对象消失 引用计数器减一。计数器为零时,进行垃圾回收
优点:实现简单、垃圾对象易识别、判定效率高、回收没有延迟性
缺点: 无法解决对象循环引用问题,也是java不采用此算法的原因,如图所示:
2.标记阶段 :可达性分析算法
根对象集合为起点,向下搜索直接或间接连接的对象为存活对象,存活的对象都会直接或者间接的相连,搜索走过的路径称为引用链(reference chain),只有被引用链相连的才是存活的,是可达的,不相连的就是需要被回收的对象.
解决了对象循环依赖问题 如图:;
gc roots可以是哪些元素?
- 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内jni(通常说的本地方法)引用的对象方法区中类静态属性引用的对象,比如:java类的引用类型静态变量
- 方法区中常量引用的对象,比如:字符串常量池(stringtable)里的引用
- 所有被同步锁synchronized持有的对象
- java虚拟机内部的引用。
- 基本数据类型对应的class对象,一些常驻的异常对象(如:nullpointerexception、outofmemoryerror),系统类加载器。
- 反映java虚拟机内部情况的jmxbean、jvmti中注册的回调、本地代码缓存等。
总结一句话就是,除了堆空间外的一些结构,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为gc roots进行可达性分析;
除了这些固定的gc roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整gc roots集合。比如:分代收集和局部回收(partialgc)。
如果只针对java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区
可达性分析算法的注意事项:
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致gc进行时必须“stop the world”的一个重要原因。即使是号称(几乎)不会发生停顿的cms收集器中,枚举根节点时也是必须要停顿的。
清除阶段
清除阶段:当成功区分出内存中存活对象和死亡对象后,gc接下来的任务是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在jvm中比较常见的三种垃圾收集算法标记-清除算法(mark-sweep),复制算法(copying),标记-压缩算法(mark-compact)
清除阶段:标记-清除算法(mark-sweep)
执行过程:
当堆中的有效内存空间(available memory)被耗尽时,就会停止整个程序(stop the world),然后进行两项工作,标记、
标记:collector(垃圾回收器)引用根节点开始遍历,标记所有被引用路径可达的对象,标记就是可达性分析算法执行的过程。
清除: collector对堆内存从头到尾进行线性遍历(注意的是线性的),如果发现某个对象在其header中没有标记为可达对象,则将其回收
注意这里的清除:这里清除并不是真正意义上的置空,而是把需要清除的对象地址保存在空闲的地址列表(空闲列表)里,下次为新对象分配内存时,判断垃圾的位置空间是否够新对象的分配,够就直接覆盖原来垃圾的对象.
标记-清除算法缺点:
- 效率不高,可达性分析需要对内存空间遍历耗时,清除阶段对堆内存从头到尾进行线性遍历,更耗时
- 在进行gc时,需要停止整个应用程序,用户体验差(stw --stop the world
- 空闲内存不连续,产生内碎片
- 需要额外维护一个空闲列表
清除阶段:复制算法(copying)
算法思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收,如图所示:
此过程正对应新生代的ygc edn区和from区 到to区是复制算法
复制算法的优点:
没有标记和清除过程,实现简单,运行高效;复制过去以后保证空间的连续性,不会出现“碎片”问题。
复制算法的缺点:
需要两倍内存空间;对于g1这种分拆成大量region(分区算法)的gc,复制而不是移动,意味着gc需要维护region之间对象引用关系,内存占用、时间开销都很大
清除阶段:标记-压缩算法(mark-compact)
思想:第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象,第二阶段将所有的存活对象压缩到内存一端,按顺序排放。之后,清理边界外所有空间。
标记-清除算法与标记-压缩算法的本质区别是标记-清除算法是一种非移动式的回收 算法,标记-压缩算法是一种移动式的,标记的存活对象将会被整理,按照内存地址依次排序,而未被标记的内存将被清理,当我们需要给新对象分配内存时,只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少许多开销
标记-压缩算法优点:
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
标记-压缩算法缺点:
从效率上来说,标记-整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动过程中,需要全程暂停用户应用程序。即:stw
小结:
分代收集算法
思想:根据jvm内存的分代分区,进行分代收集
年轻代(young gen):
区域相对老年代较小,对象生命周期短、存活率低,回收频繁,.
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(tenured gen):
区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
mark阶段的开销与存活对象的数量成正比。
sweep阶段的开销与所管理区域的大小成正相关。
compact阶段的开销与存活对象的数据成正比。
增量收集算法
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the world的状态。在stop the world状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成
如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(incremental collecting)算法的诞生。
基本思想 :如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
增量收集算法的缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
吞吐量=程序运行时间/(程序运行时间 gc时间)
分区算法
一般来说,在相同条件下,堆空间越大,一次gc时所需要的时间就越长,有关gc产生的停顿也越长。
为了更好地控制gc产生的停顿时间,将一块大的内存区域分割成多个小块(region),根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次gc所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。