本文是《深入理解Java虚拟机 JVM高级特性与最佳实践》的读书笔记
在介绍Java的垃圾回收方法之前,我们先来了解一下Java虚拟机在执行Java程序的过程中把它管理的内存划分为若干个不同的的数据区的什么?
1.Java运行时数据区的划分
如下图:
其中程序计数器,虚拟机栈,本地方法栈这3个区域的内存随线程而生,随线程而灭的,因此这几个区域的内存分配与回收都是有确定的,我们不需要考虑这几个区域的内存的分配与回收。而堆和方法区则不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分的内存的分配和回收都是动态的,垃圾收集器关注的就是这部分内存(堆和方法区)。
下面我们先简单介绍一下这几部分区域存放的什么东西;
- 程序计数器:(线程私有)当前线程所执行的字节码的行号指示器,解释器工作时就是通过改变这个计数器的值来取得下一条需要执行的指令。
- Java虚拟机栈:(线程私有)描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从刻调用直到执行完成,就对应于栈帧在虚拟机栈中的入栈和出栈的过程。我们常说的栈内存就是这个。
- 本地方法栈:(线程私有)与Java虚拟机栈类似,只不过这是为虚拟机会用到的Native方法服务的,它也会抛出*Error和OutOfMemoryError异常。
- Java堆:(所有线程共享)几乎所有的对象实例都会在这里分配内存,Java堆还可以细分为新生代和老年代;
- 方法区:(线程共享)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;(在某些虚拟机上也称为永久代)。
2 Java(JVM)的垃圾回收机制
2.1 哪些内存需要回收?
在Java中,都是通过可达性分析来对象是否存活的(如果对象是死的,那么它所占用的内存就是需要回收的)。可达性分析算法的基本思想就是通过一系列被称为“GC Roots”的对象开始,从这些节点向下搜索,搜索走过的路线称为引用链。当一个对象没有在任何引用链上出现,则这个对象会被判定为不可用的(死的,可回收的)。
在被可达性分析算法判定为不可用的对象,也并非是一定就是会被回收的,它们还会经历一次筛选的过程,筛选的条件就是此对象是不是要执行finalize()方法,如果对象没有覆盖finalize()方法或它的finalize()方法在上一次垃圾回收器工作时已经执行过了,则被判定为不用执行finalize()方法(对象会在这次回收中被回收),若判定为需要执行finalize()方法,则这个对象会被放置在一个F-Queue队列中,稍后虚拟机会建立一个finalizer线程(低优先级)来触发这个方法,但虚拟机不承诺会等它执行完这个方法。(也就是这个对象可能在执行finalize()方法时被回收了),如果在finalize()方法中,对象加入了任何一个引用链中,则这个对象在这次回收器工作时就不会被回收了。
在Java中,有几种可作为GC Roots对象:虚拟机栈(栈帧中的本地变量表)中引用对象。方法区中类静态属性和常量引用的对象和本地方法栈中JNI引用的对象;
2.2 垃圾回收算法
2.2.1 标记-清除(Mark-Sweep)算法
首先会利用前面的可达性分析算法标记出需要回收的对象,在标记完成后就统一回收所有被标记的对象,这个算法的缺点主要有:
- 效率问题,在标记和清除两个过程中效率都不高;
- 空间问题,标记清除之后会产生大量的内存碎片,碎片太多,可能导致在下次为大对象分配内存时,提前触发一次垃圾回收动作;
2.2.2 复制算法(Coping)
将可用的内存分为两块,每次只使用其中的一块,这样每次只需要顺序分配内存就可以,当一块的内存用完后,就把还存活的对象复制到另一块内存中去,然后对使用过的内存空间进行回收就可以了。(一般不会采用平均分成两块的方式,现代虚拟机一般会将内存分成一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间,回收时,将Eden空间和Survivors空间里还存活的对象复制到另一块没有使用的Survivor空间中,然后清理掉用过的空间),一般会这种算法回收新生代的内存空间;
2.2.3 标记-整理(Mark-Compact)算法
先利用可达性分析算法标记需要回收的对象,然后就让还存活的对象(出现在任何引用链中的对象)都向一端移动,然后清理掉端边界外的内存。(一般用来回收老年代的对象);
3 什么时候回收
大多数情况下,对象优先在Eden区中分配(大对象直接在老年代分配),当Eden没有足够空间时,JVM就会发起一次Minor GC。在进行Minor GC 之前,JVM会检查老年代最大可用的空间是否大于新生代所有对象的空间,如果成立,则Minor GC是安全的,否则,JVM就会去检查HandlePromotionFailure设置值是否允许担保失败。如果允许担保失败,则会继续检查老年代最大可用空间是否大于历次晋升到老年代对象的平均大小,如果是,则会尝试进行Minor GC(若失败,就会进行一次Full GC);否则就会改为进行一次Full GC;