一,垃圾收集的相关原理
- 垃圾回收的前提是清楚哪些内存可以被释放
- 跟进Java里JVM内存结构可以知道,对象的实例都存放在堆上,还有就是方法区的元数据,这2部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
1. 判断对象是否存活可回收
- 对对象实例的收集主要是两种基本算法
1 | 引用计数法 |
引用计数法:在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。
1
2引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引用的问题。
例如:对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是引用计数算法无法通知 GC 收集器回收它们可达性分析法: 所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
GC Roots 指虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量。即方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象,Native方法中引用的对象
2.回收方法区内存
- 方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
1 | 废弃常量 |
- 判定废弃常量:
- 只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 “bingo” 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 “bingo” 常量,也没有其它地方引用这个字面量,必要的话,”bingo”常量会被清理出常量池。
- 判定无用的类 , 判定一个类是否是“无用的类”,条件较为苛刻。
1
2
3
4该类的所有对象都已经被清除
加载该类的 ClassLoader 已经被回收
该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除
垃圾收集算法
知道如何判定无效对象、无用类、废弃常量之后,剩余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个:
1.标记清除算法: 算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点在于,垃圾被回收以后造成了大量不连续的内存碎片。碎片太多可能会导致以后需要分配较大对象时,无法找到连续的足够内存从而频繁触发垃圾收集,降低系统效率。
2. 复制算法:为了解决“标记,清除”算法的问题一种被称为复制的算法出现了,它将内存平均分为两块,每次只使用其中一块,当这一块存满时触发垃圾收集,将还存活的对象复制到另一块内存,然后将这块内存清掉,这样就不会存在内存碎片的问题
缺点:内存缩小为原来的一半,浪费空间。
3.标记整理算法: 复制算法在存活对象较多的时候需要复制的操作也较多,最关键的是只能利用一半的内存,标记整理算法可以解决这个问题,标记整理算法中的标记和标记清除算法一样,要被回收的对象找出来以后让所有存活的对象向一端移动,然后将内存的剩余部分直接清理掉。
4. 分代收集算法: 分代收集算法将内存分为新生代和老年代,新生代又分为:较大的Eden区(占80%)和两块Survivor区(各占10%),刚刚创建的对象存放在新生代的Eden区。
1 | 新生代: 复制算法 。 (因为新生代对象生存时间比较短,80%都是要回收的对象,采用标记-清除算法则内存空间碎片化严重,采用复制算法可以灵活高效,且便与整理空间。) |