对于从事C、 C++程序开发的开发人员来说, 在内存管理领域, 他们既是拥有最高权力的“皇帝”,又是从事最基础工作的劳动人民——既拥有每一个对象的“所有权”, 又担负着每一个对象生命从开始到终结的维护责任。
对于Java程序员来说, 在虚拟机自动内存管理机制的帮助下, 不再需要为每一个new操作去写配对的delete/free代码, 不容易出现内存泄漏和内存溢出问题, 看起来由虚拟机管理内存一切都很美好。 不过, 也正是因为Java程序员把控制内存的权力交给了Java虚拟机, 一旦出现内存泄漏和溢出方面的问题, 如果不了解虚拟机是怎样使用内存的, 那排查错误、 修正问题将会成为一项异常艰难的工作
1.为什么要垃圾回收
通过之前内存结构的知识讲解,我们知道,存储在JVM中的Java对象可以被划分为两类:
➷ 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,生命周期短的,及时回收即可。
➷ 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
存活较短的对象存放在新生代,存活较长的对象存放在老年代。
我们来一起看下如下的代码,对他们在内存中的分配做一个剖析:
public class Test {
private static User user = new User();
public static void main(String[] args) throws InterruptedException {
user.login();
for (int i = 0; i < 10; i++) {
doSomething();
Thread.sleep(2000);
}
}
public static void doSomething(){
Student stu = new Student();
stu.study();
}
}
class User{
public void login(){
System.out.println("登录");
};
}
class Student{
public void study(){
System.out.println("I'm studying");
};
}
Test类中静态成员变量 user 是长期存活的,并且分配在新生代中。
main方法中通过for循环调用了10次 doSomething() 方法,方法中会创建Student()对象。
我们先将时间定格在执行完第一次后,内存中的分配情况是:
以上仅仅是执行第一次doSomething() 方法后的情况,如果执行10次后的情况会发生什么样的变化呢?
首先大家要明确,我们的doSomething() 方法执行完后对应的栈帧肯定会弹栈,那么对应栈帧的局部变量也相应被释放回收,我们堆内存中的实例对象就会变成无引用的垃圾对象了:
当最后一次 doSomething() 方法执行完后 对应栈帧弹栈,那么堆内存中新生代里面的Student实例对象就存在了有10个对象没有地址引用,后续如果再继续产生一些垃圾对象,当新生代中的内容空间已无法分配空间的时候,就会进行“ Minor GC ”,将对应新生代的垃圾对象进行回收:
回收后的内存就仅剩User这个对象了:
当然如果我们的程序在经历了15次“ Minor GC”后还没有被回收的对象就会被放入我们的老年代进行管理;比如我们的User实例对象,因为一直被Test类静态变量引用,所以它不会被回收。
当然如果老年代里面的空间也存满了后,也会触发垃圾回收,把老年代中没用的垃圾对象进行清理。
2.大厂面试题-如何判断对象可以回收
接下来我们继续分析,当触发垃圾回收的时候,我们的JVM到底按照一个什么样的规则来回收垃圾对象。到底哪些对象可以被回收,哪些对象不能被回收。我们先来说一个可达性分析算法:
2.1可达性分析算法
当前主流的商用程序语言(Java、 C#, 上溯至古老的Lisp) 的内存管理子系统, 都是通过可达性分析(Reachability Analysis) 算法来判定对象是否存活的。 这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) , 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。
总结下就是: 每一个对象,都分析下有谁在引用他,然后一层一层往上去判断,看是否有一个GC Roots
那么有哪些是被作为是GC Root对象的?
在Java技术体系里面, 固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表) 中引用的对象, 譬如各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等。
- 在方法区中类静态属性引用的对象, 譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象, 譬如字符串常量池(String Table) 里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法) 引用的对象。
- Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
- 所有被同步锁(synchronized关键字) 持有的对象。
- 反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等
在我们刚才的代码中:
public class Test {
private static User user = new User();
public static void main(String[] args) throws InterruptedException {
user.login();
for (int i = 0; i < 10; i++) {
doSomething();
Thread.sleep(2000);
}
}
public static void doSomething(){
//根对象
Student stu = new Student();
stu.study();
}
}
局部变量stu就是一个GC Roots,这种也是最常见的一种情况。假如代码正在调用执行doSomething()方法,这时局部变量stu持有实例对象,新生代空间存满,发生垃圾回收,就会去分析这个stu对象的可达性,这时发现stu对象被局部变量GC Roots持有无法回收。
类静态变量也是一种常见的GC Roots,上述代码中的user对象就是被user静态变量持有,那么当发生垃圾回收的时候也不会被回收。
小结
通过本节的内容学习,相信大家已经搞懂了为什么要垃圾回收以及通过可达性算法分析哪些对象是可以被回收的,哪些对象是存活的,常见的两种GC Roots希望大家记住:1是我们的局部变量引用,2是我们的类静态变量引用。通过GC Roots引用的对象是无法被回收的。
那么除了被GCRoots引用的对象无法被回收以外,java还有哪些对象是可以回收的或不能回收的呢?下一节我们将重点介绍Java的引用类型:强引用、软引用、弱引用、虚引用这四者的概念以及对象的finalization机制。