posts - 12, comments - 0, trackbacks - 0, articles - 7
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

《JVM调优总结.pdf》。。。

Posted on 2011-12-27 17:36 cooperzh 阅读(623) 评论(0)  编辑  收藏
作者blog:http://pengjiaheng.iteye.com/

数据类型:基本类型和引用类型
基本类型的变量保存原始值,它代表的值就是数值本身。byte,short,int,long,char,float,double,Boolean,returnAddress
引用类型代表某个对象的引用,存放引用值的地址。类类型,接口类型,数组

栈stack 和 堆heap:
stack是运行时单位,每个线程都会有线程栈与之对应。里面存储的是当前线程相关信息。包括局部变量,程序运行状态,方法返回值等
heap是存储数据的地方,所有线程共享。存放对象信息。

1 从软件设计的角度看,stack代表处理逻辑,heap代表数据。
2 heap的数据被多个线程共享,则多个线程可以访问同一对象。heap的数据可以供所有stack访问,节省了空间
3 stack因为运行时的需要,保存系统运行的上下文,需要进行地址段的划分。并且只能向上增长,因此限制了stack的存储能力。而heap中的数据可以根据需要动态增长,相应stack中只需要记录heap中的一个地址即可。
4 面向对象就是stack和heap的完美结构。对象的属性就是数据,存放在heap中。而对象的方法就是运行逻辑,放在stack中。

在java中,main函数就是stack的起点,也是程序的起点。

heap中存放的是对象实例,stack中是基本数据类型和heap中对象的引用。一个对象的大小是不可以估计的,甚至动态变化的。但是在stack中,一个对象只对应了一个4byte的引用。这就是stack和heap分离的好处。

因为基本数据类型占用的空间是1到8个字节,需要空间比较小,而且不会出现动态增长的情况,因此stack中存储就够了。

java中参数传递时传值还是传引用?
程序永远在stack中运行,因而参数传递的只是基本类型或者对象的引用。不会传递对象本身。
简单说,java在方法调用传递参数时,都是进行传值调用。
当传递引用的时候,程序会将引用查找到heap中的那个对象,这个时候进行修改,修改的是真实的对象,而不是引用本身!!!
所以传递引用也是传递的最终值。
另外,因为传递基本类型是传递了基本值,所以修改的也是另一个copy,而无法修改原值。只有传递的是对象引用时才能修改原对象。

stack是程序运行最根本的东西。程序运行可以没有heap,但必须有stack。

java中,stack的大小是通过-Xss来设置的,当stack中数据比较多时,需要适当调大这个值,否则会出现java.long.StackOverflowError异常


Java对象的大小
一个空object的大小是8byte,这个大小只是保存heap中一个没有任何属性的对象大大小。如:
Object o = new Object();
它所占的空间为4byte + 8byte。4byte为stack中保存对象引用需要的空间,8byte是heap中对象的信息。
因为所有java非基本类型对象都是集成自Object,所以不论什么样的java对象,大小都必须大于8byte
1 
2 Class NewObject{
3     int count;
4     boolean flag;
5     Object o;
6 }
其大小为:对象大小(8) + int型(4) + boolean(1) + 对象引用(4) = 17byte
但是因为java在内存中对对象进行分配时都是以8的倍数来分配,因此会为NewObject对象实例分配 24byte。

需要注意基本类型的包装类型的大小。因为包装类型已经成为对象了,因此要把包装类型当对象来看待。如Integer,Float,Double等
一个包装类型最少占用16byte(8的倍数),它是使用基本类型的N倍,因此尽量少用包装类。

对象引用分为:强引用,软引用,弱引用和虚引用。
1 强引用StrongReference:我们一般声明对象时jvm生成的引用,强引用环境下,垃圾回收需要严格判断。如果被强引用则不会被回收
2 软引用SoftReference:一般作为缓存来使用。在垃圾回收的时候,jvm会根据当前系统的剩余内存来决定是否对软引用进行回收。如果jvm发生outOfMemory时,肯定是没有软引用存在的。
3 弱引用WeakReference:与软引用类似,都是作为缓存来使用。不同的是在垃圾回收的时候,弱引用一定会被回收。因此其生命周期只存在一个垃圾回收周期内。
4 虚引用PhantomReference:形同虚设,随时会被垃圾回收。其主要功能是与引用队列(ReferenceQueue)联合使用。当垃圾回收发现一个对象有虚引用时,就会在回收内存之前,将虚引用添加到与之关联的引用队列中。程序可以通过判断引用队列中是否有虚引用来了解引用对象是否将要被回收。从而决定是否采取行动。

