提到JVM垃圾回收,总觉得离我们程序员有一定的距离。在JAVA中,那是系统自己干的事,我们关心那个干嘛?也就是说我们为什么要学习这个东西,大家开开心心地敲代码不好吗?
还真的不好,一方面我觉得我们可以学习下JAVA语言设计上的一些思想,另一方面,在我们以后从事一些较为高级一点的开发,尤其是性能调优之类的,知道这些基础知识就显得很必要了。我打算从以下几个方面开始进行简单地说明。
GC如何知道哪些对象是垃圾对象?
GC不可能随便指派说哪个对象是垃圾,要有一定的依据。常用的标记垃圾的算法有两个:
引用计数算法
引用计数算法,就是每个对象有一个引用计数器,当该对象被引用的时候计数器加1,当引用失效的时候,计数器减1。
那么这么做有什么缺点吗?
那就是当两个对象相互引用的时候,这两个对象都会无法释放。
根搜索算法
从根对象开始,所有能被触及的对象都可以认为是“存活的”对象,换句话说,就是“仍然使用的”对象。不能被触及的对象,就会被认为是垃圾,需要回收。
那么什么对象可作为GC Roots呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性的引用;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。
什么是虚拟机栈?
虚拟机栈为Java方法服务。说的简单点,就是我们平时所写的Java方法,没调用一个Java方法,就是入栈的过程,退出方法就是出栈的过程。每个Java方法对应于虚拟机栈中的一个栈帧。
什么是本地方法栈?
本地方法栈为native方法服务。
方法区是什么呢?
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。可以看做堆的一个逻辑部分。
常用的垃圾回收算法有哪些?
看下图,整幢大楼灯火通明,其中不排除一些办公室没人但还是灯亮着的情况,为了节约资源,我们需要关掉那些办公室没人的灯。那么怎么办呢?
标记——清除算法
我从顶楼开始到一楼,一块一块办公区域看,对没人的区域进行关灯。将整个大楼看成内存,灯亮着的区域表示有对象存在,灯灭着的表示空闲区域。我们一块一块区域检查的这个过程就是标记的过程,关灯的操作就是清除的过程。这就是标记——清除算法。
弊端:
- 那就是费时费力,效率太低。
- 不连续,不美观(内存碎片严重)。
复制回收算法
老板说了,浪费太严重了,到了晚上,我们对需要加班的同事进行统一安排。假设大楼共10层,只能使用15层或者610层(毕竟晚上加班的人不多)。比如现在使用的就是15楼,到了晚上要用灯了,需要加班的同事自己去610楼找位置,保安一听乐了,再也不用一块一块区域关灯了,有需要的人都去610楼了,剩下的即便是灯亮着的办公区域那也是没人,让我分别去15楼拉个总闸先(回收的过程)。
可以看到,在任意时刻只用到了内存的一半。
弊端:
- 有需要的同事搬到6~10楼的过程,太麻烦。特别是需要加班的同事比较多的时候。(需要对有用的对象进行复制)。
- 整栋大楼只能用一半,哎(内存使用率降低)。
优点:
从外面看,我知道哪些地方有人,哪些地方没人,方便了管理(内存无碎片)。
标记——整理算法
下班后,保安大哥将空的办公区域依次统计出来(标记的过程),需要加班的同事按照统计结果,依次搬到空闲的办公区域。保安大哥知道,我只需要找到最后一个有人的区域,那么这块区域之后肯定不会有人了,不用挨个检查了,去拉后面的闸。
优点:
- 内存无碎片。
- 同时避免了当有用对象比较多的时候,复制回收算法的麻烦。
分代回收算法
新生代:
刚创建的对象都在新生代,新生代采用复制回收算法。新生代分为三个区,一个Eden区,一般两个Survivor区。大部分对象在Eden区生成,当Eden区域满时,将还存活的对象复制到其中一个Survivor区域,当这个Survivor区域满时,将其中还存活的对象复制到第二个Survivor区域。那么当第二个Survivor区域满时该怎么办呢?那就是将第二个Survivor区域中由第一个Survivor区域复制过来的对象,复制到“老年代”中。
这个过程是有点绕,但是可以想象成面试过程中层层选拔的过程,能力越强的可以想象成生命周期越长的对象。
老年代:
这个区域中的对象都是在新生代中经历了层层回收后仍然存活的对象,这个区域采用标记整理的算法进行垃圾回收。
持久代:
持久代中用于存放一些静态文件,static常亮,常量池等。这块区域对垃圾回收没有显著影响。
什么时候会进行垃圾回收?
GC有两种类型:Minor GC和Full GC。
Minor GC
当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清理非存活对象。
Full GC
对整个堆进行整理,所以比Minor要慢,所以尽可能地减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
- 老年代被写满;
- 持久代被写满;
- System.gc()被显式调用。
参考资料:《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》