略
概述
GC主要需要完成3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
对象挂了吗?
JVM首先要知道哪些内存已经不用了,即对象已死.
引用计数算法
虚拟机不用,具体实现是:每个对象有一个引用计数器,当有地方引用它的时候,计数器就加上1,引用失效时,计数器减去1,计数器为0时表示该对象没有被引用可以被回收.
但是这样有个弊端:当对象a,b互相引用,并且a,b都应该被回收时,它们如果使用引用技术法,它们都不会被回收,导致内存泄漏.
可达性分析算法
基本思想是:先找到一个使用率最高的对象作为一个”GC Roots”,以它为起点,向下搜索,搜索所走的路径被称为引用链,不在引用链的对象都被视为已死对象,可以被GC回收.
可以作为GC Roots的对象包括如下:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
解释一下引用
JDK1.2之前,引用和被引用在于reference类型的数据中存储的数值是某块内存地址和内存地址被reference类型的数值所记载,这样显得特别牵强,一个对象是否对引用有点生硬.所以在JDK1.2之后给引用增加了许多的类型,分为:
- 强引用
- 软引用
- 弱引用
- 虚引用
一个对象的自我救赎
从前,有一个叫A对象的人,在人口普查中(可达性分析算法),他掉队了没有一个人认识他,他也不认识任何人.他就会被警察(GC)第一次标记,标记的条件是:他是否有finalize()免死金牌,第一次标记代表他即将要被回收掉.如果它有免死金牌它会被放入到一个F-Queue的队列中,虚拟机会让另外一个组织来(finalize线程)处理队列中的对象,这个组织是低优先级的线程,他们去检查队列中对象的免死金牌,在检查的时候如果这个对象和引用链中的对象建立了联系,那么他就会被移出死亡的队伍,他就不用死亡了.
注意
finalize()方法是用来拖延死亡时间的方法,拖延的时期内需要建立连接才不死.
另外一个组织(finalize线程)是不会等待finalize()方法执行完成的(不耐烦的组织啊),为了避免finalize()方法太耗时导致队列不会停止不前.
一个对象的finalize()只会被执行一次,下次如果该对象再次陷入回收的边缘,他不会执行.
这个方法用的不多,大家可以忘记Java有这个方法(原谅我最后才说).
方法区的回收
方法区不好回收,效率比较低,分为常量的回收和无用类的回收.
常量的回收
例如字符串"abc"
当没有任何一个String对象叫做"abc"
时,它就会被移出常量池.
无用类的回收
需要满足三个条件:
- 该类的实例都已经被回收,Java堆中没有该类的任何实例.
- 加载类的ClassLoader已经被回收.
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射获取该类的方法.
满足以上3个条件就可以被回收了,但不一定会被回收.(不像对象一样,无用就必然被回收).
回收方法区主要用在,大量的反射,动态代理,CGLib等ByteCode框架,动态生成JSP,以及OSGi等类频繁自定义ClassLoader的场景时,需要类的卸载.
垃圾收集算法
标记-清除算法
分为标记和清除两个阶段.第一个阶段是标记,就是把已死的对象所在的内存区域进行标记,第二个阶段是清除,就是标记的内存区域的空间释放掉.
它主要有两点不足,一是:标记和清除的效率都不高,另外一个是空间的问题,标记-清除后会有大量的不连续的内存碎片,空间碎片太多会导致以后程序如果需要大块的内存区域时候,无法得到足够大小德连续内存,而触发多一次垃圾收集动作.
复制算法
为了解决效率的问题,可以使用复制算法,复制算法是先把内存分为两个相同大小的内存,一个用于存储对象,另外一个空闲着准备在垃圾收集时,先把不需要回收的内存复制到空闲内存中去,然后再把需要清除的区域完全释放干净.下次垃圾收集依次类推.
这样的算法的缺点在于,它对内存有极大的浪费,它的有效内存空间只有一半,有点太高了.
标记-整理算法
标记-整理与标记-清除类似,是先把可回收的标记起来,然后把存活对象都向一端移动,然后直接把端边界以外的内存直接清除.
分代收集算法
根据对象存活的周期不同将内存划分为几块,一般是新生代和老年代,然后根据不同的代来确定使用的收集算法.
新生代中,每次都有大量的对象死去,只有少量的存活,那么就用复制算法,只需要复制少量的内存,即可实现垃圾收集
老年代中,对象存活率比较高,那就使用标记-清理或者标记整理算法来进行回收.
HotSpot的算法实现
先不看
垃圾收集器
垃圾收集器是内存回收的具体实现.介绍7个,它们使用场景不同,作用也不同,有适用于新生代的,也后适用于老年代的,他们之间也可以配合使用.