1.需要回收的地方
程序计数器,虚拟机栈,本地方法栈随线程而生而灭,栈中的栈帧随方法执行而进栈出栈,栈帧的内存分配在类结构确定以后也是已知的。故这几个区域不需要过多考虑回收问题。
java堆与方法区则不一样,一个接口的多个实现类的内存可能不一样,一个方法多个分支需要内存也不一样,要在运行期才能会创建那些对象,这部分内存的分配与回收是动态的。
2.对象的死亡判定
-
程序计数器:
每个对象都加一个程序计数器,在被引用后,计数器加1,当计数器为0时表示无地方在使用这个对象。
这个方法实现简单,判定效率高,但在两个对象相互引用且均不被其他地方引用时,其都为死亡状态,但由于相互引用的原因,其程序计数器都不为0,导致无法回收。 -
可达性分析:利用一系列GC roots作为根节点与其他对象建立联系,只要从任意GC roots能到达的对象,都称为可达的,若某个对象与任意一个GC roots都是不可达的,则称此对象是不可用的,是可回收的对象。
可作为GC roots的对象:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
-
java中一般用引用来指明对象,jdk1.2以后,java把引用分为四类:
- 强引用:垃圾收集器永远都不会回收的对象,一般用于程序代码中普遍存在的引用。
- 软引用:描述一些还有用但非必需的对象,在发生内存溢出异常之前,将会吧这些对象列入回收范围进行第二次回收
- 弱引用:描述非必需对象,无论是否内存紧张,都会在下一次GC时被回收
- 虚引用:没有强度的等级,完全不会对生存时间构成影响。(虚引用甚至无法用来取得一个对象实例,设立虚引用的目的是用来在对象被回收时接收一个系统通知)
-
finalize方法
对象的回收有两个标记阶段: -
当第一次检测到对象没有GC roots可达时进行第一次标记,并判断是否要执行finalize方法
- 若对象没有覆盖finalize方法或者已经执行过finalize方法则不执行
- 否则执行
-
执行finalize方法的对象有可能被拯救,它会被放入F-Queue队列,并由一个虚拟机建立的低优先级的Finalizer去执行(触发对象的finalize方法),若对象在此期间重新与某个GC roots可达,则会被拯救。
-
第二次GC时,带第一次标记,且未被拯救的对象(未执行finalize方法或执行了但没有与GC roots建立连接的对象)会被真的回收。
-
每个对象的finalize方法只能执行一次
-
该方法执行代价大,一般不使用。
-
回收方法区
-
方法区中可以不进行垃圾回收,并且垃圾回收效率极低,代价又高。回收一个字符常量,要保证没有任何一个String对象是这个常量,且没有其他地方引用这个常量,那么这个常量可以被回收,并清除出常量池。
-
而判断一个类是否为“无用的类”的条件就更苛刻。满足下面件也是可以进行回收,仅仅是可以
- 此类所有实例都会回收(堆中不存在这个类的实例)。
- 加载这个类的ClassLoader已经被回收
- 此类的class对象未在其他地方被引用,无法通过反射访问此类的方法
-
3.垃圾收集算法
- 标记-清除算法:最基础的算法,首先标记处要回收的对象,在标记完后在统一回收这些对象。但标记和清除两个过程的效率都不高,且这样回收以后,可用空间是大量不连续的碎片。
- 复制算法:将空间分为两半,一半用完以后,将存活对象一起复制到另一边,每次只使用一边。这样做实现简单,运行高效,但代价是牺牲一半的空间。
由于新生代98%都是“朝生夕死“,寿命很短,所以1:1来划分空间不合理,目前使用的是8:1:1划分为三块,分别是一个Eden空间(8)和两个survivor空间(1),每次使用Eden空间和一个survivor空间(s1),回收将少量的存活新生代复制到未使用的survivor(s2)上,然后再把survivor(s1)和Eden整体回收,将survivor(s2)结合Eden来使用,用survivor(s1)来接收下一次的存活对象。但为了保证空间合理(如本次存活对象大于10%,备份的survivor空间不够用),就使用其他内存(老年代)进行分配担保(多的丢到这里) - 标记,整理算法:这种算法,先标记要回收的对象,然后整理时,把未标记对象往前移动,使得可用空间连续
- 分代收集算法:集合前几种算法,将对象分代,内存分块,不同的代在不同的区域,新时代存活率低使用复制算法,老年代存活率高使用标记,整理算法