垃圾收集(Garbage Collection,GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC须要完成的3件事情:html
Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操做。每个栈帧中分配多少内存基本上是在类结构肯定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大致上能够认为是编译期可知的),所以这几个区域的内存分配和回收都具有肯定性,在这几个区域内就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。而 Java堆和方法区则不同,一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,咱们只有在程序处于运行期间时才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,垃圾收集器所关注的是这部份内存。java
在堆里面存放着Java世界中几乎全部的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要肯定这些对象之中哪些还“存活”着,哪些已经“死去”。算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任什么时候候计数器为 0 的对象就是不可能再被使用的。数组
这个方法实现简单,效率高,可是目前主流的虚拟机中并无选择这个算法来管理内存,其最主要的缘由是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,以下面代码所示:除了对象 objA 和 objB 相互引用着对方以外,这两个对象之间再无任何引用。可是他们由于互相引用对方,致使它们的引用计数器都不为 0,因而引用计数算法(Reference Counting)没法通知 GC 回收器回收他们。缓存
public class ReferenceCountingGc { Object instance = null; public static void main(String[] args) { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; } }
这个算法的基本思路就是经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲,就是从GC Roots到这个对象不可达)时,则证实此对象是不可用的。Java是经过可达性分析(Reachability Analysis)来断定对象是否存活的。安全
在Java语言中,可做为GC Roots的对象包含如下几种:post
能够理解为:测试
不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。优化
在JDK1.2以后,Java对引用的概念作了扩充,将引用分为:url
这四种引用从上到下,依次减弱。
须要注意的是:在程序设计中通常不多使用弱引用与虚引用,使用软引用的状况较多,这是由于软引用能够加速 JVM 对垃圾内存的回收速度,能够维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
即便在可达性分析法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过期,虚拟机将这两种状况视为没有必要执行。
被断定为须要执行的对象将会被放在一个叫作F-Queue的队列之中进行第二次标记,除非这个对象与引用链上的任何一个对象创建关联,不然就会被真的回收。
JDK1.7 及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
假如一个字符串“abc”已经进入了常量池中,可是当前系统没有任何一个String对象是叫作“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其余地方引用了这个字面量,若是这时发生内存回收,并且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其余类(接口)、方法、字段的符号引用也与此相似。
断定一个类是不是“无用的类”的条件则相对苛刻许多。类须要同时知足下面3个条件才能算是“无用的类”:
虚拟机能够对知足上述 3 个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样不使用了就会必然被回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息。
Java技术体系中的自动内存管理分为对象内存回收和内存分配。这里研究一下对象内存分配的问题。
对象的内存分配,往大方向讲,就是在堆上分配(但也可能通过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,若是启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数状况下也可能会直接分配在老年代中,分配的规则并非百分之百固定的,其细节取决于当前使用的是哪种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
回顾一下以前在 JVM探究之 —— Java内存区域 提到的Java堆:Java 堆是垃圾收集器管理的主要区域,所以也被称做GC 堆(Garbage Collected Heap)。从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此 Java 堆还能够细分为:新生代和老年代:再细致一点,年轻代能够划分为:Eden 空间、From Survivor (S0)、To Survivor (S1) 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。大部分状况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,若是对象还存活,则会进入 s1("To"),而且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增长到必定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,能够经过参数 -XX:MaxTenuringThreshold
来设置。通过此次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。无论怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满以后,会将全部对象移动到年老代中。
首先了解一下常见的 Minor GC和Full GC的区别:
在大多数状况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,而且在进程退出的时候输出当前的内存各区域分配状况。
/** * Eden区对象内存分配测试 * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails */ public class EdenAllocationTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1, allocation2,allocation3,allocation4; // allocation1=new byte[2*_1MB]; // allocation2=new byte[2*_1MB]; // allocation3=new byte[2*_1MB]; } }
如上面代码中,在运行时经过-Xms20M、-Xmx20M、-Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,从输出的结果也能够清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。
分配两个2MB的对象以后,空间使用状况以下:
能够看到,eden区新增使用空间 2*2*1MB(1024KB) = 4096KB,Eden区剩余空间大小为 8192KB-6620KB=1572KB < 2MB,此时再分配一个2MB的对象会怎么样呢?
能够看到,当给allocation3对象分配内存时发生一次Minor GC,此次GC的结果是新生代6456KB变为750KB,而总内存占用量则几乎没有减小(由于allocation一、allocation2对象都是存活的,虚拟机几乎没有找到可回收的对象)。此次GC发生的缘由是给allocation3分配内存的时候,剩余空间已不足以分配allocation3所需的2MB内存,所以发生Minor GC。GC期间虚拟机又发现已有的2个2MB大小的对象所有没法放入Survivor空间(Survivor空间只有1MB大小),因此只好经过分配担保机制提早转移到老年代去。老年代上的空间足够存放 allocation1和allocation2对象,因此不会出现 Full GC。执行 Minor GC 后,后面分配的对象若是可以存在 eden 区的话,仍是会在 eden 区分配内存。
大对象是指,须要大量连续内存空间的Java对象,如:字符串以及数组。
虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。从而避免在Eden区及两个Survivor区之间发生大量的内存复制。
/** * 大对象老年代分配测试 PretenureSizeThreshold * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728 * 说明: * -XX:+UseSerialGC 使用SerialGC * -XX:PretenureSizeThreshold=3145728 设置PretenureSizeThreshold为3MB,这个参数不能像-Xmx之类的参数同样直接写3MB */ public class PretenureSizeThresholdTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1; allocation1=new byte[4*_1MB]; } }
须要注意的是:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器通常并不须要设置。若是遇到必须使用此参数的场合,能够考虑ParNew加CMS的收集器组合。下面示例代码经过 -XX:UseSerialGC 来指定JVM使用Serial垃圾收集器演示。
从上面代码中,能够看出当给 allocation1对象分配空间时, Eden空间几乎没有被使用,而老年代的10MB空间被使用了4MB,allocation1对象直接就分配在老年代中,这是由于PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能像-Xmx之类的参数同样直接写3MB),所以超过3MB的对象都会直接在老年代进行分配。
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了作到这点,虚拟机给每一个对象定义了一个对象年龄(Age)计数器。
若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间中,而且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增长1岁,当它的年龄增长到必定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,能够经过参数-XX:MaxTenuringThreshold设置。
/** * 对象晋升老年代的年龄阈值测试 XX:MaxTenuringThreshold * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * 说明: * -XX:+UseSerialGC 使用SerialGC * -XX:MaxTenuringThreshold=1 年龄阈值,对象每熬过一次Minor GC,它的age会加1,age达到此值对象就会晋升老年代 * -XX:+PrintTenuringDistribution 输出对象年龄 */ public class MaxTenuringThresholdTest { private static final int _1MB =1024*1024; public static void main(String[] args) { byte[] allocation1,allocation2,allocation3; allocation1=new byte[_1MB/4]; allocation2=new byte[4*_1MB]; allocation3=new byte[4*_1MB]; //首次GC allocation3=null; allocation3=new byte[4*_1MB]; //第二次GC } }
设置JVM参数-XX:MaxTenuringThreshold=1来查看执行结果:
能够看出,上面执行结果中的allocation1对象须要256KB内存,在第一次GC时Survivor空间能够容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后很是干净地变成0KB。
TODO:MaxTenuringThreshold=15
为了能更好地适应不一样程序的内存情况,虚拟机并非永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半(-XX:TargetSurvivorRatio=50 即:50%),年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
JVM究竟是如何来计算S区对象晋升到Old区的呢? 首先介绍另外一个重要的JVM参数: -XX:TargetSurvivorRatio
:一个计算指望s区存活大小(Desired survivor size)的参数。默认值为50,即50%。 当一个S区中全部的age对象的大小若是大于等于Desired survivor size,则从新计算threshold,以age和MaxTenuringThreshold二者的最小值为准。
在发生Minor GC以前,虚拟机会检查老年代最大可用的连续空间是否大于新生代全部对象的总空间,
上面提到了Minor GC依然会有风险,是由于新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端状况为内存回收后新生代中全部对象均存活),而Survivor空间是比较小的,这时就须要老年代进行分配担保,把Survivor没法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,所以只好取以前每次垃圾回收后晋升到老年代的对象大小的平均值做为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
取平均值仍然是一种几率性的事件,若是某次Minor GC后存活对象陡增,远高于平均值的话,必然致使担保失败,若是出现了分配担保失败,就只能在失败后从新发起一次Full GC。虽然存在发生这种状况的几率,但大部分时候都是可以成功分配担保的,这样就避免了过于频繁执行Full GC。