对于Java虚拟机的了解,我认为是一个Java程序员已经入门的重要标志,而JVM中的垃圾回收机制(Garbage Collection,简称GC)又是JVM中的重点,所以hans在这里用篇文章时间和大家一起了解一下GC。
GC是Java平台中最重要的标志之一,它最早诞生于有MIT研发的一门叫做Lisp的语言之中,所以它的历史要比Java更悠久。在它广泛使用之前,程序员要经常处理由于内存处理不完善而引起的bug。但是即使在内存处理极其完善的今天,我们这些想成为优秀程序员的程序猿,也不能将它视为透明,因为当垃圾收集成为系统达到更高并发量的瓶颈的时候,我们必须会对它实施必要的监控和调节。
其实在垃圾回收机制刚刚诞生的时候,关于内存,人们就经常想3个问题:
- 哪些内存需要回收?
- 什么时候回收?
- 怎么回收?
这几个问题咱们一个个看。
先说哪些内存需要回收
众所周知,Java的内存大体可划分为方法区、栈(虚拟机栈和本地方法栈)、堆。因为栈是线程私有的,每一个栈中的内存如何分配,基本上在编译期就已知了,同时这部分内存随着线程或方法的结束就跟着回收了,所以栈中的内存分配和回收具有确定性。但堆和方法区就不一样了,由于具有“动态绑定”和“运行时产生新常量”等特性,所以这部分的内存是在运行期分配的,是不确定的,内存回收指的就是回收这一部分的内存。
知道了要回收的内存是堆中和方法区中的内存,现在需要知道就是“什么时候回收”了。(抢答————在调用System.gc()
的时候回收。抢答正确!但如果就这么简单我会在这里另起一段?)
这里说的是“什么时候回收”确切的说是“内存处于什么状态时回收”(骂我表述不准确?我愿意,你来打我呀!)一般来讲大家的第一个反应是“有地方引用它就不回收、没地方引用就回收”,很好!这是一个很棒的思路(Python的垃圾回收就是用的这种思路)但是你细想想这种思路有一个比较致命的问题,想到是什么了吗?没想到就看看下面的代码吧。
public class Test {
public Test instace = null;
public static void main(String[] args) {
Test testA = new Test();
Test testB = new Test();
testA.instace = testB;
testB.instace = testA;
testA = null;
testB = null;
}
}
这种思路的致命问题就是它存在对象间循环引用的问题,像上面例子中的testA和testB已经无法访问了,理应被回收。但是由于它们互相引用着对方,所以它们有地方被引用,利用上述思路就无法回收。如何解决这种问题呢?
“可达性分析”就可以完美解决这种问题,可达性分析是通过一些“根”作为起点,将“根”能引用到的对象加入“可达对象集合”中,再讲其中的对象引用到的对象加入该集合中,重复这个过程直到没有任何对象可以加入该集合。我们得到的这个集合就是不用回收的对象集合,剩下的就都可以回收了。现在问题来了,“根”是什么呢?(我不会告诉你它是火之国木叶村的地下忍者组织。活跃一下气氛别太当真)“根”也是对象,包括下面几种:
- 栈中所引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
就如上面那个例子,testA和testB虽然都被别人引用,但是无法从“根”开始达到它们,所以“可达性分析”就很成功的解决了循环引用的问题,所以现在Java、C#等都用的是这种方式来确定哪些对象可以回收。
现在前两个问题解决了,开始解决第三个问题——怎么回收?
最简单的方法就是将可以回收的对象标记一下,然后召唤“对象回收程序”将它们回收,这种方法即简单又可行,但存在一个关键的问题(就知道不可能这么简单)那就是——碎片化。想象一下,内存是连续的,但可回收对象的位置是随机的,这样就导致了内存回收后,虽然有很大的内存空间,但是这些内存空间被一些“不可被回收的对象”分成了一个个小块,若重新整理一遍内存、清理碎片又要花费大量的时间。为了解决这个问题出现了两种优秀的解决办法分别是“复制法”和“标记-整理法”
先谈谈“标记-整理法”。这种方式其实将内存回收和内存碎片整理合二为一的方法,让两个工作同时进行,从而极大的减少了时间。它是在可以回收的对象标记之后,将不可回收的对象向内存一端移动,这个过程中直接可以将部分可回收的对象覆盖,当移动完成后,直接清空边界以外的内存。这样就极大的减少了回收后再进行碎片整理的时间。
再来看看“复制法”。这种方式的原形是将内存分为两块,分配对象时只在其中的“内存块1”上操作,当这一块用完了之后,将不可以被回收的对象复制到“内存块2”上去,然后将“内存块1”清空。这种方式会比上面那种方法还要快,但是存在一个不足,就是让分配对象时的内存缩小为原来的一半,这显然不是很合算。人们通过观察发现,新建立的对象会有很大一部分是可回收的,所以就有了一种更巧妙的改进版。
这种改进版就是将内存分成一块较大的Eden空间和两块较小的Survivor空间(以HotSpot为例,大小比例是8:1:1),每次使用Eden和一个Survivor。回收时,将两者中存活的对象复制到另一Survivor中,然后清空这两个内存空间。这样的话之会有10%的内存被浪费。通过这样的改进“复制法”的性能就是很可观的了。但是也有时会有大量的对象存活下来,导致Survivor空间不够用,这就使人们加深对对象存活的观察和思考。
观察发现虽然刚刚新建的对象“可被回收率”高达98%,但经过几轮回收后剩下来的对象,一般是经常要用到的,很少可以被回收。所以人们就想可以将Survivor空间中存活了几轮的对象,放到一个新的空间中,在这个空间每次回收的对象会相对较少,因此可以将回收频率调低。这一部分的回收方法自然要用到“标记-整理法”,并且将这部分称为“老年代”,将刚刚新建的对象称为“新生代”。“新生代”的内存回收用“复制法”而老年代用“标记-整理法”,这样的方式就是现在大部分虚拟机采用的“分代收集法”
关于GC的基本理论就讲到这里了,以后等我再有所长进,再跟大家深入的讨论。这篇文章可能有一些错误或不足,希望大家及时通知我会改的。