1、基本介绍:【Garbage Collection】
- 在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。
- 它让开发者无需关注空间的创建和释放,而是以守护进程的形式在后台自动回收垃圾。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。
- 它针对的是堆内存中的内存空间。
2、如何确定一个对象是垃圾:【垃圾就是所有不再存活的对象】
2.1、引用计数法:【java没有采用】
- 在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。
- 这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。
//对象循环引用的问题:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
class MyObject {
public Object object = null;
}
//将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
复制代码
2.2、可达性分析法:【java中采用】
- 基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
- 通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。
- 被标记为不可达的对象会立即被垃圾收集器回收吗?很显然是不会的,被标记为不可达的对象会进入收集阶段,这时会执行该对象重写的finalize方法,如果没有重写finalize方法或者finalize方法中没有重新与一个可达的对象进行关联才会进入终结阶段,并最终被回收。
2.2.1、GC Roots讲解:
- GC Roots 本身一定是可达的,这样从它们出发遍历到的对象才能保证一定可达。
- 每次垃圾回收器会从这些GC Roots根结点开始遍历寻找所有可达节点,所有 GC Roots 不可达的对象都称为垃圾。
- 能作为GC Root的对象必定为可以存活的对象,比如全局性的引用(静态变量和常量)以及某些方法的局部变量(栈帧中的本地变量表)。
- java主要有以下4种GC Roots对象: * 虚拟机栈(帧栈中的本地变量表)中引用的对象。 * 方法区中静态属性引用的对象。 * 方法区中常量引用的对象。 * 本地方法栈中 JNI 引用的对象。 * 方法区中运行时常量池引用的对象。
2.3、java中将对象判定为可回收对象的情况:
- 显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象:
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
复制代码
- 局部引用所指向的对象:
//这是一个方法,循环每执行完一次,生成的Object对象都会成为可回收的对象。
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}
复制代码
- 只有弱引用与其关联的对象:
3、垃圾回收算法:【垃圾收集器所使用的回收垃圾的算法】
3.1、Mark-Sweep(标记-清除)算法:
- 分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
- 缺点:标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
3.2、Mark-Compact(标记-整理)算法:
- 该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的可回收内存。
- 适合存活对象多,垃圾少 的情况,它只需要清理掉少量的垃圾,然后挪动下存活对象就可以了。
3.3、Copying(复制)算法:
- 为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
- 适合 存活对象少,垃圾多 的情况,这样在复制时就不需要复制多少对象过去,多数垃圾直接被清空处理。
- 缺点:
- 这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
- 而且Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
3.4、Generational Collection(分代收集)算法【java采用】:
3.4.1、Java 的堆结构:
- 一块 Java 堆空间一般分成两部分:
- 刚刚创建的对象:在代码运行时会持续不断地创造新的对象,这些新创建的对象会被统一放在一起。因为有很多局部变量等在新创建后很快会变成 不可达 的对象,快速死去 ,因此这块区域的特点是 存活对象少,垃圾多 。形象点描述这块区域为: 新生代;
- 存活了一段时间的对象:这些对象早早就被创建了,而且一直活了下来。我们把这些 存活时间较长 的对象放在一起,它们的特点是 存活对象多,垃圾少 。形象点描述这块区域为: 老年代;
- 也就是说,常规的 Java 堆至少包括了 新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:
- 新生代:存活对象少、垃圾多
- 老年代:存活对象多、垃圾少
3.4.2、新生代-复制 回收机制:
思路:把内存按 8:1:1 分
复制代码
- 9:1 的内存划分有可能把年轻对象放到 老年区 ,那就换成 8:1:1,依次取名为 Eden['i:dn]、SurvivorA [sə'vaɪvə(r)] 、Survivor B 区,其中 Eden 意为伊甸园,形容有很多新生对象在里面创建;Survivor区则为幸存者,即经历 GC 后仍然存活下来的对象。
- 工作原理如下:
- 首先,Eden区最大,对外提供堆内存。当 Eden 区快要满了,则进行 Minor GC,把存活对象放入 Survivor A 区,清空 Eden 区;
- Eden区被清空后,继续对外提供堆内存;
- 当 Eden 区再次被填满,此时对 Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,同时清空 Eden 区和Survivor A 区;
- Eden区继续对外提供堆内存,并重复上述过程,即在 Eden 区填满后,把 Eden 区和某个 Survivor 区的存活对象放到另一个 Survivor 区;
- 当某个 Survivor 区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15 次左右时,则把这部分剩余对象放到Old老年代 区;
- 当 Old 区也被填满时,进行 Major GC,对 Old 区进行垃圾回收。
- 注意:在真实的 JVM 环境里,可以通过参数 SurvivorRatio 手动配置 Eden 区和单个 Survivor 区的比例,默认为 8:1:1。
3.4.3、老年代-标记整理 回收机制:
- 老年代一般存放的是存活时间较久的对象,所以每一次 GC 时,存活对象比较较大,也就是说每次只有少部分对象被回收。
- 选择 存活对象多,垃圾少 的标记整理 回收机制,仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。
注意:
- 在堆区之外【JVM的方法区里】还有一个永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
- 当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。以常量池中字面量的回收为例,若字符串“abc”已经进入常量池中,但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该“abc”就会被系统清理出常量池。
- 方法区中的类需要同时满足以下三个条件才能被标记为无用的类:
- Java堆中不存在该类的任何实例对象;
- 加载该类的类加载器已经被回收;
- 该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法。
- 虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样(不使用了就必然回收)。
4、3种垃圾回收的区别:
4.1、Minor GC:[ˈmaɪnə(r)]
- 从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
- 当Eden区满时,触发Minor GC。
4.2、Major GC:[ˈmeɪdʒə(r)]
- Major GC 是清理老年代。
- Major GC速度一般会比Minor GC速度慢10倍以上!
4.3、Full GC:
- 清理整个堆空间:包括新生代和老年代。
- 触发Full GC:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
5、Java对象在虚拟机中的生命周期:
-
创建阶段(Created):
- 为对象分配存储空间。
- 构造对象。
- 从超类到子类对static成员进行初始化。
- 递归调用超类的构造方法。
- 调用子类的构造方法。
-
应用阶段(In Use):
- 当对象被创建,并分配给变量赋值,状态就切换到了应用阶段。
- 这一阶段的对象至少要具有一个强引用,或者显式的使用软引用、弱引用或者虚引用。
-
不可见阶段(Invisible):
- 程序中找不到对象的任何强引用,比如程序的执行已经超出了该对象的作用域。在不可见阶段,对象仍可能被特殊的强引用GC Roots持有着,比如对象被本地方法栈中JNI引用或是被运行中的线程引用等。
-
不可达阶段(Unreachable):
- 程序中找不到对象的任何强引用,并且垃圾收集器发现对象不可达。
-
收集阶段(Collected):
- 垃圾收集器已经发现对象不可达,并且垃圾收集器已经准备好要对该对象的内存空间重新进行分配时。这个时候如果该对象重写了finalize方法,则会调用该方法。
-
终结阶段(Finalized):
- 当对象执行完finalize法后仍然处于不可达状态时,或者对象没有重写finalize方法,则该对象进入终结阶段,并等待垃圾收集器回收该对象空间。
- JVM中真正将一个对象判***至少需要经历两次标记过程:
- 第一个过程,就是我们所熟知的可达性分析。
- 第二个过程,就是判断这个对象是否需要执行finalize()方法。
-
对象空间重新分配阶段(Deallocated):
- 当垃圾收集器对对象的内存空间进行回收或者再分配时,这个对象就会彻底消失。