JVM的stack和heap,JVM内存模型,垃圾回收策略,分代收集,增量收集

(转自:http://my.oschina.net/u/436879/blog/85478html

  在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,咱们从JVM的内存管理原理的角度来认识Stack和Heap,并经过这些原理认清Java中静态方法和静态属性的问题。java

  通常,JVM的内存分为两部分:Stack和Heap。web

  Stack(栈)是JVM的内存指令区。Stack管理很简单,push必定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop必定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,而且每次操做的数据或者指令字节长度是已知的。因此Java 基本数据类型,Java 指令代码,常量都保存在Stack中。算法

  Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配必定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象自己的类型标记等,并不保存对象的方法(方法是指令,保存在 Stack中),在Heap 中分配必定的内存保存对象实例和对象的序列化比较相似。而对象实例在Heap 中分配好之后,须要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。数据库

  因为Stack的内存管理是顺序分配的,并且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题; 所以在JVM中另有一个GC进程,按期扫描Heap ,它根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),而且假设Heap 中没有扫描到的区域都是空闲的,通通refresh(其实是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程;关于垃圾收集的更 深刻讲解请参考51CTO以前的文章《JVM内存模型及垃圾收集策略解析》。编程


JVM的体系结构数组

  咱们首先要搞清楚的是什么是数据以及什么是指令。而后要搞清楚对象的方法和对象的属性分别保存在哪里。缓存

  1)方法自己是指令的操做码部分,保存在Stack中;服务器

  2)方法内部变量做为指令的操做数部分,跟在指令的操做码以后,保存在Stack中(其实是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操做码和指令操做数构成了完整的Java 指令。session

  3)对象实例包括其属性值做为数据,保存在数据区Heap 中。

  非静态的对象属性做为对象实例的一部分保存在Heap 中,而对象实例必须经过Stack中保存的地址指针才能访问到。所以可否访问到对象实例以及它的非静态属性值彻底取决于可否得到对象实例在Stack中的地址指针。

  非静态方法和静态方法的区别:

  非静态方法有一个和静态方法很重大的不一样:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和咱们怎么写代码无关,这个隐含的参数就是对象实 例在Stack中的地址指针。所以非静态方法(在Stack中的指令代码)老是能够找到本身的专用数据(在Heap 中的对象属性值)。固然非静态方法也必须得到该隐含参数,所以非静态方法在调用前,必须先new一个对象实例,得到Stack中的地址指针,不然JVM将 没法将隐含参数传给非静态方法。

  静态方法无此隐含参数,所以也不须要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法便可被调用。固然此时静态方法是存取不到Heap 中的对象属性的。

  总结一下该过程:当 一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。而后程序技术器开始执行指令,若是是静态方法,直接依次执行指令代码,固然此时指令代码是不能访问Heap 数据区的;若是是非静态方法,因为隐含参数没有值,会报错。所以在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时可以访问到Heap 数据区了。

  静态属性和动态属性:

  前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须经过Stack中的地址指针才可以被指令(类的方法)访问到。所以能够推断出:静态属性是保存在Stack中的,而不一样于动态属性保存在Heap 中。正由于都是在Stack中,而Stack中指令和数据都是定长的,所以很容易算出偏移量,也所以无论什么指令(类的方法),均可以访问到类的静态属 性。也正由于静态属性被保存在Stack中,因此具备了全局属性。

  在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。

 

JVM内存模型是Java的核心技术之一,以前51CTO曾为你们介绍过JVM分代垃圾回收策略的基础概念,如今不少编程语言都引入了相似Java JVM的内存模型和垃圾收集器的机制,下面咱们将主要针对Java中的JVM内存模型及垃圾收集的具体策略进行综合的分析。

一 JVM内存模型

1.1 Java栈

Java栈是与每个线程关联的,JVM在建立每个线程的时候,会分配必定的栈空间给线程。它主要用来存储线程执行过程当中的局部变量,方法的返回值,以 及方法调用上下文。栈空间随着线程的终止而释放。StackOverflowError:若是在线程执行的过程当中,栈空间不够用,那么JVM就会抛出此异 常,这种状况通常是死递归形成的。

1.2 堆

Java中堆是由全部的线程共享的一块内存区域,堆用来保存各类JAVA对象,好比数组,线程对象等。