系统一般使用强引用。软引用和弱引用一般是在内存大小比较受限的情况下使用。常用在桌面引用系统中。


垃圾回收基本算法
1 引用计数 Reference Counting
古老的回收算法。对象多一个引用,就增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只收集计数为0的对象。
该算法最致命的是无法处理循环引用的问题。
2 标记-清除 Mark-Sweep
此算法分为两个阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个heap,把未标记的对象清除。
此算法需要暂停整个应用。同时产生内存碎片。
3 复制 Copying
此算法把空间划分为2个相等的区域。每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现碎片。
此算法的缺点是需要两倍的内存空间。
4 标记-整理 Mark-Compact
此算法结合了 Mark_Sweep 和 Copying 两个算法的优点。
第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个heap,清除未标记对象并且把存活对象压缩到heap的其中一块,按照顺序摆放。
此算法避免了碎片问题,和Copying算法的空间问题。

5 增量收集 Incremental Collecting 
实时垃圾回收算法,在应用进行的同时进行垃圾回收,jdk5 没有使用此算法
6 分代收集 Generational Collecting
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同周期的对象采用不同的算法(上面算法的一个)进行回收。

7 串行收集
使用单线程处理所有垃圾回收工作,因为无序多线程交互,更容易实现,而且效率很高。但是无法使用多处理器的优势,所以只适合单处理器的机器。
8 并行收集
使用多线程处理垃圾回收工作速度快,效率高。理论上cpu数目越多,越能体现并行收集的优势
9 并发收集
前面两个在进行垃圾回收的时候,需要暂停整个运行环境。因此系统会有明显的暂停,暂停时间因为heap越大而越长。


垃圾回收面临的问题

1 如何区分垃圾?
因为引用计数无法解决循环引用。所有后来的垃圾回收算法都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。因为stack是真正进行程序执行的地方,所以要知道哪些对象正在被使用,需要从java stack开始。如果有多个线程,必须对这些线程对应的所有stack进行检查。
除了stack外,还有系统运行时的寄存器,也是存储程序运行时数据的地方。这样以stack和寄存器中的引用为起点,来找到heap中的对象,又从这些对象中找到对heap中其他对象的引用,这样逐步扩展。最终以null引用或基本类型结束,这样就形成了一棵以java stack中引用所对应的对象为根节点的一棵对象数。如果stack中有多个引用,则最终形成多棵对象树。这些对象树上的对象,都是当前系统运行所需要的对象,不能被回收。而其他剩余对象,视为无法被引用的对象,可以被回收。
因此垃圾回收的起点是一些根对象(java stack,static 变量,寄存器……)。最简单的java stack就是main函数。这种回收方式,就是Mark-Sweep。

2 如何处理碎片?
因为不同java对象存活时间不同,因此程序运行一段时间后,会出现零散的内存碎片。碎片最直接的问题就是导致无法分配大块的内存空间,以及程序运行效率降低。Copying和Mark-Compact都可以解决碎片问题

3 如何解决同时存在的对象创建和对象回收问题
垃圾回收线程是回收内存的,程序运行是消耗内存的,一个回收内存,一个分配内存,两者是毛段的。因此,在现有的垃圾回收方式中,在垃圾回收前,一般都需要暂停整个应用(暂停内存分配),然后进行垃圾回收,回收完成后再继续应用。
这样的弊端是,当heap空间持续增大时,垃圾回收的时间也将相应增长,相应的程序暂停时间也增长。一些对时间要求很高的应用,比如最大暂停时间要求是几百ms,那么当heap空间大于几个G时,就可能超时。这种情况下,垃圾回收会成为系统运行的一个瓶颈。为了解决这个矛盾,有了并发垃圾回收算法。使用这个算法,垃圾回收线程与程序运行线程同时运行。没有暂停,算法复杂性会大大增加,系统处理能力也相应降低。同时碎片问题将会比较难解决。


分代垃圾回收详述:
1 为什么要分代?
基于这样一个事实:不同对象的生命周期是不一样的。因此不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在java运行过程中会产生大量对象。其中有些对象是与业务信息相关的。比如http请求中的session对象、线程、socket连接,这些跟业务直接挂钩,因此生命周期比较长。但是程序运行过程中生成的临时变量,生命周期会比较短。比如String对象等。

2 如何分代?
jvm中共划为三个代:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)
持久代主要存放Java类信息,与垃圾回收要收集的java对象关系不大。年青代和年老代是对垃圾收集影响最大的。

