在面试java后端开发的时候通常都会问到java的自动回收机制(GC)。在了解java的GC回收机制以前,咱们得先了解下Java虚拟机的内存区域。html
java虚拟机在执行的过程当中会将其管理的内存划分为不用的数据区域,不一样的区域有不一样的做用以及线程时间。java
数据区划分以下:面试
下面将介绍不一样区域的做用,若是已经了解能够跳过算法
程序计数器(线程私有)后端
程序计数器的做用很简单,就是记录当前线程所执行的位置(因此为线程私有),能够当作当前线程所执行的字节码的行号指示器。若是执行的是native方法,则这个计数器为空。数组
Java虚拟机栈(线程私有,生命周期与线程相同)spa
虚拟机栈描述的是Java方法执行的内存模型:每一个Java方法在执行的时候都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。线程
本地方法栈(线程共享)code
本地方法栈与虚拟机栈发挥的做用相似,不过它执行的是虚拟机使用的Native方法。htm
Java堆(线程共享)
Java堆是Java虚拟机管理内存中最大的一块,在虚拟机启动的时候建立。此区域的惟一目的就是存放对象示例,几乎全部的对象实例都是在这分配内存的。
方法区(线程共享)
刚开始的时候,看到方法区域,第一想法就是Java中的方法
,不过实际上并非这样。方法区储存的是已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。咱们能够想想,当咱们须要建立一个对象的时候,咱们须要根据类的信息去建立,那么类的信息在哪?固然是在方法区!
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各类字面量和符号引用。
前面说了这么多,如今咱们终于能够来讲说垃圾回收机制了。
首先咱们得说下垃圾回收回收的是哪一部份内存区域。在前面咱们知道:程序计数器,虚拟机栈,本地方法栈都是线程私有的,随着线程生或灭。这部分咱们就不须要考虑了。因此咱们须要考虑的就是Java堆
和方法区
。
对象是否能够被回收
判断对象是否被回收就是当一个对象死了的时候就须要进行回收。那么如何判断一个对象是否死亡,在Java中,咱们使用了可达性分析算法来判断对象是否存活。
当一个对象到GC Roots没有任何链(称为引用链
)相连(也就是对象到GC Roots不可达)则断定对象已经死亡(如图中的Object5,Object6),可进行回收。
可做为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即通常说的Native方法)引用的对象
在前面中,咱们知道,不可达就意味着回收,但是当咱们的内存很够时,有一些对象又是“食之无味弃之惋惜”的时候,咱们怎么办呢?在JDK1.2中,Java对引用进行扩张,分为如下引用:
- 强引用(Strong Reference):只要强引用还在,则不回收
- 软引用(Soft Reference):描述一些有用但非必须的对象,在系统将要发生内存溢出以前,将这些对象列入回收范围之中进行第二次回收。<java.lang.ref.SoftReference>
- 弱引用(Weak Reference):比软引用还要弱,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。<java.lang.ref.WeakReference>
- 虚引用(Phantom Reference):不会对生存时间构成影响,惟一的做用就是这个对象被回收的时候会收到一个通知。<java.lang.ref.PhantomReference>
最终判断对象是否可以存活
在可达性分析算法中,若是一个对象不可达,那么这个对象就进入到了“缓刑”阶段,真正宣告一个对象死亡还须要进行两次标记。
第一次标记进行筛选
对不可达的对象进行第一次标记并进行筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过(意思就是finalize()方法只能被调用一次,也就是对象只可以有一次避免被回收),虚拟机将这两种状况都视为“没有必要执行”,对象被回收。
第二次标记
若是这个对象被断定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动创建的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样作的缘由是,若是一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的状况),将极可能会致使F-Queue队列中的其余对象永久处于等待状态,甚至致使整个内存回收系统崩溃。
finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,若是对象要在finalize()中成功拯救本身----只要从新与引用链上的任何的一个对象创建关联便可,譬如把本身赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。若是对象这时候还没逃脱,那基本上它就真的被回收了。
在Java虚拟机规范中说过不要求方法区实现垃圾收集,而且进行垃圾收集的“性价比”也较低。不过既然写了,那一定有方法区的垃圾收集,主要回收如下两部份内容:
废弃常量:字面量和符号引用
无用的类:
- 该类的全部实例都被回收,即:Java堆中不存在该类的任何实例
- 该类的Classloader已经被回收
- 该类对用的java.lang.Class对象没有任何地方被引用,没法在任何地方经过反射访问到该类的方法。
当知足以上三个条件时,也未必说是必定要被回收。也仅仅是能够。
咱们经过对象的存活周期来将JVM堆中内存空间划分为新生代和老年代。
新生代:主要是用来存放新生的对象。通常占据堆的1/3空间。
老年代:主要存放应用程序中生命周期长的内存对象。
OK,说了这么多,咱们如今终于能够来讲说垃圾收集的算法了。
下面的图片来源于这位大佬,这位大佬讲的真滴不错。
标记-清除算法(Mark-Sweep)
标记:首先标记须要回收的对象,标记完成统一回收
清除:就是清除对象,释放空间
缺点:标记和清除的效率不高,同时产生大量不连续的内存碎片(可能不利于下次的空间分配)。
标记整理法
标记整理算法相比较于标记清除算法,标记-整理算法在清除的时候并非一个一个的清除对象释放空间,而是一次清除所有的可回收的空间。这样使得空间变得连续,有利于对象空间的分配。
复制算法
优势:速度快,效率高,不会产生内存碎片。
缺点:显而易见,空间浪费大,缩小了一半。
解决方法:
IBM研究代表:新生代98%的对象是“朝生夕死”,因此咱们并不须要将空间划分为1:1,而是将空间划分为Eden:Survivor:Survivor = 8:1:1
。每次使用Eden和其中一块Survivor。
若是第二步中Survivor的空间不足,则依赖于其余内存(老年代)进行分配担保(也就是讲存活的对象放入老年代)。
分代收集算法
分代收集算法其实就是前面几种算法的应用。根据年代使用不一样的算法
参考书籍:《深刻理解Java虚拟机》——周志明,这本书写的太好了,写的通熟易懂。强烈推荐去看看。