1.2.1 Generation

JVM堆通常又能够分为如下三部分:

 

◆ Perm

Perm代主要保存class,method,filed对象,这部门的空间通常不会溢出,除非一次性加载了不少的类,不过在涉及到热部署的应用服务器的 时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,形成这个错误的很大缘由就有多是每次都从新部署,可是从新部署后,类的class没有被卸载掉,这样就形成了大量的class对象保存在了 perm中,这种状况下,通常从新启动应用服务器能够解决问题。

◆ Tenured

Tenured区主要保存生命周期长的对象,通常是一些老的对象,当一些对象在Young复制转移必定的次数之后,对象就会被转移到Tenured区,通常若是系统中用了application级别的缓存,缓存中的对象每每会被转移到这一区间。

◆ Young

Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另一 个留作垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在通过几回垃圾收集后,任然存活于Survivor的对象将被移动到 Tenured区间。

1.2.2 Sizing the Generations

JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。

◆ Total Heap

-Xms :指定了JVM初始启动之后初始化内存

-Xmx:指定JVM堆得最大内存,在JVM启动之后,会分配-Xmx参数指定大小的内存给JVM,可是不必定所有使用,JVM会根据-Xms参数来调节真正用于JVM的内存

-Xmx -Xms之差就是三个Virtual空间的大小

◆ Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9

堆内存

-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.

-Xmn 参数设置了年轻代的大小

◆ Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

1.3 堆栈分离的好处

呵呵,其它的先不说了,就来讲说面向对象的设计吧,固然除了面向对象的设计带来的维护性,复用性和扩展性方面的好处外,咱们看看面向对象如何巧妙的利用了 堆栈分离。若是从JAVA内存模型的角度去理解面向对象的设计,咱们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而咱们编写的那些方法通常都 是运行在栈中,所以面向对象的设计是一种很是完美的设计方式,它完美的统一了数据存储和运行。

二 JAVA垃圾收集器

2.1 垃圾收集简史

垃圾收集提供了内存管理的机制,使得应用程序不须要在关注内存如何释放,内存用完后,垃圾收集会进行收集,这样就减轻了由于人为的管理内存而形成的错误, 好比在C++语言里,出现内存泄露时很常见的。Java语言是目前使用最多的依赖于垃圾收集器的语言,可是垃圾收集器策略从20世纪60年代就已经流行起 来了,好比Smalltalk,Eiffel等编程语言也集成了垃圾收集器的机制。

2.2 常见的垃圾收集策略

全部的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了,而在 JAVA中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在全部线程栈的对象的引用)引用或者对象被另外一个可到达 的对象引用。

2.2.1 Reference Counting(引用计数)
 
引用计数是最简单直接的一种方式,这种方式在每个对象中增长一个引用的计数,这个计数表明当前程序有多少个引用引用了此对象,若是此对象的引用计数变为0,那么此对象就能够做为垃圾收集器的目标对象来收集。

优势:

简单,直接,不须要暂停整个应用

缺点:

1.须要编译器的配合,编译器要生成特殊的指令来进行引用计数的操做,好比每次将对象赋值给新的引用,或者者对象的引用超出了做用域等。

2.不能处理循环引用的问题

2.2.2 跟踪收集器

跟踪收集器首先要暂停整个应用程序,而后开始从根对象扫描整个堆,判断扫描的对象是否有对象引用,这里面有三个问题须要搞清楚:

1.若是每次扫描整个堆,那么势必让GC的时间变长,从而影响了应用自己的执行。所以在JVM里面采用了分代收集,在新生代收集的时候minor gc只须要扫描新生代,而不须要扫描老生代。

2.JVM采用了分代收集之后,minor gc只扫描新生代,可是minor gc怎么判断是否有老生代的对象引用了新生代的对象,JVM采用了卡片标记的策略,卡片标记将老生代分红了一块一块的,划分之后的每个块就叫作一个卡 片,JVM采用卡表维护了每个块的状态,当JAVA程序运行的时候,若是发现老生代对象引用或者释放了新生代对象的引用,那么就JVM就将卡表的状态设 置为脏状态,这样每次minor gc的时候就会只扫描被标记为脏状态的卡片,而不须要扫描整个堆。具体以下图:
3.GC在收集一个对象的时候会判断是否有引用指向对象,在JAVA中的引用主要有四种:Strong reference,Soft reference,Weak reference,Phantom reference.

