一、运行时数据区域
首先垃圾收集主要是是针对堆和方法区进行。(我理解是共有部分。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,所以不需要对这三个区域进行垃圾回收。
堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建,作用是存放内存对象实例,几乎所有的对象实例都在这里分配。对于java虚拟机规范的描述:所有的对象实例以及数组都要在堆上分配。所以Java堆也是垃圾收集器管理的主要区域。
方法区是和堆一样,是线程共享的内存区域,它用来存储已经被虚拟机加载过类信息、常量、静态变量即时编译器编译后的代码等数据。别名叫做非堆。目的是跟堆区分开来。 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。
JAVA 虚拟机栈是线程私有的,与线程的生命周期相同,java虚拟机栈描述的是java方法执行的内存模型。每个方法在执行是都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至结束完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。其中局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用类型(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,亦可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和retuenAddress类型(指向了一条字节码指定的地址)。
本地方法栈与虚拟机栈发挥的作用是非常相似的。他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码服务),而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
运行时常量池(Runtime Constant Poll)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。<eg Class文件中的常量池(编译器生成的字面量和符号引用)会在类加载后放在这个区域>,除了在编译期生成的常量,还允许动态生成,例如String类的intern()。
直接内存在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
二、垃圾收集
判断一个对象是否可被回收
1、引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1,引用计数器为0的对象可被回收,但是在两个对象出现循环引用的时候,此时计数器永远不为0,导致无法对他们进行回收,正式因为循环引用的存在,因此JAVA虚拟机不适用引用计数器算法。
2、可达性分析算法
以GC Roots为起点进行搜索,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有引用到的节点,即无用的节点,无用的节点将会判定是可回收的对象。
在JAVA语言中,可作为GC Roots的对象包括下面几种:
1)虚拟机栈中引用的对象(栈帧中的本地变量表)
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Native方法)引用的对象
2.1Java中的引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
##强引用 在程序代码中普遍存在的,类似 object obj=new Object() 这类引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
##软引用
用来描述一些还有用但是非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
##弱引用 也是用来描述非必须对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作室吗,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
##虚引用
也要幽灵引用或者幻影引用,是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例,他的作用是能在这个对象被收集器回收时收到的一个系统通知。
无论引用计数器算法还是可达性分析算法都是基于强引用而言的。
2.2对象死亡(被回收)前的最后一次挣扎 即时是在可达性算法中不可达的对象,也并非是非死不可的,这时候他们暂时处于缓刑阶段,要真正的宣告一个对象的死亡,至少要经历两次标记过程。
第一次标记: 如果对象在进行可达性分析后发现并没有与GC Root 相连接的引用链,那么他将会被第一次标记。
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,在finalize()方法中没有重新与引用链简历关联关系的,将被进行第二次标记。 第二次标记成功后的对象真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。
2.3方法区如何判断是否需要回收
方法区主要回收的内容是废弃的常量和无用的类,对于废弃的常量可以用可达性分析来判断,但是无用的类则需要同时满足以下三个条件:
1、该类所有的实例都已经被回收,也就是Java堆中不存在改类的任何实例;
2、加载该类的ClassLoader已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
三、常用的垃圾收集算法
标记-清除算法
标记清除算法: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,是最基础的收集算法。 标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下较为高效,但是标记跟清除的过程效率不高。由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
3.2复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。将可用内存按容量分为大小相等的两块,每次只是用其中的一块,当这块内存用完了。就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是每个半区在进行内存回收,不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,缺点是将内存缩小为原来的一半。
3.3标记整理算法 根据老年代的特性,一般使存活用标记整理算法,过程与‘标记清除’算法一样,但是后续步骤不是直接对可回收对象进行清理。而是让所有存活对象都像一端移动,然后直接清理掉端边界以外的内存。
当前商业虚拟机都使用分代收集算法,根据对象的存活周期的不同将内存分为几块,一般是把java堆分成新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。新生代中,每次都有大量对象死去,只有少量存活,就用复制算法,将存活的对象复制到新的区域完成收集。而老年代中对象的存活率高,没有额外空间对他进行分配担保,就必须使用标记清除 或者标记整理算法就行回收。