http://www.cnblogs.com/leesf456/p/5218594.html
http://jbutton.iteye.com/blog/1569746
http://blog.csdn.net/hqq2023623/article/details/50993165
《并发的艺术》中大概是这样写的,系统中有多个任务同时存在可称之为“并发”,系统内有多个任务同时执行可称之为“并行”;并发是并行的子集。比如在单核CPU系统上,只可能存在并发而不可能存在并行。
三、垃圾收集算法
任何垃圾收集算法都必须做两件事情。首先,它必须检测出垃圾对象。其次,它必须回收垃圾对象所使用的堆空间并还给程序。那么问题来了,如何检测出一个对象是否为垃圾对象呢?一般有两种算法解决这个问题。1. 引用计数算法 2. 可达性分析算法。
1.引用计数算法
堆中的每一个对象有一个引用计数,当一个对象被创建,并把指向该对象的引用赋值给一个变量时,引用计数置为1,当再把这个引用赋值给其他变量时,引用计数加1,当一个对象的引用超过了生命周期或者被设置为新值时,对象的引用计数减1,任何引用计数为0的对象都可以被当成垃圾回收。当一个对象被回收时,它所引用的任何对象计数减1,这样,可能会导致其他对象也被当垃圾回收。
问题:很难检测出对象之间的额相互引用(引用循环问题)
注意:局部变量区的第一项并没有this引用,因为testGC方法是类方法。
在代码objA = null 和 objB = null 之后,内存结构示意图如下
objA和objB到堆对象的引用已经没有了,但是ReferenceCountingGC对象内部还存在着循环引用,我们在图中也可以看到。即便如此,JVM还是把这两个对象当成垃圾进行了回收。具体的GC日志如下:
由GC日志可知发生了两次GC,由11390K -> 514K,即对两个对象都进行了回收,也从侧面说明JVM的垃圾收集器不是采用的引用计数的算法来进行垃圾回收的。
2.可达性分析算法
此算法的基本思想就是选取一系列GCRoots对象作为起点,开始向下遍历搜索其他相关的对象,搜索所走过的路径成为引用链,遍历完成后,如果一个对象到GCRoots对象没有任何引用链,则证明此对象是不可用的,可以被当做垃圾进行回收。
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:
1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2. 方法区中的类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI(Native方法)引用的对象。
下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。
由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。
四、对象的内存布局
Java中我们提到最多的应该就是对象,但是我们真的了解对象吗,对象在内存中的存储布局如何?对象的内存布局如下图所示
说完对象的内存布局,现在来说说对象的引用,当我们在堆上创建一个对象实例后,如何对该对象进行操作呢?好比一个电视机,我如何操作电视机来收看不同的电视节目,显然我们需要使用到遥控,而虚拟机中就是使用到引用,即虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种:
1. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象示例数据与类型数据的具体地址信息,相当于二级指针。
2. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。
两种方式有各自的优缺点。当垃圾回收移动对象时,对于方式一而言,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;而对于方式二,则需要修改reference中存储的地址。从访问效率上看,方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。下图是句柄访问与指针访问的示意图。
五、对象的引用
前面所谈到的检测垃圾对象的两种算法都是基于对象引用。在Java语言中,将引用分为强引用、软引用、弱引用、虚引用四种类型。引用强度依次减弱。具体如下图所示
对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。
七、垃圾收集算法
垃圾收集的主要算法有如下几种:
1. 标记 - 清除算法
2. 复制算法
3. 标记 - 整理算法
4. 分代收集算法
7.1 标记 - 清除算法
首先标记出所有需要回收的对象,使用可达性分析算法判断一个对象是否为可回收,在标记完成后统一回收所有被标记的对象。下图是算法具体的一次执行过程后的结果对比。
说明:1.效率问题,标记和清除两个阶段的效率都不高。2.空间问题,标记清除后会产生大量不连续的内存碎片,以后需要给大对象分配内存时,会提前触发一次垃圾回收动作。
7.2 复制算法
将内存分为两等块,每次使用其中一块。当这一块内存用完后,就将还存活的对象复制到另外一个块上面,然后再把已经使用过的内存空间一次清理掉。图是算法具体的一次执行过程后的结果对比。
说明:1.无内存碎片问题。2.可用内存缩小为原来的一半。 3.当存活的对象数量很多时,复制的效率很慢。
7.3 标记 - 整理算法(综合了标记-清除和复制)
标记过程还是和标记 - 清除算法一样,之后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,标记 - 整理算法示意图如下
说明:1.无需考虑内存碎片问题。
7.4 分代收集算法
把堆分为新生代和老年代,然后根据各年代的特点选择最合适的回收算法。在新生代基本上都是朝生暮死的,生存时间很短暂,因此可以采拥标记 - 复制算法,只需要复制少量的对象就可以完成收集。而老年代中的对象存活率高,也没有额外的空间进行分配担保,因此必须使用标记 - 整理或者标记 - 清除算法进行回收。
八、HotSpot的算法实现
对于可达性分析而言,我们知道,首先需要选取GCRoots结点,而GCRoots结点主要在全局性的引用(如常量或类静态属性)与执行上下文(如栈帧中的局部变量表)中。方法区可以很大,这对于寻找GCRoots结点来说会非常耗时。当选取了GCRoots结点之后,进行可达性分析时必须要保证一致性,即在进行分析的过程中整个执行系统看起来就好像被冻结在某个时间点上,不可以在分析的时候,对象的关系还在动态变化,这样的话分析的准确性就得不到保证,所以可达性分析是时间非常敏感的。
为了保证分析结果的准确性,就会导致GC进行时必须停顿所有Java执行线程(Stop the world),为了尽可能的减少Stop the world的时间,Java虚拟机使用了一组称为OopMap的数据结构,该数据结构用于存放对象引用的地址,这样,进行可达性分析的时候就可以直接访问OopMap就可以获得对象的引用,从而加快分析过程,减少Stop the world时间。
OopMap数据结构有利于进行GC,是不是虚拟机无论何时想要进行GC都可以进行GC,即无论虚拟机在执行什么指令都可以进行GC?答案是否定的,因为要想让虚拟机无论在执行什么指令的时候都可以进行GC的话,需要为每条指令都生成OopMap,显然,这样太浪费空间了。为了节约宝贵的空间,虚拟机只在”特定的位置“存放了OopMap数据结构,这个特定的位置我们称之为安全点。程序执行时并非在所有地方都能够停顿下来开始GC(可达性分析),只有到达安全点的时候才能暂停。安全点可以由方法调用、循环跳转、异常跳转等指令产生,因为这些指令会让程序长时间执行。
现在我们已经知道了安全点的概念,即进行GC必须要到达安全点,那么在发生GC时如何让所有线程到达安全点再暂停呢?有两种方法1. 抢先式中断,在发生GC时,首先把所有线程全部中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。2. 主动式中断,在发生GC时,不中断线程,而是设置一个标志,所有线程执行时主动轮询这个标志,发生标志位真就自己中断挂起,轮询标志的地方和安全点是重合的,也有可能是创建对象需要分配内存的地方。
现在问题又来了,当程序不执行的时候,如何让所有线程达到安全点呢?典型的就是线程处于Sleep状态或者Blocked状态,这时候线程是无法跑到安全点再中断自己的,虚拟机也肯定不可能等待该线程被唤醒并重新分配CPU时间后,跑到安全点再暂停。为了解决这个问题,引入安全区域的概念。安全区域是对安全点的扩展,可以看成由很多安全点组成,安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。当线程执行到安全区域的代码时,首先标示自己已经进入了安全区域,那么,在这段时间里JVM发起GC时,就不用管标示自己为安全区域状态的线程了。在线程奥离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),若完成,线程继续执行;否则,它必须等待直到收到可以安全离开安全区域的信号。
九、垃圾收集器
垃圾收集器是内存回收的具体实现,HotSpot虚拟机包含的所有收集器如下:
说明:图中存在连线表示可以搭配使用,总共有7种不同分代的收集器。
9.1 Serial收集器
Serial收集器为单线程收集器,在进行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束。运行过程如下图所示
说明:1. 需要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
9.2 ParNew收集器
ParNew是Serial的多线程版本,除了使用多线程进行垃圾收集外,其他行为与Serial完全一样,运行过程如下图所示
说明:1.Server模式下虚拟机的首选新生收集器,与CMS进行搭配使用。
9.3 Parallel Scavenge收集器
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,并且虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略。
9.4 Serial Old收集器
老年代的单线程收集器,使用标记 - 整理算法,运行过程在之前的Serial收集器已经给出。不再累赘。
9.5 Parallel Old收集器
老年代的多线程收集器,使用标记 - 整理算法,吞吐量优先,适合于Parallel Scavenge搭配使用,运行过程如下图所示
9.6 CMS收集器
CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
1. 初始标记,标记GCRoots能直接关联到的对象,时间很短。
2. 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
3. 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
4. 并发清除,回收内存空间,时间很长。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示
说明:1. 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降。2. 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。3. 由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长,虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的GC。
9.7 G1收集器
可以在新生代和老年代中只使用G1收集器。具有如下特点。
1. 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
2. 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
3. 空间整合。基于标记 - 整理算法,无内存碎片产生。
4. 可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆会被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但两者已经不是物理隔离了,都是一部分Region(不需要连续)的集合。G1收集器中,Region之间的对象引用以及其他收集器的新生代和老年代之间的对象引用,虚拟机都使用Remembered Set来避免全堆扫描的。每个Region对应一个Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查老年代的对象是否引用了新生代的对象),如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
对于上述过程我们可以看如下代码加深理解
public class G1 {
private Object obj;
public init() {
obj = new Object();
}
public static void main(String[] args) {
G1 g1 = new G1();
g1.init();
}
}
说明:程序中执行init函数的时候,会产生一个Write Barrier暂停中断写操作,此时,假定程序中G1对象与Object对象被分配在不同的Region当中,则会把obj的引用信息记录在Object所属的Remembered Set当中。具体的内存分布图如下
如果不计算维护Remembered Set的操作,G1收集器的运作可以分为如下几步
1. 初始并发,标记GCRoots能直接关联到的对象;修改TAMS(Next Top At Mark Start),使得下一阶段程序并发时,能够在可用的Region中创建新对象,需停顿线程,耗时很短。
2. 并发标记,从GCRoots开始进行可达性分析,与用户程序并发执行,耗时很长。
3. 最终标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,变动的记录将被记录在Remembered Set Logs中,此阶段会把其整合到Remembered Set中,需要停顿线程,与用户程序并行执行,耗时较短。
4. 筛选回收,对各个Region的回收价值和成本进行排序,根据用户期望的GC时间进行回收,与用户程序并发执行,时间用户可控。
G1收集器具体的运行示意图如下
java -Xmx4g -Xms4g -Xmn2g -XX:PermSize=512m -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:-UseBiasedLocking
UseConcMarkSweepGC:激活CMS收集器
UseCMSCompactAtFullCollection