◆ Strong Reference

强引用是JAVA中默认采用的一种方式,咱们平时建立的引用都属于强引用。若是一个对象没有强引用,那么对象就会被回收。

  1. public void testStrongReference(){  
  2. Object referent = new Object();  
  3. Object strongReference = referent;  
  4. referent = null;  
  5. System.gc();  
  6. assertNotNull(strongReference);  

◆ Soft Reference

软引用的对象在GC的时候不会被回收,只有当内存不够用的时候才会真正的回收,所以软引用适合缓存的场合,这样使得缓存中的对象能够尽可能的再内存中待长久一点。

  1. Public void testSoftReference(){  
  2. String  str =  "test";  
  3. SoftReference<String> softreference = new SoftReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNotNull(softreference.get());  
  7. }  

Weak reference

弱引用有利于对象更快的被回收,假如一个对象没有强引用只有弱引用,那么在GC后,这个对象确定会被回收。

  1. Public void testWeakReference(){  
  2. String  str =  "test";  
  3. WeakReference<String> weakReference = new WeakReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNull(weakReference.get());  
  7. }  

Phantom reference

 

回收算法转自http://pengjiaheng.iteye.com/blog/520228

按照基本回收策略分

引用计数(Reference Counting):

比较古老的回收算法。原理是此对象有一个引用,即增长一个计数,删除一个引用则减小一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是没法处理循环引用的问题。

 

标记-清除(Mark-Sweep):

 

 

此算法执行分两阶段。第一阶段从引用根节点开始标记全部被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法须要暂停整个应用,同时,会产生内存碎片。

 

复制(Copying):

 

 

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。次算法每次只处 理正在使用中的对象,所以复制成本比较小,同时复制过去之后还能进行相应的内存整理,不会出现“碎片”问题。固然,此算法的缺点也是很明显的,就是须要两 倍内存空间。

 

标记-整理(Mark-Compact):

 

 

此算法结合了“标记-清除”和“复制”两个算法的优势。也是分两阶段,第一阶段从根节点开始标记全部被引用对象,第二阶段遍历整个堆,把清除未标记对象并 且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

按分区对待的方式分

增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么缘由JDK5.0中的收集器没有使用这种算法的。

 

分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不一样生命周期的对象使用不一样的算法(上述方式中的一个)进行回收。如今的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

 

按系统线程分

串行收集:串行收集使用单线程处理全部垃圾回收工做,由于无需多线程交互,实现容易,并且效率比较高。可是,其局限性也比较明显,即没法使用多处理器的优点,因此此收集适合单处理器机器。固然,此收集器也能够用在小数据量(100M左右)状况下的多处理器机器上。

 

并行收集:并行收集使用多线程处理垃圾回收工做,于是速度快,效率高。并且理论上CPU数目越多,越能体现出并行收集器的优点。

 

并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工做时,须要暂停整个运行环境,而只有垃圾回收程序在运行,所以,系统在垃圾回收时会有明显的暂停,并且暂停时间会由于堆越大而越长。

 

如何区分垃圾

 

    上面说到的“引用计数”法,经过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象便可。可是这种方法没法解决循环引用。所 以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始的呢? 即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,因此要获取哪些对象正在被使用,则须要从 Java栈开始。同时,一个栈是与一个线程对应的,所以,若是有多个线程的话,则必须对这些线程对应的全部的栈进行检查。

    同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,咱们能够找到堆中的对象,又从这些对象找到对 堆中其余对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就造成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如 果栈中有多个引用,则最终会造成多颗对象树。在这些对象树上的对象,都是当前系统运行所须要的对象,不能被垃圾回收。而其余剩余对象,则能够视为没法被引 用到的对象,能够被当作垃圾进行回收。

所以,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器...)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式

 

如何处理碎片

   因为不一样Java对象存活时间是不必定的,所以,在程序运行一段时间之后,若是不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会致使没法 分配大块的内存空间,以及程序运行效率下降。因此,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,均可以解决碎片的问题。

 

