27.jvm怎么优化?jvmjvm的内存分配怎么分配

JVM内存模型与性能调优
我的图书馆
JVM内存模型与性能调优
Java是一门面向对象的编程语言,用对象来定义,描述和操作一切。对象数据存储在计算机内存中,Java的内存模型到底是个什么样子,让Java引为自豪的垃圾回收器又是如何工作的,如何针对JVM的内存管理进行性能调优,笔者将通过本文带您揭开这些Java世界深处不为人知的内幕。
堆内存(Heap)
堆是由Java虚拟机(JVM,下文提到的JVM特指Sun hotspot JVM)用来存放Java类、对象和静态成员的内存空间,Java程序中创建的所有对象都在堆中分配空间,堆只用来存储对象,应用程序通过存放在堆栈(Stack)内的引用来访问堆数据,一个JVM进程只能拥有一个堆。JVM通过-Xms和-Xmx参数分别设置堆的初始值和最大值,初始值默认是物理内存的1/64但小于1G,最大值默认是物理内存的1/4但小于1G 。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio来指定百分比。默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio来设定百分比。
Java堆使用分代的方式来管理不同类型的Java对象,通常情况下,JVM会将堆分为三个区域(代):年青代(Young),年老代(Old)和永久代(Permanent),针对不同不区域中的不同类型的Java对象JVM采用不同的垃圾回收算法。年青代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的1/15,可以通过-Xmn参数设置年青代为固定大小,也可以通过-XX:NewRatio来设置年青代与年老代的大小比例,年青代的特点是对象更新速度快,在短时间内产生大量的“死亡对象”。年老代用来存放存活时间长相对稳定的对象。在年青代中“存活”次数最多的对象会被移动到年老代,通过-XX:MaxTenuringThreshold参数来设置“存活”几次的对象才被移入年老代,年老代的大小由整个堆空间的尺寸减掉年青代和永久代计算得到 。
永久代用来存放类及类的静态成员,这些对象通常来讲很少会被垃圾收集。永久代的初始值和最大值分别通过-XX:PermSize和-XX:MaxPermSize参数来设定。永久代的大小是由JVM独立管理的,并不会随堆的大小变化而变化。当永久代溢出时,JVM会增大永久代的尺寸,但不会超过XX:MaxPermSize设置的最大值,当永久代达到最大值后,继续申请永久代空间将造成系统崩溃(Out of Memery)。年青代在堆内存中是从上向下分配空间的,而永久代则是从下向上分配的,这样作可以最大程度的减少内存碎片的产生。当且只当年老代空间不足时才会触发堆的增长,但不会超过-Xmx设置的上限值。
垃圾回收器(GC)
Java垃圾回收器是一个或多个运行在JVM中低优先级的守护线程,它负责监视JVM堆空间的使用情况,在预定的条件满足时负责回收“死亡对象”,释放可用的内存空间供应用程序使用。死亡对象是指在JVM所有线程堆栈引用中没有任何一个有效指向的对象,死亡对象是无法重新被程序使用的Java对象,在Java中,通过程序无法释放死亡对象的内存空间,JVM使用垃圾回收器来自动的不间断的回收死亡对象。
Java垃圾回收器的三种常用回收算法:
记数器清除(tracing):该算法使用引用计数器来区分存活对象和死亡的对象。堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1,当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
复制清除(replicate):&该算法将堆内存分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问每一个关联的活跃对象,将空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A。
标记清除(mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个内存区域,把所有没有标记活跃的对象进行回收处理。
Java垃圾回收器的三种类型:
串行收集器(Serial):使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,适合单处理器或小尺寸堆(100M左右)环境下使用。使用-XX:+UseSerialGC参数打开该收集器。
并行收集器(Parallel):Java5.0新增加的收集器,使用多线程并行的对指定的内存块进行垃圾回收,可以充分的发挥多处理器的优势。使用-XX:+UseParallelGC参数打开该收集器,使用-XX:+UseParallelOldGC参数强制在年老代使用该收集器,使用-XX:ParallelGCThreads参数设置并行收集器使用的线程数 。
并发收集器 (ConcMarkSweep):该收集器主要是针对标记清除算法,可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。Java5.0默认情况下对年青代使用并行收集器(Java1.4使用的是串行收集器),对年老代使用并发收集器,当然可以使用上面提到的参数强制JVM使用其它的收集器。
垃圾回收策略
垃圾回收策略主要是针对年青代和年老代的不同特点分别进行处理的。年青代的特点是产生大量的死亡对象且要求连续的可用空间,所以使用复制清除算法和并行收集器来进行垃圾回收。对年青代的垃圾回收称作叫作初级回收(minor GC)。只有在年青代出现溢出时才会触发初级回收,由于此时年青代已经没有可用空间,所以GC必须挂起应用程序只至回收过程完成。初级回收将年青代分成三个区域,一个新生代(Eden )和两个大小一样的复活代(Survivor),应用程序只能使用新生代和一个复活代(活动复活代),当初级垃圾回收发生时,GC挂起应用程序,将新生代和活动复活代中的存活对象复制到另一个非活动复活代中,然后一次性清除新生代和活动复活代,同时将原来的非复活代标记为活动复活代。初级回收会将存活的对象移动到活动复活代中,将在指定次数回收后仍旧存活的对象移动到年老代中,初级回收的效果是得到一个空的可用的新生代。
年老代的特点是尺寸较大且只存在少量的死亡对象,所以使用标记清除算法和并发收集器来进行垃圾回收。对年老代的垃圾回收又称作次级回收(major GC)。由于年老代比年青代大的多,所以相对于初级回收,次级回收将消耗更多的系统资源和时间。次级回收发生时,GC短暂的挂起应用程序以便标记根对象,然后GC会和应用程序并发执行标记所有的非存活对象,当标记完成时GC再次短暂的挂起应用程序完成清除操作。在清除完成后还会执行一次压缩年老代的操作,以便消除在标记清除过程中产生的内存碎片。由于次级回收大部分时间是和应用程序并发进行的,为了在收集过程中给应用程序留出充足可用内存空间,当年老代的可用空间低于68% 时,JVM就会就会触发次级回收,同时也由于并发执行的原因,次级回收不能100%的回收年老代中所有的死亡对象。
初级回收和次级回收是完全垃圾回收(Full GC)的一部分,完全垃圾回收还会尝试回收永久代(尽管只有很少的概率能从永久代中回收对象,但JVM还是会这样作),通常情况下完全垃圾回收会在以下情况下触发:
次级回收后年老代仍然出现溢出
永久代溢出
System.gc()被显示调用
上一次GC之后Heap的各代分配策略动态变化
完全垃圾回收会消耗相当可观的系统资源,并造成应用程序挂起。一次完全垃圾回收后如果整个堆的可用空间仍然小于指定比例将会造成堆空间的增长,如果堆空间已经达到最大值且仍出现代溢出,则会造成系统崩溃(Out of Memery)。
堆栈内存(Stack)
堆栈(以下简称栈)是由线程管理的一块线性访问的独立于堆的内存空间,一般用来记录程序的执行和存储线程的局部变量,存储在栈中的数据必须在编译阶段确定所占内存的大小。栈空间遵循后进先出的线性访问原则,访问速度要快于堆(Heap)。Java中支持的栈操作只有压栈(Pack)和出栈(Peek)两种,压栈操作将数据存入栈顶,出栈操作将栈顶的数据弹出栈。每一个Java线程对应唯一的一个栈空间,栈空间大小默认值为1M,可以通过设置-Xss参数来增大或缩小为每个线程分配的栈空间,设置过小的栈空间会导致堆栈溢出,设置过大的栈空间会造成最大的线程数减少。
存在于堆空间内的对象一般会在某个或某几个线程对应的栈内找到一个或多个引用,当所有线程对应的栈空间内都找不到引用时,堆内存中的对象就是一个“死亡对象”,死亡对象是Java垃圾回收器负责回收的对象。当同一个堆对象在多个线程中被引用时,通过这些线程访问堆对象可能会出现抢占。为了防止抢占现象的发生,Java提供了线程锁定(Lock)方式来为堆对象加锁,被加锁的堆对象只能被持有该锁的唯一一个线程访问(详见《Java多线程》),栈内存放的数据无法进行锁定。
线程使用的局部变量(方法内变量),无论该变量是堆对象还是基本数据类型,都不会存在抢占现象,当程序运行于多线程模式下时尽量使用局部变量来避免发生抢占。当局部变量引用一个堆对象时,线程结束后该堆对象就变成一个非存活对象。栈空间不受Java垃圾回收器管理,栈的使用完全由程序代码控制,在程序的编译阶段,线程使用的栈空间已经分配完成。当线程结束后,线程对应的栈空间同时被释放。
通过设置-verbose:gc -XX:+PrintGCDetails参数来跟踪系统GC的详细情况,通过设置-XX:+ PrintGCApplicationStoppedTime参数可以显示每次垃圾回收系统挂起的时间。一个好的应用系统会根据年老对象和新生对象的多少来决定堆内存的分配和GC策略的选择,当一个系统存在大量的年老对象时,设置一个小的年青代来减少次级回收的发生频率。反之,当一个系统存在大量的新生对象的时,设置一个大的年青代来减少初级回收的发生频率,有时还需要根据系统加载类的多少来灵活的设置永久代的大小。
JVM如何使用堆内存
当应用程序生成一个新的Java对象,JVM负责在堆内存中为其申请存储空间,通常内存申请过程如下:
JVM试图为Java对象在新生代中初始化一块内存区域,当新生代空间足够时,内存申请结束。否则到下一步;
JVM试图释放新生代中所有死亡的对象,这将引发初级垃圾回收,初级垃圾回收将新生代中的活跃对象移动到复活代,当复活代空间不足时,复活代的对象会被移动到年老代;
当年老代空间不足时,JVM会在年老代进行次级垃圾收集,如果回收后年老代仍然不足以存放新创建的Java对象,则会引发完全垃圾回收,完全垃圾回收会试图回收永久代;
如完全垃圾回收后年老代仍然空间不足,JVM会引发堆增长;
如堆增长后仍然空间不足,则会重复3和4,直至堆空间增长至最大值;
如果当堆的尺寸增长至最大值后仍然无法容纳新的Java对象,则导致JVM无法为新对象创建内存区域,出现“out of memory错误”。
堆内存结构图
当创建一个对象时,JVM首先在堆中为对象分配空间,然后在线程堆栈中压栈一个变量,将变量指向新创建的对象,当将变量置为Null时,该变量不再引用任何对象,先前在堆中创建的对象也因此变为死亡对象。
Person p = new Person();
灵活的使用虚引用可以避免反复创建对象造成的性能浪费,只有当完全垃圾回收发生时,JVM才会回收虚引用指向的对象。
A a = new A();
SoftReference sr = new SoftReference(a);
if (sr != null) {&&&
&&&&a = (A)sr.get();&&&&
&&&&a.test();
else {&&&&&&
&&&&a = new A();&&&&
&&&&a.test();&&&
&&&&a = null;&&&
&&&&sr = new SoftReference(a);
内存溢出通常发生在JVM无法为新创建的Java对象分配足够的内存空间情况下,针对不同区域的内存分配情况,内存溢出分为以下几种类型。
年老代溢出(java.lang.OutOfMemoryError: Java heap space),这种内存溢出是最常见的情况之一,产生的原因可能是:
堆尺寸设置过小或年青代所占堆内存的比例设置过大(Xms/Xmx, XX:NewRatio);
程序所申请内存过大,有的程序会申请几十乃至几百兆内存(如不分页的Grid导出),此时JVM也会因无法申请到资源而出现内存溢出,对此首先要找到相关功能模块,然后交予程序员修改,可以使用JProbe之类的Java调优工具对问题模块进行定位。
当Java对象使用完毕后没有及时销毁(内存泄漏),使得JVM认为他还是活跃的对象而不进行回收,这样累计占用了大量内存而无法释放。可以使用JProbe之类的Java调优工具对Dump出来的崩溃的JVM进程进行静态分析,找到泄漏点进行修改。
永久代溢出(java.lang.OutOfMemoryError: Perm Gen):
系统需要加载大量的类或在类中大量使用静态成员时可能会导致永久代溢出,解决方法是加大永久代尺寸(XX:PermSize和XX:MaxPermSize),或修改代码,将静态成员改为普通成员。
堆栈溢出(java.lang.StackOverFlowException):
当程序使用大的递归或循环算法时可能会造成堆栈溢出,比如由于程序设计不当出现死循环或次数超过几千次的递归,解决方法是修改代码或加大线程的堆栈尺寸(Xss)。
内存优化最佳实践
Java内存优化主要是指对JVM的内存模型进行优化的过程,主要手段是通过JVM参数来调整JVM内存的运行时状态,通常JVM参数分为三大类:
标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
非标准参数(-X):所有的JVM实现都必须实现这些参数的功能,但不保证向后兼容;
非Stable参数(-XX):此类参数各个JVM实现会有所不同,将来可能会随时取消,需要慎重使用;
下面我们将针对常见的涉及性能调优的JVM参数逐个进行讲解。
了解JVM的两种启动模式:调试模式和生产模式(-client -server):
设置JVM使用client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或者PC应用开发和调试。
设置JVM使server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。
在具有64位能力的Java环境下将默认启用生产模式,而忽略-client参数。
将堆内存的初始值与最大值设置为相同(-Xms -Xmx):
将堆内存的初始值与最大值设置为相同意味着禁用堆增长,通过前面学习的内容可以知道,每当发生一次堆增长时,至少会伴随两次的完全垃圾回收(前后各一次),如果一次增长不能满足要求时,情况会变的更糟。所以记住这一条,永远将堆内存的初始值与最大值设置为相同。
注意,当堆内存的初始值与最大值设置为相同时,-XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 参数将失效。
合理的分配年青代与年老代的大小(-Xmn -XX:NewRatio -Xmn):
当使用Spring Ioc/AOP、Hibernate或其它的需要在系统中维持大量缓存对象的框架时,应当将年老代所占比例加大。通常来讲缓存对象会长期占用年老代,设置一个大的年老代可以有效的降低次级垃圾回收和完全垃圾回收的发生概率,JVM参数中没有直接设置年老代尺寸的参数,但可以通过减小年青代的尺寸来变相加大年老代,事实上现在流行的J2EE框架都大量使用缓存技术来提高系统性能,所以加大年老代的尺寸是一个通用的JVM调优方法,通过一个设置得当的内存参数加上优秀的架构作支撑,理论上可以100%避免次级垃圾回收和完全垃圾回收的发生。-Xmn是设置固定的年青代大小, -XX:NewRatio 是设置年青代在整个堆内存中占的比例;
当不使用前面提到的框架,很少或不使用缓存对象时,应当设置一个大的年青代,一个大的年青代带来的好处是降低初级垃圾回收的发生(只是降低,永远不要期望避免初级垃圾回收的发生),象基于JSP/JavaBean模式开发的小型WEB应用或通过精确控制的小型的Java桌面应用,可以考虑使用这种配置方案,事实上,系统80%以上的GC消耗都来自于次级垃圾回收和完全垃圾回收,减少次级垃圾回收和完全垃圾回收带来的性能提高要远远高于降低初级垃圾回收带来的效果。
根据项目规模适当的设置永久代的大小(-XX:PermSize -XX:MaxPermSize):
永久代被用来存储类和类的静态成员,当系统需要加载大量的类或在类中大量使用静态成员(静态方法或变量)时(工具类?),应设置一个大的永久代。事实上一个系统稳定运行一段时间后,永久代的大小是可以评估出来的(需要借助一些调优工具)。永久代设的过大其实对系统性能没有提升,反而因为过多的占用了堆空间而限制了年青代和年老代的大小,如果你的堆内存总量在1.5G,那么将永久代的尺寸设置为256M吧。
调整新生代与复活代的尺寸比例(-XX:SurvivorRatio):
这是一个高级的调优内容,确保除非确实需要,否则轻易不要使用这个参数。通过前面学习的内容我们知道,初级垃圾回收只发生在新生代溢出的情况下(只此一条),因此加大新生代在年青代中所占的比例可以更明显的降低初级垃圾回收的发生概率(试想一下,如果将整个堆空间全部设置为新生代会发生什么情况?)。 这么作带来的负效应是使复活代变的很小(回想一下复活代的作用),这样将导致复活带无法容纳足够多的“准死亡对象”从而造成大量本应该在初级垃圾回收中销毁的对象被迫提前进入到年老代中,最终的结果会导致次级垃圾回收的频繁发生而造成不必要的性能损耗。反之,如果将复活代所占的比例调大,那么准死亡对象的问题就不会发生了,但由于新生代太小,造成频繁的初级垃圾回收,对系统性能也是不利的。Sun hotspot JVM默认的新生代与复活代的比例为50%(由于存在活动复活代和非活动复活代两个大小一样的复活代,所以一个复活代占用年青代的比例为25%)。
设置一个合理的垃圾存活年龄(-XX:MaxTenuringThreshold):
这个参数网上很少被提到,其实它对系统性能的影响还是很大的。根据Sun的官方文档,这个参数用来设置Java对象在复活代内“熬”过几次初级垃圾回收后才会被移入年老代。当一个系统中存在比较多的“准缓存对象”时应该考虑将这个值设大。准缓存对象是这样一类对象,它会在内存中存在一段时间,但总会有新的缓存对象来替换它(频繁变化的数据库缓存?AA10?)或它的存在具有一定的时效性(一个繁忙的WEB系统的Session对象?Request?),那么为了尽可能的优化系统性能,我们应该让JVM在初级垃圾回收时就能够回收这些对象,而不应是次级垃圾回收。当然将这个值设的太大会导致一些真正的缓存对象长时间留在复活区内从而加重初级垃圾回收的压力,而且,当复活区溢出时,总是会将年龄最大的对象移动到年老区,不管你将这个值设的有多大,补充一点,将这个参数设置为0代表你要禁用复活区,系统默认值是15。
强制JVM在进行完全垃圾回收前先进行初级垃圾回收(-XX:+ScavengeBeforeFullGC):
望名知义,如果初级垃圾回收成功回收了足够的可用空间,就不用再进行完全垃圾回收了,但如果初级垃圾回收没有回收到足够的可用空间,则完全垃圾回收照样会进行,这时反而白白消耗了一次初级垃圾回收。
设置线程的堆栈大小(-Xss)
当且只当你开发的是一个“算法密集”型的系统(视频压缩解压缩软件?)时,可以考虑加大堆栈尺寸。其它任何时候当看到“Stack Over Flow”异常时首先考虑你的代码是否出现的死循环。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在左右。
挑战极限性能:禁用垃圾回收器(-Xnoclassgc):
设置该参数后将禁用JVM使用任何垃圾回收器,禁用垃圾回器意味着系统性能将达到极限值,但后果是必须由我们自己来负责回收垃圾,不幸的是Java语言并没有提供给我们任何手动回收对象的方法。所以只有在一些极端的环境下才考虑使用这个参数来换取最高的系统性能,比如象JNDI这一类只会不断增加对象,而“绝对”不会出现垃圾的情况。
选择一个合适的垃圾回收器(-XX:UseParallelGC -XX:-UseConcMarkSweepGC -XX:-UseSerialGC):
Java5.0为我们提供了三种可用的垃圾回收器:串行回收器、并行回收器和并发回收器。串行回收器性能最差,但在单核处理器和小尺寸堆上会有意外表现。并行回收器具有最大回收速度,但在回收过程中会暂停应用程序。并发回收器速度较慢但却不会暂停程序(不暂停不代表不减慢)。Java5.0默认在年青代使用并行回收器以换取最大的吞吐量,在年老代使用并发收集器来减少对应用程序的暂停时间。在年青代永远使用并行回收器吧,因为初级回收几乎是在每时每刻发生,使用速度最快的并行回收器永远是你的最佳选择。如果你面对的是一个繁忙的系统,而频繁的年老代回收正在严重拖慢系统性能,这时考虑使用-XX:-UseParallelGC参数强制JVM在年老代使用并行收集器,此时快速的并行收集器会为你带来可观的性能提升。否则,使用系统默认的并发收集器吧。
跟踪JVM调试信息(-verbose:gc -verbose:class -Xprof -Xloggc:file):
-verbose:gc:输出每次GC的相关情况,输出形式:[GC 118250K-&0112K), 0.0094143 secs] [Full GC 121376K- &1K), 0.0650971 secs]。
-verbose:class:输出JVM载入类的相关信息,当JVM报告说找不到类或者类冲突时可此进行诊断。
-Xprof:打开JVM调试信息,跟踪正在运行的程序,并将跟踪数据在标准输出输出,适合于开发环境调试 。
-Xloggc:file:与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中。
内存参数设计示例
&&-server&
&&-XX:UseParallelGC&
&&-Xprof -verbose:gc&
&&-Xmx1550m&
&&-Xms1550m&
&&-Xss128k&
&&-Xns512m&
&&-XX:NewRatio=4&
&&-XX:SurvivorRatio=4&
&&-XX:MaxPermSize=256m&
&&-XX:MaxTenuringThreshold=10&
&&-XX:NewRatio=4
-server:使用产品模式启动JVM;
-XX:UseParallelGC:在年老代使用并行回收器;
-Xmx1550m Xms1550m:将堆的最小值与最大值设置为相同;
-Xns512m:设置年轻代大小为512M。整个堆大小=年轻代+年老代+持久代。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5;
-XX:SurvivorRatio=4:设置年轻代中新生代与复活代的大小比值。设置为4,则两个复活代与一个新生代的比值为2:4,一个复活代占整个年轻代的1/6;
-XX:MaxPermSize=128m:设置永久代大小为256M;
-XX:MaxTenuringThreshold=10:设置垃圾最大年龄,对象在复活区内存活10次后被移动到年老区;
-Xss128k:设置每个线程的堆栈大小为128K;
-Xprof -verbose:gc:打开JVM调试信息,打开垃圾回收调试信息。
TA的最新馆藏
喜欢该文的人也喜欢本帖子已过去太久远了,不再提供回复功能。Java虚拟机JVM性能优化(一):JVM知识总结
投稿:junjie
字体:[ ] 类型:转载 时间:
这篇文章主要介绍了Java虚拟机JVM性能优化(一):JVM知识总结,本文是系列文章的第一篇,后续篇章请继续关注脚本之家,需要的朋友可以参考下
Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化。之后的文章会讲JVM性能优化,包括最新的JVM设计——支持当今高并发Java应用的性能和扩展。
如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法。我个人很喜欢学习新知识带来的这种感觉。我已经有过很多次这样的经历了,在我使用JVM技术工作时,特别是使用垃圾回收和JVM性能优化时。在这个新的Java世界中,我希望和你分享我的这些启发。希望你能像我写这篇文章一样兴奋的去了解JVM的性能。
这个系列文章,是为所有有兴趣去学习更多JVM底层知识,和JVM实际做了什么的Java开发人员所写的。在更高层次,我将讨论垃圾回收和在不影响应用运行的情况下,对空闲内存安全和速度上的无止境追求。你将学到JVM的关键部分:垃圾回收和GC算法,编译优化,和一些常用的优化。我同样会讨论为什么Java标记这样难,提供建议什么时候应该考虑测试性能。最后,我将讲一些JVM和GC的新的创新,包括Azul's Zing JVM, IBM JVM, 和Oracle's Garbage First (G1) 垃圾回收中的重点。
我希望你读完这个系列时对Java可扩展性限制的特点有更深的了解,同样的这样限制是如何强制我们以最优的方式创建一个Java部署。希望你会有一种豁然开朗的感受,并且能激发了一些好的Java灵感:停止接受那些限制,并去改变它!如果你现在还不是一个开源工作者,这个系列或许会鼓励你往这方面发展。
JVM性能和“一次编译,到处运行”的挑战
我有新的消息告诉那些固执的认为Java平台本质上是缓慢的人。当Java刚刚做为企业级应用的时候,JVM被诟病的Java性能问题已经是十几年前的事了,但这个结论,现在已经过时了。这是真的,如果你现在在不同的开发平台上运行简单静态和确定的任务时,你将很可能发现使用机器优化过的代码比使用任何虚拟环境执行的要好,在相同的JVM下。但是,Java的性能在过去10年有了非常大的提升。Java产业的市场需求和增长,导致了少量的垃圾回收算法、新的编译创新、和大量的启发式方法和优化,这些使JVM技术得到了进步。我将在以后的章节中介绍一些。
JVM的技术之美,同样是它最大的挑战:没有什么可以被认为是“一次编译,到处运行”的应用。不是优化一个用例,一个应用,一个特定的用户负载,JVM不断的跟踪Java应用现在在做什么,并进行相应的优化。这种动态的运行导致了一系列动态的问题。当设计创新时(至少不是在我们向生产环境要性能时),致力于JVM的开发者不会依赖静态编译和可预测的分配率。
JVM性能的事业
在我早期的工作中我意识到垃圾回收是非常难“解决”的,我一直着迷于JVMs和中间件技术。我对JVMs的热情开始于我在JRockit团队中时,编码一种新的方法用于自学,自己调试垃圾回收算法(参考 Resources)。这个项目(转变为JRockit一个实验性的特点,并成为Deterministic Garbage Collection算法的基础)开启了我JVM技术的旅程。我已经在BEA系统、Intel、Sun和Oracle(因为Oracle收购BEA系统,所以被Oracle短暂的工作过)工作过。之后我加入了在Azul Systems的团队去管理Zing JVM,现在我为Cloudera工作。
机器优化的代码可能会实现较好的性能(但这是以牺牲灵活性来做代价的),但对于动态装载和功能快速变化的企业应用这并不是一个权衡选择它的理由。大多数的企业为了Java的优点,更愿意去牺牲机器优化代码带来的勉强完美的性能。
1.易于编码和功能开发(意义是更短的时间去回应市场)
2.得到知识渊博的的程序员
3.用Java APIs和标准库更快速的开发
4.可移植性——不用为新的平台去重新写Java应用
从Java代码到字节码
做为一个Java程序员,你可能对编码、编译和执行Java应用很熟悉。例子:我们假设你有一个程序(MyApp.java),现在你想让它运行。去执行这个程序你需要先用javac(JDK内置的静态Java语言到字节码编译器)编译。基于Java代码,javac生成相应的可执行字节码,并保存在相同名字的class文件:MyApp.class中。在把Java代码编译成字节码后,你可以通过java命令(通过命令行或startup脚本,使用不使用startup选项都可以)来启动可执行的class文件,从而运行你的应用。这样你的class被加载到运行时(意味着Java虚拟机的运行),程序开始执行。
这就是表面上每一个应用执行的场景,但是现在我们来探究下当你执行java命令时究竟发生了什么。Java虚拟机是什么?大多数开发人员通过持续调试来与JVM交互——aka selecting 和value-assigning启动选项能让你的Java程序跑的更快,同时避免了臭名昭著的”out of memory”错误。但是,你是否曾经想过,为什么我们起初需要一个JVM来运行Java应用呢?
什么是Java虚拟机?
简单的说,一个JVM是一个软件模块,用于执行Java应用字节码并且把字节码转化到硬件,操作系统特殊指令。通过这样做,JVM允许Java程序在第一次编写后可以在不同的环境中执行,并不需要更改原始的代码。Java的可移植性是通往企业应用语言的关键:开发者并不需要为不同平台重写应用代码,因为JVM负责翻译和平台优化。
一个JVM基本上是一个虚拟的执行环境,作为一个字节码指令机器,而用于分配执行任务和执行内存操作通过与底层的交互。
一个JVM同样为运行的Java应用照看动态资源管理。这就意味着它掌握分配和释放内存,在每个平台上保持一致的线程模型,在应用执行的地方用一种适于CPU架构的方式组织可执行的指令。JVM把开发人员从跟踪对象当中的引用,和它们需要在系统中存在多长时间中解放出来。同样的它不用我们管理何时去释放内存——一个像C语言那样的非动态语言的痛点。
你可以把JVM当做是一个专门为Java运行的操作系统;它的工作是为Java应用管理运行环境。一个JVM基本上是一个虚拟的通过与底层的交互的执行环境,作为一个字节码指令机器,而用于分配执行任务和执行内存操作。
JVM组件概述
有很多写JVM内部和性能优化的文章。作为这个系列的基础,我将会总结概述下JVM组件。这个简短的阅览会为刚接触JVM的开发者有特殊的帮助,会让你更想了解之后更深入的讨论。
从一种语言到另一种——关于Java编译器
编译器是把一种语言输入,然后输出另一种可执行的语句。Java编译器有两个主要任务:
1. 让Java语言更加轻便,不用在第一次写的时候固定在特定的平台;
2. 确保对特定的平台产生有效的可执行的代码。
编译器可以是静态也可以是动态。一个静态编译的例子是javac。它把Java代码当做输入,并转化为字节码(一种在Java虚拟机执行的语言)。静态编译器一次解释输入的代码,输出可执行的形式,这个是在程序执行时将被用到。因为输入是静态的,你将总能看到结果相同。只有当你修改原始代码并重新编译时,你才能看到不同的输出。
动态编译器,例如Just-In-Time (JIT)编译器,把一种语言动态的转化为另一种,这意味着它们做这些时把代码被执行。JIT编译器让你收集或创建运行数据分析(通过插入性能计数的方式),用编译器决定,用手边的环境数据。动态的编译器可以在编译成语言的过程之中,实现更好的指令序列,把一系列的指令替换成更有效的,甚至消除多余的操作。随着时间的增长你将收集更多的代码配制数据,做更多更好的编译决定;整个过程就是我们通常称为的代码优化和重编译。
动态编译给了你可以根据行为去调整动态的变化的优势,或随着应用装载次数的增加催生的新的优化。这就是为什么动态编译器非常适合Java运行。值得注意的是,动态编译器请求外部数据结构,线程资源,CPU周期分析和优化。越深层次的优化,你将需要越多的资源。然而在大多数环境中,顶层对执行性能的提升帮助非常小——比你纯粹的解释要快5到10倍的性能。
分配会导致垃圾回收
分配在每一个线程基于每个“Java进程分配内存地址空间”,或者叫Java堆,或者直接叫堆。在Java世界中单线程分配在客户端应用程序中很常见。然而,单线程分配在企业应用和工作装载服务端变的没有任何益处,因为它并没有使用现在多核环境的并行优势。
并行应用设计同样迫使JVM保证在同一时间,多线程不会分配同一个地址空间。你可以通过在整个分配空间中放把锁来控制。但这种技术(通常叫做堆锁)很消耗性能,持有或排队线程会影响资源利用和应用优化的性能。多核系统好的一面是,它们创造了一个需求,为各种各样的新的方法在资源分配的同时去阻止单线程的瓶颈,和序列化。
一个常用的方法是把堆分成几部分,在对应用来说每个合式分区大小的地方——显然它们需要调优,分配率和对象大小对不同应用来说有显著的变化,同样线程的数量也不同。线程本地分配缓存(Thread Local Allocation Buffer,简写:TLAB),或者有时,线程本地空间(Thread Local Area,简写:TLA),是一个专门的分区,在其中线程不用声明一个全堆锁就可以自由分配。当区域满的时候,堆就满了,表示堆上的空闲空间不够用来放对象的,需要分配空间。当堆满的时候,垃圾回收就会开始。
使用TLABs捕获异常,是把堆碎片化来降低内存效率。如果一个应用在要分配对象时正巧不能增加或者不能完全分配一个TLAB空间,这将会有空间太小而不能生成新对象的风险。这样的空闲空间被当做“碎片”。如果应用程序一直保持对象的引用,然后再用剩下的空间分配,最后这些空间会在很长一段时间内空闲。
碎片就是当碎片被分散在堆中的时候——通过一小段不用的内存空间来浪费堆空间。为你的应用分配 “错误的”TLAB空间(关于对象的大小、混合对象的大小和引用持有率)是导致堆内碎片增多的原因。在随着应用的运行,碎片的数量会增加在堆中占有的空间。碎片导致性能下降,系统不能给新应用分配足够的线程和对象。垃圾回收器在随后会很难阻止out-of-memory异常。
TLAB浪费在工作中产生。一种方法可以完全或暂时避免碎片,那就是在每次基础操作时优化TLAB空间。这种方法典型的作法是应用只要有分配行为,就需要重新调优。通过复杂的JVM算法可以实现,另一种方法是组织堆分区实现更有效的内存分配。例如,JVM可以实现free-lists,它是连接起一串特定大小的空闲内存块。一个连续的空闲内存块和另一个相同大小的连续内存块相连,这样会创建少量的链表,每个都有自己的边界。在有些情况下free-lists导致更好的合适内存分配。线程可以对象分配在一个差不多大小的块中,这样比你只依靠固定大小的TLAB,潜在的产生少的碎片。
有一些早期的垃圾收集器拥有多个老年代,但是当超过两个老年代的时候会导致开销超过价值。另一种优化分配减少碎片的方法,就是创造所谓的新生代,这是一个专门用于分配新对象的专用堆空间。剩余的堆会成为所谓的老年代。老年代是用来分配长时间存在的对象的,被假定会存在很长时间的对象包括不被垃圾收集的对象或者大对象。为了更好的理解这种分配的方法,我们需要讲一些垃圾收集的知识。
垃圾回收和应用性能
垃圾回收是JVM的垃圾回收器去释放没有引用的被占据的堆内存。当第一次触发垃圾收集时,所有的对象引用还被保存着,被以前的引用占据的空间被释放或重新分配。当所有可回收的内存被收集后,空间等待被抓取和再次分配给新对象。
垃圾回收器永远都不能重声明一个引用对象,这样做会破坏JVM的标准规范。这个规则的异常是一个可以捕获的soft或weak引用 ,如果垃圾收集器将要将近耗尽内存。我强烈推荐你尽量避免weak引用,然而,因为Java规范的模糊导致了错误的解释和使用的错误。更何况,Java是被设计为动态内存管理,因为你不需要考虑什么时候和什么地方释放内存。
垃圾收集器的一个挑战是在分配内存时,需要尽量不影响运行着的应用。如果你不尽量垃圾收集,你的应用将耗近内存;如果你收集的太频繁,你将损失吞吐量和响应时间,这将对运行的应用产生坏的影响。
有许多不同的垃圾回收算法。稍后,在这个系列里将深入讨论几点。在最高层,垃圾收集两个最主要的方法是引用计数和跟踪收集器。
引用计数收集器会跟踪一个对象指向多少个引用。当一个对象的引用为0时,内存将被立即回收,这是这种方法的优点之一。引用计数方法的难点在于环形数据结构和保持所有的引用即时更新。
跟踪收集器对仍在引用的对象标记,用已经标记的对象,反复的跟随和标记所有的引用对象。当所有的仍然引用的对象被标记为“live”时,所有的不被标记的空间将被回收。这种方法管理环形数据结构,但是在很多情况下收集器应该等待直到所有标记完成,在重新回收不被引用的内存之前。
有不种的途径来被上面的方法。最著名的算法是 marking 或copying 算法, parallel 或 concurrent算法。我将在稍后的文章中讨论这些。
通常来说垃圾回收的意义是致力于在堆中给新对象和老对象分配地址空间。其中“老对象”是指在许多垃圾回收后幸存的对象。用新生代来给新对象分配,老年代给老对象,这样能通过快速回收占据内存的短时间对象来减少碎片,同样通过把长时间存在的对象聚合在一起,并把它们放到老年代地址空间中。所有这些在长时间对象和保存堆内存不碎片化之间减少了碎片。新生代的一个积极作用是延迟了需要花费更大代价回收老年代对象的时间,你可以为短暂的对象重复利用相同的空间。(老空间的收集会花费更多,是因为长时间存在的对象们,会包含更多的引用,需要更多的遍历。)
最后值的一提的算法是compaction,这是管理内存碎片的方法。Compaction基本来说就是把对象移动到一起,从来释放更大的连续内存空间。如果你熟悉磁盘碎片和处理它的工具,你会发现compaction跟它很像,不同的是这个运行在Java堆内存中。我将在系列中详细讨论compaction。
总结:回顾和重点
JVM允许可移植(一次编程,到处运行)和动态的内存管理,所有Java平台的主要特性,都是它受欢迎和提高生产力的原因。
在第一篇JVM性能优化系统的文章中我解释了一个编译器怎么把字节码转化为目标平台的指令语言的,并帮助动态的优化Java程序的执行。不同的应用需要不同的编译器。
我同样简述了内存分配和垃圾收集,和这些怎么与Java应用性能相关的。基本上,你越快的填满堆和频繁的触发垃圾收集,Java应用的占有率越高。垃圾收集器的一个挑战是在分配内存时,需要尽量不影响运行着的应用,但要在应用耗尽内存之前。在以后的文章中我们会更详细的讨论传统的和新的垃圾回收和JVM性能优化。
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具

我要回帖

更多关于 jvm堆内存分配 的文章

 

随机推荐