Java中的垃圾回收通常是在Java堆中进行,由于堆中几乎存放了Java中全部的对象实例。谈到Java堆中的垃圾回收,天然要谈到引用。在JDK1.2以前,Java中的引用定义很很纯粹:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。但在JDK1.2以后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。java
Java堆中存放着几乎全部的对象实例,垃圾收集器对堆中的对象进行回收前,要先肯定这些对象是否还有用,断定对象是否为垃圾对象有以下算法:算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任什么时候刻计数器都为0的对象就是不可能再被使用的。编程
引用计数算法的实现简单,断定效率也很高,在大部分状况下它都是一个不错的选择,当Java语言并无选择这种算法来进行垃圾回收,主要缘由是它很难解决对象之间的相互循环引用问题。数组
Java和C#中都是采用根搜索算法来断定对象是否存活的。这种算法的基本思路是经过一系列名为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证实此对象是不可用的。在Java语言里,可做为GC Roots的兑现包括下面几种:服务器
实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:若是对象在进行根搜索后发现没有与GC Roots相链接的引用链,那它会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为没有必要执行。若是该对象被断定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动创建的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(由于一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是要在finalize()方法中成功拯救本身,只要在finalize()方法中让该对象重引用链上的任何一个对象创建关联便可。而若是对象这时尚未关联到任何链上的引用,那它就会被回收掉。网络
断定除了垃圾对象以后,即可以进行垃圾回收了。下面介绍一些垃圾收集算法,因为垃圾收集算法的实现涉及大量的程序细节,所以这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。ide
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉全部被标记的对象,它的标记过程其实就是前面的根搜索算法中断定垃圾对象的标记过程。标记—清除算法的执行状况以下图所示:性能
回收前状态:优化
该算法有以下缺点:spa
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而获得的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,而后再把已使用过的内存空间一次清理掉。复制算法有以下优势:
它的缺点是:可一次性分配的最大内存缩小了一半。
复制算法的执行状况以下图所示:
回收前状态:
复制算法比较适合于新生代,在老年代中,对象存活率比较高,若是执行较多的复制操做,效率将会变低,因此老年代通常会选用其余算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程同样,但对标记后出的垃圾对象的处理状况有所不一样,它不是直接对可回收对象进行清理,而是让全部的对象都向一端移动,而后直接清理掉端边界之外的内存。标记—整理算法的回收状况以下所示:
回收前状态:
回收后状态:
当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不一样将内存划分为几块,通常是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少许存活,所以可选用复制算法来完成收集,而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
下面咱们来看以下代码:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 byte[] holder = new byte[32*1024*1024]; 4 System.gc(); 5 } 6 }
[GC 208K->134K(5056K), 0.0017306 secs]
[Full GC 134K->134K(5056K), 0.0121194 secs]
[Full GC 32902K->32902K(37828K), 0.0094149 sec
注意第三行,“->”以前的数据表示垃圾回收前堆中存活对象所占用的内存大小,“->”以后的数据表示垃圾回收堆中存活对象所占用的内存大小,括号中的数据表示堆内存的总容量,0.0094149 sec 表示垃圾回收所用的时间。
从结果中能够看出,System.gc(()运行后并无回收掉这32MB的内存,这应该是意料之中的结果,由于变量holder还处在做用域内,虚拟机天然不会回收掉holder引用的对象所占用的内存。
咱们把代码修改以下:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 { 4 byte[] holder = new byte[32*1024*1024]; 5 } 6 System.gc(); 7 } 8 }
加入花括号后,holder的做用域被限制在了花括号以内,所以,在执行System.gc()时,holder引用已经不能再被访问,逻辑上来说,此次应该会回收掉holder引用的对象所占的内存。但查看垃圾回收状况时,输出信息以下:
[GC 208K->134K(5056K), 0.0017100 secs]
[Full GC 134K->134K(5056K), 0.0125887 secs]
[Full GC 32902K->32902K(37828K), 0.0089226 secs]
很明显,这32MB的数据并无被回收。下面咱们再作以下修改:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 { 4 byte[] holder = new byte[32*1024*1024]; 5 holder = null; 6 } 7 System.gc(); 8 } 9 }
此次获得的垃圾回收信息以下:
[GC 208K->134K(5056K), 0.0017194 secs]
[Full GC 134K->134K(5056K), 0.0124656 secs]
[Full GC 32902K->134K(37828K), 0.0091637 secs]
说明此次holder引用的对象所占的内存被回收了。咱们慢慢来分析。
首先明确一点:holder可否被回收的根本缘由是局部变量表中的Slot是否还存有关于holder数组对象的引用。
在第一次修改中,虽然在holder做用域以外进行回收,可是在此以后,没有对局部变量表的读写操做,holder所占用的Slot尚未被其余变量所复用(回忆Java内存区域与内存溢出一文中关于Slot的讲解),因此做为GC Roots一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,所以GC收集器不会将holder引用的对象内存回收掉。 在第二次修改中,在GC收集器工做前,手动将holder设置为null值,就把holder所占用的局部变量表中的Slot清空了,所以,此次GC收集器工做时将holder以前引用的对象内存回收掉了。
固然,咱们也能够用其余方法来将holder引用的对象内存回收掉,只要复用holder所占用的slot便可,好比在holder做用域以外执行一次读写操做。
为对象赋null值并非控制变量回收的最好方法,以恰当的变量做用域来控制变量回收时间才是最优雅的解决办法。另外,赋null值的操做在通过虚拟机JIT编译器优化后会被消除掉,通过JIT编译后,System.gc()执行时就能够正确地回收掉内存,而无需赋null值。
Java虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序(尤为服务器端)的性能和稳定性有着很是重要的影响。性能调优须要具体状况具体分析,并且实际分析时可能须要考虑的方面不少,这里仅就一些简单经常使用的状况做简要介绍。
除了Java堆和永久代以及直接内存外,还要注意下面这些区域也会占用较多的内存,这些内存的总和会受到操做系统进程最大内存的限制:
一、线程堆栈:可经过-Xss调整大小,内存不足时抛出StackOverflowError(纵向没法分配,即没法分配新的栈帧)或OutOfMemoryError(横向没法分配,即没法创建新的线程)。
二、Socket缓冲区:每一个Socket链接都有Receive和Send两个缓冲区,分别占用大约37KB和25KB的内存。若是没法分配,可能会抛出IOException:Too many open files异常。关于Socket缓冲区的详细介绍参见个人Java网络编程系列中深刻剖析Socket的几篇文章。
三、JNI代码:若是代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。
四、虚拟机和GC:虚拟机和GC的代码执行也要消耗必定的内存。
转自:http://blog.csdn.net/ns_code/article/details/18076173