如何解决同时存在的对象建立和对象回收问题

    垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,二者是矛盾的。所以,在现有的垃圾回收方式中,要进行垃圾回收前,通常都须要暂停整个应用(即:暂停内存的分配),而后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,并且最有效的解决两者矛盾的方式。

可是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,好比最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就颇有可能超过这个限制,在这种状况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,可是由于须要在新生成对象的同时又要回收对象,算法复杂性会大大增长,系统的处理能力也会相应下降,同时,“碎片”问题将会比较难解决。



因为不一样对象的生命周期不同,所以在JVM的垃圾回收策略中有分代这一策略。本文介绍了分代策略的目标,如何分代,以及垃圾回收的触发因素。

文章总结了JVM垃圾回收策略为何要分代,如何分代,以及垃圾回收的触发因素。

为何要分代

        分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。

        在Java程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关,好比Http请求中的Session对象、线程、Socket链接, 这类对象跟业务直接挂钩,所以生命周期比较长。可是还有一些对象,主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短,好比:String对 象,因为其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次便可回收。

        试想,在不进行对象存活时间区分的状况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,由于每次回收都须要遍历全部存活对象,但 实际上,对于生命周期长的对象而言,这种遍历是没有效果的,由于可能进行了不少次遍历,可是他们依旧存在。所以,分代垃圾回收采用分治的思想,进行代的划 分,把不一样生命周期的对象放在不一样代上,不一样代上采用最适合它的垃圾回收方式进行回收。

如何分代

如图所示:

如何分代 

        虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比 较大的。

年轻代:

        全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(通常而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制 过来的而且此时还存活的对象,将被复制“年老区(Tenured)”。须要注意,Survivor的两个区是对称的,没前后关系,因此同一个区中可能同时 存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。并且,Survivor区总有一个是空 的。同时,根据程序须要,Survivor区是能够配置为多个的(多于两个),这样能够增长对象在年轻代中的存在时间,减小被放到年老代的可能。

年老代:

        在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。

持久代:

        用于存放静态文件,现在Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如 Hibernate等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。持久代大小经过-XX:MaxPermSize=& lt;N>进行设置。

什么状况下触发垃圾回收

        因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

        通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对 年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。因 而,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

          对 整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个对进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于FullGC的调节。有以下缘由可能致使Full GC:

 

· 年老代(Tenured)被写满

· 持久代(Perm)被写满

· System.gc()被显示调用

·上一次GC以后Heap的各域分配策略动态变化

常见配置汇总

堆设置

  -Xms:初始堆大小

  -Xmx:最大堆大小

  -XX:NewSize=n:设置年轻代大小

  -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

  -XX:MaxPermSize=n:设置持久代大小

收集器设置

  -XX:+UseSerialGC:设置串行收集器

  -XX:+UseParallelGC:设置并行收集器

  -XX:+UseParalledlOldGC:设置并行年老代收集器

  -XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

  -XX:+PrintGC

  -XX:+PrintGCDetails

  -XX:+PrintGCTimeStamps

  -Xloggc:filename

并行收集器设置

  -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU状况。

  -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

 

调优总结

年轻代大小选择

响应时间优先的应用:尽量设大,直到接近系统的最低响应时间限制(根据实际状况选择)。在此种状况下,年轻代收集发生的频率也是最小的。同时,减小到达年老代的对象。

吞吐量优先的应用:尽量的设置大,可能到达Gbit的程度。由于对响应时间没有要求,垃圾收集能够并行进行,通常适合8CPU以上的应用。

 

 

年老代大小选择

 

响应时间优先的应用:年老代使用并发收集器,因此其大小须要当心设置,通常要考虑并发会话率会话持续时间等一些参数。若是堆设置小了,能够会形成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;若是堆大了,则须要较长的收集时间。最优化的方案,通常须要参考如下数据得到:

  1. 并发垃圾收集信息

  2. 持久代并发收集次数

  3. 传统GC信息

  4. 花在年轻代和年老代回收上的时间比例

减小年轻代和年老代花费的时间,通常会提升应用的效率

 

 

吞吐量优先的应用

通常吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。缘由是,这样能够尽量回收掉大部分短时间对象,减小中期的对象,而年老代尽存放长期存活对象。

 

 

较小堆引发的碎片问题

由于年老代的并发收集器使用标记、清除算法,因此不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样能够分配给较大的对象。可是,当堆空 间较小时,运行一段时间之后,就会出现“碎片”,若是并发收集器找不到足够的空间,那么并发收集器将会中止,而后使用传统的标记、清除方式进行回收。若是 出现“碎片”,可能须要进行以下配置:

    1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

    2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的状况下,这里设置多少次Full GC后,对年老代进行压缩

 

垃圾回收的瓶颈

    传统分代垃圾回收方式,已经在必定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。可是他没法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是没法接受的。这类应用可能要求请求的返回时间在几百甚 至几十毫秒之内,若是分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,可是这样有限制了应用自己的处理能力,一样也是不可 接收的。

    分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,可是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。

    为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。能够很好的解决传统分代方式带来的问题。

 

增量收集的演进

    增量收集的方式在理论上能够解决传统分代方式带来的问题。增量收集把对堆空间划分红一系列内存块,使用时,先使用其中一部分(不会所有用完),垃圾收集时 把以前用掉的部分中的存活对象再放到后面没有用的空间中,这样能够实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的状况。

    固然,传统分代收集方式也提供了并发收集,可是他有一个很致命的地方,就是把整个堆作为一个内存块,这样一方面会形成碎片(没法压缩),另外一方面他的每次 收集都是对整个堆的收集,没法进行选择,在暂停时间的控制上仍是很弱。而增量方式,经过内存空间的分块,偏偏能够解决上面问题。

 

 

Garbage Firest(G1)

这部分的内容主要参考这里,这篇文章算是对G1算法论文的解读。我也没加什么东西了。

目标

从设计目标看G1彻底是为了大型应用而准备的。

支持很大的堆

高吞吐量

  --支持多CPU和垃圾回收线程

  --在主线程暂停的状况下,使用并行收集

  --在主线程运行的状况下,使用并发收集

实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收

固然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。

 

 

算法详解

    G1可谓博采众家之长,力求到达一种完美。他吸收了增量收集优势,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以 region为单位;同时,他也吸收了CMS的特色,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;并且,G1也认同分代垃圾回收的思想,认为 不一样对象的生命周期不一样,能够采起不一样收集方式,所以,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region之后,对其中的 活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),由于活跃对象小,里面能够认为多数都是垃 圾,因此这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

 

 

