关于作者
郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。
Java与C++之间有一堵由内存动态分配和垃圾收集技术围成的高墙,墙外的人想进去,墙里的人想出来。
Java虚拟机在执行程序的时候会将内存划分为若干个不同的区域,这些区域有各自的用途以及创建、销毁时间,有些区域随着虚拟机进程启动而启动,有些则依赖用户线程的启动和结束 而建立和销毁。
Java虚拟机运行时的内存结构如下图所示:
- 方法区:线程共享,用于存储已被虚拟机加载的类信息,常量,静态变量以及及时编译器编译后的戴拿等数据。方法区还有个别名叫"非堆"。
- 堆:线程共享,Java虚拟机管理的内存区域中最大的一块,在虚拟机启动时创建,该区域的作用是存放对象实例。
- 虚拟机栈:线程私有,生命周期与线程相同,虚拟机栈描述Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧,栈帧用于存储局部变量表,操作数 栈,动态链接,方法出口等信息,每个方法从调用到结束就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 本地方法栈:线程私有,它与虚拟机栈十分相似,相对于虚拟机栈为Java方法服务,本地方法栈为Native方法服务。
- 程序计数器:空间较小,每个线程私有,当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器来选择下一条需要执行的字节码指令。 如果执行的是Java方法,则该计数器记录的是正在执行的虚拟机字节码指令的地址;如果是Native方法,则计数器的值为空。
提到垃圾回收,我们可以先思考一下,如果我们去做垃圾回收需要解决哪些问题? 🤔
一般说来,我们要解决一些三个问题:
- 哪些内存回收?
- 什么时候回收?
- 如何回收?
这些问题分别对应着引用管理和回收策略等方案。
提到引用,我们都知道Java中有四种引用类型:
- 强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
- 软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足的时候回回收这类对象。
- 弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次GC发生时,当GC发生时,无论内存是否足够,都会回收该对象。
- 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知。
不同的引用类型,在做GC时会区别对待,我们平时生成的Java对象,默认都是强引用,也就是说只要强引用还在,GC就不会回收,那么如何判断强引用是否存在呢?🤔
一个简单的思路就是:引用计数法,有对这个对象的引用就+1,不再引用就-1,但是这种方式看起来简单美好,但它却不嫩解决循环引用计数的问题。
因此可达性分析算法登上历史舞台😎,用它来判断对象的引用是否存在。
可达性分析算法通过一系列称为GC Roots的对象作为起始点,从这些节点从上向下搜索,搜索走过的路径称为引用链,当一个对象没有任何引用链 与GC Roots连接时就说明此对象不可用,也就是对象不可达。
GC Roots对象通常包括:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法去中类的静态属性引用的对象
- 方法区中常量引用的对象
- Native方法引用的对象
可达性分析算法整个流程如下所示:
- 第一次标记:对象在经过可达性分析后发现没有与GC Roots有引用链,则进行第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行finalize()方法。没有覆盖finalize()方法或者finalize()方法已经被执行过都会被
认为没有必要执行。
- 如果有必要执行:则该对象会被放在一个F-Queue队列,并稍后在由虚拟机建立的低优先级Finalizer线程中触发该对象的finalize()方法,但不保证一定等待它执行结束,因为如果这个对象的finalize()方法发生了死循环或者执行 时间较长的情况,会阻塞F-Queue队列里的其他对象,影响GC。
- 第二次标记:GC对F-Queue队列里的对象进行第二次标记,如果在第二次标记时该对象又成功被引用,则会被移除即将回收的集合,否则会被回收。