标签:guava 处理器 可用内存 weakref 难解 研究 自适应 引用计数器 .com
Java 中的垃圾回收一般是在 Java 堆中进行,因为堆中几乎存放了 Java 中所有的对象实例。谈到 Java 堆中的垃圾回收,自然要谈到引用。在 JDK1.2 之前,Java 中的引用定义很很纯粹:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但在 JDK1.2 之后,Java 对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。
其中软引用和弱引用经常被用来实现缓存,比如Guava中的缓存实现:
public static <K,V> Cache<K , V> callableCached() throws Exception { Cache<K, V> cache = CacheBuilder .newBuilder() .maximumSize(10000) .softValues() //使用软引用 .expireAfterWrite(10, TimeUnit.MINUTES) .build(); return cache; }
如何判断一个对象是垃圾对象?一般有2种方法,引用计数和引用链法。
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1,当引用失效时,计数器值就减1,任何时刻计数器都为 0 的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,当 Java 语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。
Java 和 C# 中都是采用根搜索算法来判定对象是否存活的。这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。在 Java 语言里,可作为 GC Roots 的兑现包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中 JNI(Native 方法)的引用对象。
实际上,要真正宣告对象私网,至少要经历2次标记。对象还有最后一次逃脱死亡的机会,java中的finalize()方法,只要在finalize()中和一个对象建立引用就可以逃脱,但是非常不建议这么做。
将可用内存按容量划分为大小相等的"两块",每次只使用其中一块,用空间换时间。当其中一块用完之后,就将存活的对象一次性全部移动到另外一块上,再把这一块清空作为备用。由于是针对整个的内存块进行回收,不会有内存碎片问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
新生代一般都采用这种算法!IBM公司的专门研究表明98%的对象都是"朝生夕死"的,不需要按照1:1来划分内存空间,因此将一块大内存分为3块,1块Eden和2块survior,比例是8:1:1,因此每次最多浪费10%的空间。
当然还需要内存担保机制来保证特殊情况,不一定每次都是98%那么多对象死亡。
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
这种算法容易导致清除后由于空间不连续容易引起碎片,且没有足够的连续的空间去安排新对象而频繁引发GC。
标记—清除算法的执行情况如下图所示:
回收前状态:
回收后状态:
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
回收前状态:
回收后状态:
当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
要了解java垃圾回收机制前必须知道java怎么分配给对象内存的,根据上面运行时数据区域的划分可以知道,几乎所有的对象都在堆上分配,而类信息、常量、静态变量在方法区分配。堆内存是分代管理的,
Java大多数的垃圾回收器都是分代的,即有的收集器适合于新生代,有的适合于老年代,比较特殊有G1收集器。
新生代的GC往往很正常,速度也非常快,老年代的则不同,老年代的GC称为Major GC/Full GC:
下图展示了JDK中常见的GC收集器以及他们的组合,注意到CMS无法跟Parallel Scavenge一起工作...
常见收集器简介:
关于CMS和G1会在后面详细介绍。
下面来做个小实验,如何显示的进行垃圾回收?一般不建议在代码进行显示垃圾回收。
public class SlotGc{ public static void main(String[] args){ byte[] holder = new byte[32*1024*1024]; System.gc(); } }
使用java -verbose:gc 打印简单的gc信息
[GC 208K->134K(5056K), 0.0017306 secs] [Full GC 134K->134K(5056K), 0.0121194 secs] [Full GC 32902K->32902K(37828K), 0.0094149 sec
发现没有变化.没有将32M内存回收。
上面的原因是gc的时候holder的作用域仍然有效,继续修改代码:
public class SlotGc{ public static void main(String[] args){ { byte[] holder = new byte[32*1024*1024]; } System.gc(); } }
[GC 208K->134K(5056K), 0.0017100 secs] [Full GC 134K->134K(5056K), 0.0125887 secs] [Full GC 32902K->32902K(37828K), 0.0089226 secs]
修改作用域之后,发现依旧没有回收. 继续修改代码:
public class SlotGc{ public static void main(String[] args){ { byte[] holder = new byte[32*1024*1024]; holder = null; } System.gc(); } }
[GC 208K->134K(5056K), 0.0017194 secs] [Full GC 134K->134K(5056K), 0.0124656 secs] [Full GC 32902K->134K(37828K), 0.0091637 secs]
终于回收了,为什么?
首先明确一点:holder 能否被回收的根本原因是局部变量表中的 Slot 是否还存有关于 holder 数组对象的引用。
在第一次修改中,虽然在 holder 作用域之外进行回收,但是在此之后,没有对局部变量表的读写操作,holder 所占用的 Slot 还没有被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,因此 GC 收集器不会将 holder 引用的对象内存回收掉。
在第二次修改中,在 GC 收集器工作前,手动将 holder 设置为 null 值,就把 holder 所占用的局部变量表中的 Slot 清空了,因此,这次 GC 收集器工作时将 holder 之前引用的对象内存回收掉了。
当然,我们也可以用其他方法来将 holder 引用的对象内存回收掉,只要复用 holder 所占用的 slot 即可,比如在 holder 作用域之外执行一次读写操作。
为对象赋 null 值并不是控制变量回收的最好方法,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。另外,赋 null 值的操作在经过虚拟机 JIT 编译器优化后会被消除掉,经过 JIT 编译后,System.gc()执行时就可以正确地回收掉内存,而无需赋 null 值。
在串行收集器进行垃圾回收时,Java 应用程序中的线程都需要暂停,等待垃圾回收的完成,这样给用户体验造成较差效果。
虽然如此,串行收集器却是一个成熟、经过长时间生产环境考验的极为高效的收集器。新生代串行处理器使用复制算法,实现相对简单,逻辑处理特别高效,且没有线程切换的开销。
强调一点:在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,它的性能表现可以超过并行回收器和并发回收器。
在 HotSpot 虚拟机中,使用-XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。当 JVM 在 Client 模式下运行时,它是默认的垃圾收集器。一次新生代串行收集器的工作输出日志类似如清单 1 信息 (使用-XX:+PrintGCDetails 开关) 所示。
一次新生代串行收集器的工作输出日志:
[GC [DefNew: 3468K->150K(9216K), 0.0028638 secs][Tenured: 1562K->1712K(10240K), 0.0084220 secs] 3468K->1712K(19456K), [Perm : 377K->377K(12288K)], 0.0113816 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
简单解释:
最前面的GC:表示垃圾收集的停顿类型,特别注意不是用来来区分Full GC的,如果有Full,表明的是发生"Stop the world"的,新生代GC也会出现Full。如果是调用System.gc()触发的收集,会出现[Full GC (System)]
接下来的DefNew, Tenured, Perm表示GC发生的区域,这里显示的名字和垃圾回收器密切相关,DefNew: Default New Generation表示是Serial新生代收集器,如果是ParNew,Parllel New Generation表示是新生代并行回收器,还有PSYoungGen。
所有的类似于 3468K->150K(9216k),表示是GC前java堆使用容量->GC后java堆使用容量(Java堆总容量)。
老年代串行收集器使用的是标记-压缩算法。
和新生代串行收集器一样,它也是一个串行的、独占式的垃圾回收器。由于老年代垃圾回收通常会使用比新生代垃圾回收更长的时间,因此,在堆空间较大的应用程序中,一旦老年代串行收集器启动,应用程序很可能会因此停顿几秒甚至更长时间。
虽然如此,老年代串行回收器可以和多种新生代回收器配合使用,同时它也可以作为 CMS 回收器的备用回收器。若要启用老年代串行回收器,可以尝试使用以下参数:-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。
新生代的垃圾收集器,它只简单地将串行回收器多线程化。它的回收策略、算法以及参数和串行回收器一样。
并行回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的 CPU 上,它产生的停顿时间要短于串行回收器,而在单 CPU 或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。
老年代的并行回收收集器也是一种多线程并发的收集器。和新生代并行回收收集器一样,它也是一种关注吞吐量的收集器。老年代并行回收收集器使用标记-压缩算法,JDK1.6 之后开始启用。
并行收集器工作时的线程数量可以使用-XX:ParallelGCThreads 参数指定。一般,最好与 CPU 数量相当,避免过多的线程数影响垃圾收集性能。在默认情况下,当 CPU 数量小于 8 个,ParallelGCThreads 的值等于 CPU 数量,大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU_Count]/8]。
Parallel Savenge也是一个新生代的收集器,它也是使用复制算法的收集器,又是并行的收集器。
它的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可以控制的吞吐量。
吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
该收集器提供了2个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
除了这2个参数之外,该收集器还支持自动的细节参数,即-XX:+UseAdaptiveSizePolicy,虚拟机会根据当前系统的运行情况来收集性能监控信息,动态调整虚拟机参数以提供最合适的参数,这种方式成为虚拟机的GC自适应调节策略。这是一个不错的选择,只需要把基本的内存数据设置好,比如最大堆再开启该参数即可。
周志明:《深入理解JVM虚拟机》
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
- 初始标记(CMS initial mark): 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快 Stop the world!
- 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing的过程。
- 重新标记(CMS remark): 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,Stop the world!。
- 并发清除(CMS concurrent sweep):
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。,而由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。
CMS是一款优秀的收集器,它的最主要优点在名字上已经体现出来了:并发收集、低停顿,Sun的一些官方文档里面也称之为并发低停顿收集器(Concurrent Low Pause Collector)。但是CMS还远达不到完美的程度,它有以下三个显著的缺点:
标签:guava 处理器 可用内存 weakref 难解 研究 自适应 引用计数器 .com
原文地址:http://www.cnblogs.com/carl10086/p/6081034.html