年轻代:
1 所有新生成的对象首先都是放在“年轻代"里的。年轻代的目标就是尽可能快速的收集那些生命周期短的对象。
年轻代分为三个区:1个Eden区,2个Survivor(幸存,残余)区
大部分对象在Eden区生成,当Eden区满时,还存活的对象将被复制到Survivor1区,当Survivor1区满时,此区的存活对象将被复制到Survivor2区,此时,Eden区满时还存活的对象将复制到Survivor2中,Survivor1区会被清空当Survivor2区也满时(包含从1中复制过来的对象和从Eden区过来的对象),从Survivor1区复制过来的并且还存活的对象,将被复制到"年老代"的年老区(Tenured Space)。Survivor2区中新增加的从Eden区过来的还存活的对象,将复制到Survivor1中,Survivor2被清空。之后,Eden区满时还存活的对象就会复制到Survivor1中。重复这样的循环。
两个Survivor区是对称的,没有先后顺序。所以同一个区中可能同时存在从Eden复制过来的对象和另一个Survivor区复制过来的对象。
而复制到年老代的Tenured区的只有从第一个Survivor区过来的对象,因为Tenured区存放的是从第一个Survivor区过来,依旧存活的对象。
两个Survivor区中总有一个是空的。同时根据需要,可以配置多个Survivor区,延长对象在年轻代中的存在时间,减少被放到年老代的可能。
2 年老代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代。因此年老代存放的都是生命周期较长的对象。
3 持久代
用于存放静态文件,如java类,方法等。
持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

什么情况下触发垃圾回收?
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。
GC(Garbage Collection)有两种类型:Scavenge GC 和 Full GC
1 Scavenge GC 
一般情况下,当新对象生成,并且在Eden区申请空间失败时,就会触发Scavenge GC。对Eden区进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。
因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Scavenge GC 会频繁进行。
一般这里需要使用速度快,效率高的算法,使Eden区尽快空闲出来。
2 Full GC
对整个Heap进行整理,包括年轻代,年老代和持久代。Full GC因为需要对整个进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对jvm调优的过程中,很大一部分工作就是对Full GC的调节。导致Full GC是因为如下方法:
  • 年老代的Tenured区被写满
  • 持久代被写满
  • System.gc()被显式调用
  • 上一次GC之后Heap的各区域分配策略动态变化

选择合适的垃圾收集算法
1 串行收集器
用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,无法使用多处理器的优势,所以只适合单处理器机器。
也可以用在小数据量(100M)情况下的多处理器机器上。用-XX:+UseSerialGC打开

2 并行收集器
对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器上使用。使用-XX:+UseParallelGC打开。
并行收集器在J2SE5.0更新上引入,在java SE6.0中进行了增强,可以对年老代进行收集。如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
设置:
  • 并行垃圾回收的线程数,使用-XX:ParallelGCThreads=<N>。此值可以设置与机器处理器数量相等。
  • 最大垃圾回收暂停,指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=<N>指定。N为毫秒,如果指定了此值,heap大小和垃圾回收相关参数会进行调整以达到指定值。设定此值会减少应用吞吐量。
  • 吞吐量,为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=<N>来设定,公式为1/(1+N)。例如,N=19时,表示5%的时间用于垃圾回收。默认值是99,即用1%的时间用于垃圾回收。
3 并发收集器
可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的大中规模应用。
使用-XX:+UseConcMarkSweepGC打开。
并发收集器主要减少年老代的暂停时间,它在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集出气并发收起会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停时间比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
并发收集器使用处理器换来短暂的停顿时间。
在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。即K小于N的四分之一。
在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可以获得较短的停顿时间。

浮动垃圾(Floating Garbage):
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所有,并发收集器一般需要预留20%的空间用于浮动垃圾。

并发模式失败(Concurrent Mode Failure):
并发收集器在应用运行时进行收集,所以需要保证heap在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,heap空间先满了。这种情况下就会发生并发模式失败,此时整个应用会暂停,进行垃圾回收。
为了保证有足够内存供并发收集器使用,可以设置-XX:CMSInitiatingOccupancyFraction=<N>指定剩余多少heap时开始执行并发收集。

1 串行serial收集器,适用于数据量比较小(100M)的应用,或者单处理器下并且对响应时间无要求的应用。
缺点:只能用于小型应用
2 并行parallel收集器,适用于对吞吐量有高要求,多cpu,对应用相应时间无要求的大中型应用。如后台处理、科学计算。
缺点:垃圾收集过程中应用响应时间可能加长
3 并发concurrent收集器:适用于对响应时间有高要求,多cpu的大中型应用。如web服务器、应用服务器、电信交换、集成开发环境。



P26……