回收步骤:

 

初始标记(Initial Marking)

    G1对于每一个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。

    开始Initial Marking以前,首先并发的清空next marking bitmap,而后中止全部应用线程,并扫描标识出每一个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,以后恢复全部应用线程。

    触发这个步骤执行的条件为:

    G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改成动态的,根据jvm的运行状况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用容许的GC暂停时间范围内尽快的执行此步骤;

    在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收可以带来最多内存空间的regions,当通过屡次的clean up,回收到没多少空间的regions时,G1从新初始化一个新的marking与clean up构成的环。

 

并发标记(Concurrent Marking)

    按照以前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新建立的对象则放入比top值更高的地址区间中,这些新建立的对象默认状态即为活跃的,同时修改top值。

 

 

最终标记暂停(Final Marking Pause)

    当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的状况下,这些remebered set logs中记录的card的修改就会被更新了,所以须要这一步,这一步要作的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步须要暂停应用,并行的运行。

 

 

存活对象计算及清除(Live Data Counting and Cleanup)

    值得注意的是,在G1中,并非说Final Marking Pause执行完了,就确定执行Cleanup这步的,因为这步须要暂停应用,G1为了可以达到准实时的要求,须要根据用户指定的最大的GC形成的暂停时 间来合理的规划何时执行Cleanup,另外还有几种状况也是会触发这个步骤的执行的:

    G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,所以G1采起的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;

    对于full-young和partially-young的分代模式的G1而言,则还有状况会触发Cleanup的执行,full-young模式 下,G1根据应用可接受的暂停时间、回收young regions须要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽可能频繁的在应用可接受的暂停时间范围内执行 Cleanup,并最大限度的去执行non-young regions的Cleanup。

 

 

展望

    之后JVM的调优或许跟多须要针对G1算法进行调优了。

 

