文章目录
1 什么是垃圾?
-
垃圾是指在运行程序中没有任何指针指向的对象
-
如果不及时对内存中垃圾进行清理,这些垃圾对象所占的内存空间会一直保留到应用程序结束,甚至会导致内存溢出。
2 为什么需要gc
-
对于高级语言来说,如果不进行垃圾回收,内存迟早都会被消耗完
-
除了释放没用的对象,垃圾回收还可以清楚内存里的记录碎片,以便jvm将整理出的内存分配给新的对象
-
业务越来越复杂,没有gc就不能保证应用程序的正常进行
-
垃圾回收器只回收方法区与堆区
-
java堆是垃圾收集器的工作重点
-
从次数上讲
- 频繁收集young区
- 较少收集old区
- 基本不懂perm(方法)区
3 垃圾回收算法
3.1 标记阶段
-
堆里存放着许多java对象实例,在进行gc之前,需要区分出内存中那些对象是存活对象,那些是已经死亡对象。只有标记为已经死亡的对象,gc才会执行垃圾回收,因此这个过程我们称为 垃圾标记阶段
-
当一个对象不再被任何存活对象继续引用时,就可以宣判死亡了。
-
判断对象存活的一般有两种方式:引用计数算法与可达性分析算法·
3.1.1 引用计数算法
-
引用计数算法,对每一个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况
-
对于一个对象a,只要有任何一个对象引用了a,则a的引用计数器就加1,当引用失效时,引用计数器就减1,只要对象a的引用计数器的值为0,即表示对象a不可能再被使用,可进行回收
优点:
** 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟**
缺点:
-
它需要单独的字段存储计数器,这样增加了 存储空间的开销。
-
每次赋值都需要更新时间器 增加了时间开销
-
有一个严重的问题,即无法处理循环引用的情况,这导致java垃圾回收器中没有使用这类的算法。
3.1.2 可达性分析算法
-
相对于引用计数算法,可达性算法具备简单和执行高校特点,该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄露发生
-
所谓gc roots 根集合就是一组必须活跃的引用。
-
基本思路:
-
可达性分析算法是以根对象集合为起点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
-
使用可达型分析算法后,内存的存活对象都会被根对象集合直接或间接连接着,搜索走过的路称为引用链
-
如果目标对象没有任何引用链相连,则是不可达的,就意味着对象已经死亡,
-
在可达性分析中,只有能够被跟对象集合直接或者间接连接的对象才是存活对象。
GC root主要包括以下几类元素:
-
虚拟机栈中引用的对象
比如:各个线程被调用的方法使用的参数,局部变量等。 -
本地方法内的jni引用的对象
-
方法区中类静态属性引用的对象
比如:java类的引用类型静态变量 -
方法区中常量引用的对象
比如:字符串常量池(string table)里的引用 -
所有被同步所synchro你这的持有的对象
-
除了这些固定的gc roots 集合以外根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性地加入,共同构成完整Gc ROOTS 集合。 如:分代收集合局部回收
- 如果只针对java堆中的某一块区域进行垃圾回收,必须考虑到内存区域是虚拟机自己实现细节更不是孤立封闭的,这个区域的对象完全有可能被其他区域对象所引用,这时就需要将关联的区域对象也加入gc roots 集合中去考虑,才能保证可达性分析准确性。
注意:
-
如果使用可达性分析算法判断是否可回收,那么分析工作必须在一个保证一致性的快照中进行。
-
这点也是gc 进行时必须进行“stop the world”的一个重要性原因,即时是不会停顿的cms收集器,枚举根节点时也是必须要停顿的
3.2 对象的finalization 机制
-
java语言提供了对象终止机制来允许开发人员提供 对象被销毁之前的自定义处理逻辑
-
垃圾回收此对象之前,总会先调用这个对象的finalize()方法
-
finallize()方法允许在子类被重写,用于在对象被回收时进行资源释放通常在这个方法中进行资源释放和清理的工作
-
永远不要主动调用某对象的finalize()的方法,应该交给垃圾回收机制调用。
-
在finalize()时可能会导致对象复活.
-
finalize()方法执行时间没有保障,完全有gc线程决定,在一些情况下,若不发生gc,则finalize方法将没有执行机会。
-
一个糟糕的finalize会严重影响gc性能
-
由于finalize()方法存在,虚拟机对象一般处于三种可能
-
可触及的:从根节点开始,可以到达这个对象。
-
可复活的:对象的所有引用都被释放,但是对象有可能在finalize中复活
-
不可触及的:对象的finalize被调用,并且没有复活。不可触及对象不可能被复活,因为finalize()只会被调用一次
以上的三种状态,由于finalize方法的存在,进行的区分,只有对象在不可触及时才可以被回收。
3.2 清除阶段
当成功区分出内存中存活对象和死亡对象后,gc接下来的任务就是执行回收。
目前在jvm中比较常见的三种垃圾收集算法是标记-清除算法(mark-sweep),复制算法(copying),标记-压缩算法(mark-compact)
3.2.1 标记-清除算法
当堆中的有效内存空间被耗尽时,就会停止整个程序(stw),然后进行两项工作,标记和清除。
-
标记:从根节点开始遍历,标记被引用的对象,一般是在对象header中记录为可达对象。(标记的是可达对象)
-
清除:对堆内存从头到尾进行线性遍历,如果某个对象在header中没有标记为可达对象,则将其回收。
缺点:
-
效率不高
-
在进行gc时,需要停止整个应用程序。
-
这种方式清理出来的内存时不连续的,产生内存碎片,需要维护一个空闲列表
注意:何为清除
- 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加装时,判断垃圾的位置空间是否够,如果够就存放。
3.2.2 复制算法
将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
-
没有标记和清除过程,实现简单,运行高效
-
复制过去后保证了空间的连续性,不会出现碎片问题
缺点
-
需要两倍的内存空间
-
复制不是移动 意味着gc需要维护对象引用的关系,时间与空间的开销都不会小。
-
如果系统中的存活对象很多,复制算法不会很理想。
3.2.3 标记-压缩算法
复制算法高效是建立在存活对象少,垃圾对象多的前提下的。这种情况是在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。因此基于老年代垃圾回收的特性,需要使用其他算法
标记-清除与标记-压缩算法区别:
本质差异在于标记-清除算法是一种非移动式的回收算法,标记压缩是移动式的
优点:
-
消除了 标记-清除算法中,内存区域分散的缺点
-
消除了复制算法当中,内存减半的高额代价
缺点:
-
从效率上来说,标记-整理算法要低于复制算法
-
移动对象的同时,需要调整对象引用的地址。
3.2.3 对比三种算法
3.2.4 分代收集算法
前面的这些算法中,没有一种算法完全可以替代算法,它们都有自己优势和特点,分代收集算法应运而生。
分代收集算法,是由于不同对象的生命周期是不一样的。因此,不同生命周期对象可以采取不同收集方式,以便提高回收效率。
目前所有的gc都是采用分代收集算法执行垃圾回收的
- 年轻代
年轻代特点:区域相对老年代较小,对象生命周期短,存活率低回收频繁。(因此适用于复制算法)
- 老年代
老年代:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。(一般由标记-清除或与标记-整理混合实现)
-
mark(标记)的开销与存活对象的数量成正比
-
sweep(清除)的开销与管理区域的大小成正比
-
compact(整理)的开销与存活对象的数据成正比
3.2.5 增量收集算法
上面的算法,在垃圾回收中,软件将处于stw的状态,此时所有的线程都不会被挂起,如果垃圾回收时间过长,将严重影响用户体验或者系统的稳定性,因此实时垃圾收集算法-增量收集算法诞生
基本思想:
-
让垃圾收集线程与应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
-
增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间的冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作
缺点:
由于线程且换和上下文转换的消耗,使得垃圾回收总体成本上升,造成系统吞吐量下降。