5.自动垃圾回收

字节码文件是通过类加载器classloader加载进来的,通过类加载器,字节码的信息就会保存到运行时数据区域的方法区,接下来执行引擎中的解释器会开始执行字节码指令,在指令中,会在堆上创建出对象。
在c/c++中,一个对象如果不再使用,需要手动释放内存,否则就会出现内存泄漏,这种释放对象的过程称为垃圾回收。
手动释放对象称为手动回收,自动释放对象称为自动回收。
内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累会导致内存溢出。
Java中为了简化内存的释放,引入了自动的垃圾回收(Garbage Collection, GC)机制,通过垃圾回收器对不再进行使用的对象完成自动回收,垃圾回收器主要负责对堆上的内存进行回收,其他很多现代语言如C#、python、go都有自己的垃圾回收器。
与c++手动回收垃圾相比,自动垃圾回收降低了变成的复杂度,但是也有缺点,程序员无法控制垃圾回收的及时性。
学习自动垃圾回收机制的应用场景如下:

之前学习运行时数据区域的时候,学了五个内容:程序计数器、Java虚拟机栈、本地方法栈、方法区和堆。**需要注意的是:程序计数器、虚拟机栈、本地方法栈都是线程不共享的,它们随着线程的创建而创建,销毁而销毁,方法的栈帧在执行完毕后弹出栈会自动释放掉对应的内存。所以这三个区域是不需要自动垃圾回收的。**需要垃圾回收的区域只有两个:方法区和堆。
5.1 方法区回收
方法区的回收比较苛刻。话说,用户自己写的类的类加载器是应用程序类加载器,这个类加载器在加载过程中是不会被回收的,所以用户自己写的类不会被回收。实际上,方法区回收的应用场景还是比较少的。

5.2 堆回收——引用计数法
如何判断一个对象是否可以被回收?
Java中的对象是否能被回收,是根据对象是否被引用来决定的,如果对象被引用了,说明该对象还在被使用,不允许被回收。(这个说法有些不太正确,之后学习的过程中会慢慢纠正)
只有对象在栈帧上的局部变量中,所有引用都被去除,这个对象才能被回收。
比如说,有两个对象相互引用,要怎么样做,这两个对象才能被回收呢?堆上的A的对象要被回收,首先要让栈帧中a1=null,断开a1中保存着的A对象的引用,接着要让堆中B对象的成员a=null,也断开A对象的引用。这样A的实例对象才能被回收。
A的对象被回收后,其成员中b自然不会再保存堆上实例对象B的引用,此时只需要让栈帧中b1=null,即可让B的对象没有任何引用,能够被垃圾回收器去除。
如果只执行a1=null,b1=null,两个对象也是可以被回收的。虽然在堆上它们彼此之间还存在着对方的引用。但是在所有栈帧上,已经不存在它们两个的引用了,所以这两个对象应该要被回收。

**引用计数法:**为每个对象维护一个引用计数器,当对象被引用时+1,取消引用时-1。垃圾回收器扫描堆中对象,如果发现该对象的引用计数器=0,那该对象就可以被回收。非常简单易懂。但是它有缺点:
- 每次引用和取消引用都需要维护计数器,对系统性能有一定的影响。
- 存在循环引用问题。当堆中存在两个对象A和B,A引用B,B引用A,就算所有栈帧中的局部变量不存在这两个对象的引用,这两个对象的引用计数器也还是为1,无法被回收。这样就出现了内存泄漏。
事实上,Java虚拟机也没有使用引用计数法,而是使用了其他方法。
小技巧:查看垃圾回收的信息
5.3 堆回收——可达性分析法
可达性分析法将将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

问题来了,哪些对象能被称为GC Root对象呢?
主要有四类对象能被称为GC Root对象。线程Thread对象、系统类加载器加载的java.lang.Class对象、监视器对象(同步锁synchronized关键字持有的对象)、本地方法调用时使用的全局对象。
-
线程Thread对象。
每个线程都会有一个虚拟机栈,实际上堆中有一个线程Thread对象,保存着栈内存的引用(栈中每一个栈帧的方法参数、局部变量的引用都会保存在Thread对象之中)。所以对于一个方法中任何创建且被赋值给局部变量或者方法参数的对象,它们的根对象就是堆中的线程Thread对象。

-
系统类加载器加载的java.lang.Class对象(引用类中的静态变量)
静态变量实际上是一个比较特殊的存在。在类加载完毕后,它会被保存在堆中的Class对象之中。Class对象实际上保存着一个GC Root对象:sun.misc.Launcher,这个对象保存着应用程序类加载器和扩展类加载器。而应用程序类加载器中保存着被应用程序类加载器加载进来的类(静态变量、静态代码块等等)。**所以类中的静态变量是可以反引用找到GC Root对象的。**如果静态变量保存着堆中对象的引用,那么这个对象是不能被回收的。
-
监视器对象
监视器对象也是一类GC Root对象。监视器对象,其实就是同步锁synchronized关键字持有的对象。一旦该对象被同步锁持有,就不能被回收了。
-
本地方法调用时使用的全局对象
这一块是虚拟机底层的对象,不需要程序员过多关注。程序员开发也不会用到。
以下代码中,在一个循环中重复创建对象并赋值给局部变量。每一次执行循环后,栈帧中的局部变量表就会更新,objects会断开引用,上一次循环中的对象就可以被回收。


总结一下:如果一个对象在它的引用链上没有GC Root对象,那么这个对象就可以被回收,如果有GC Root对象,那么这个对象就不能被回收。
查看GC Root,可以使用arthas的heapdump命令先生成一个堆内存快照,然后使用专门的工具去打开快照文件。

本文详细解析了Java中的自动垃圾回收机制,包括方法区和堆的回收策略,以及引用计数法和可达性分析法的区别。重点介绍了GCRoot对象的概念,指出类的静态变量如何影响对象的回收。此外,文章还提供了检查垃圾回收情况的小技巧。

791

被折叠的 条评论
为什么被折叠?