垃圾回收的悖论

    所谓“成也萧何败萧何”。Java的垃圾回收确实带来了不少好处,为开发带来了便利。可是在一些高性能、高并发的状况下,垃圾回收确成为了制约Java应 用的瓶颈。目前JDK的垃圾回收算法,始终没法解决垃圾回收时的暂停问题,由于这个暂停严重影响了程序的相应时间,形成拥塞或堆积。这也是后续JDK增长 G1算法的一个重要缘由。

    固然,上面是从技术角度出发解决垃圾回收带来的问题,可是从系统设计方面咱们就须要问一下了:

    咱们须要分配如此大的内存空间给应用吗?

    咱们是否可以经过有效使用内存而不是经过扩大内存的方式来设计咱们的系统呢?    

 

咱们的内存中都放了什么

    内存中须要放什么呢?我的认为,内存中须要放的是你的应用须要在不久的未来再次用到到的东西。想一想看,若是你在未来不用这些东西,何须放内存呢?放文件、数据库不是更好?这些东西通常包括:

1. 系统运行时业务相关的数据。好比web应用中的session、即时消息的session等。这些数据通常在一个用户访问周期或者一个使用过程当中都须要存在。

2. 缓存。缓存就比较多了,你所要快速访问的均可以放这里面。其实上面的业务数据也能够理解为一种缓存。

3.  线程。

    所以,咱们是否是能够这么认为,若是咱们不把业务数据和缓存放在JVM中,或者把他们独立出来,那么Java应用使用时所需的内存将会大大减小,同时垃圾回收时间也会相应减小。

    我认为这是可能的。

 

 

解决之道

 

数据库、文件系统

    把全部数据都放入数据库或者文件系统,这是一种最为简单的方式。在这种方式下,Java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取都在每次请求时从数据库和文件系统中获取。也能够理解为,一次业务访问之后,全部对象均可以进行回收了。

    这是一种内存使用最有效的方式,可是从应用角度来讲,这种方式很低效。

 

 

内存-硬盘映射

    上面的问题是由于咱们使用了文件系统带来了低效。可是若是咱们不是读写硬盘,而是写内存的话效率将会提升不少。

    数据库和文件系统都是实实在在进行了持久化,可是当咱们并不须要这样持久化的时候,咱们能够作一些变通——把内存当硬盘使。

    内存-硬盘映射很好很强大,既用了缓存又对Java应用的内存使用又没有影响。Java应用仍是Java应用,他只知道读写的仍是文件,可是其实是内存。

    这种方式兼得的Java应用与缓存两方面的好处。memcached的普遍使用也正是这一类的表明。

 

 

同一机器部署多个JVM

    这也是一种很好的方式,能够分为纵拆和横拆。纵拆能够理解为把Java应用划分为不一样模块,各个模块使用一个独立的Java进程。而横拆则是一样功能的应用部署多个JVM。

    经过部署多个JVM,能够把每一个JVM的内存控制一个垃圾回收能够忍受的范围内便可。可是这至关于进行了分布式的处理,其额外带来的复杂性也是须要评估的。另外,也有支持分布式的这种JVM能够考虑,不要要钱哦:)

 

 

程序控制的对象生命周期

    这种方式是理想当中的方式,目前的虚拟机尚未,纯属假设。即:考虑由编程方式配置哪些对象在垃圾收集过程当中能够直接跳过,减小垃圾回收线程遍历标记的时间。

    这种方式至关于在编程的时候告诉虚拟机某些对象你能够在*时间后在进行收集或者由代码标识能够收集了(相似C、C++),在这以前你即使去遍历他也是没有效果的,他确定是还在被引用的。

    这种方式若是JVM能够实现,我的认为将是一个飞跃,Java即有了垃圾回收的优点,又有了C、C++对内存的可控性。

 

 

线程分配

    Java的阻塞式的线程模型基本上能够抛弃了,目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增加,而NIO则能够转换成为常数 线程。所以,对于服务端的应用而言,NIO仍是惟一选择。不过,JDK7中为咱们带来的AIO是否能让人眼前一亮呢?咱们拭目以待。

 

 

其余的JDK

    本文说的都是Sun的JDK,目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高不少,不过Sun JDK6.0之后提升也很大。并且JRocket在垃圾回收方面,也具备优点,其可设置垃圾回收的最大暂停时间也是很吸引人的。不过,系统Sun的G1实 现之后,在这方面会有一个质的飞跃。

相关文章
相关标签/搜索