对于创建大量对象的大型应用程序,JVM 花在垃圾收集(GC)上的时间会非常多。默认情况下,进行 GC 时,整个应用程序都必须等待它完成,这可能要有几秒钟甚至更长的时间(Java 应用程序启动器的命令行选项 -verbose:gc 将导致向控制台报告每一次 GC 事件)。要将这些由 GC 引起的暂停(这可能会影响快速任务的执行)降至最少,应该将应用程序创建的对象的数目降至最低。同样,在单独的 JVM 中运行计划代码是有帮助的。同时,可以试用几个微调选项以尽可能地减少 GC 暂停。例如,增量 GC 会尽量将主收集的代价分散到几个小的收集上。当然这会降低 GC 的效率,但是这可能是时间计划的一个可接受的代价 资料引用:
* JVM 的类型:服务器(-server)与客户机(-client)。
* 确保有足够的内存可用(-Xmx)。
* 使用的垃圾收集器类型(高级的 JVM 提供许多调优选项,但是要小心使用)。
* 是否允许类垃圾收集(-Xnoclassgc)。默认设置是允许类 GC;使用 -Xnoclassgc 可能会损害性能。
* 是否执行 escape 分析(-XX:+DoEscapeAnalysis)。
* 是否支持大页面堆(-XX:+UseLargePages)。
* 是否改变了线程堆栈大小(例如,-Xss128k)。
* 使用 JIT 编译的方式:总是使用(-Xcomp)、从不使用(-Xint)或只对热点使用(-Xmixed;这是默认选项,产生的性能最好)。
* 在执行 JIT 编译之前(-XX:CompileThreshold)、后台 JIT 编译期间(-Xbatch)或分级的 JIT 编译期间(-XX:+TieredCompilation)收集的剖析数据量。
* 是否执行偏向锁(biased locking,-XX:+UseBiasedLocking);注意,JDK 1.6 及更高版本会自动执行这个特性。
* 是否激活最近的试验性性能调整(-XX:+AggressiveOpts)。
* 启用还是禁用断言(-enableassertions 和 -enablesystemassertions)。
* 启用还是禁用严格的本机调用检查(-Xcheck:jni)。
* 为 NUMA 多 CPU 系统启用内存位置优化(-XX:+UseNUMA)。
Class Data Sharing类共享.
java5引入了类共享机制,指在java程序第一次启动时, 优化一些最常用的基础类到一个共享文件中,暂只支持Client VM和serialGC.存放在client/classes.jsa中, 这就是为什么程序在第一次执行较慢的原因. 开启参数-Xshare.
J2SE 6(代号:Mustang野马)主要设计原则之一就是提升J2SE的性能和扩展能力,主要通过最大程度提升运行效率,更好的垃圾收集和一些客户端性能来达到。
1、偏向锁(Biased locking)
Java 6以前加锁操作都会导致一次原子CAS(Compare-And-Set)操作,CAS操作是比较耗时的,即使这个锁上实际上没有冲突,只被一个线程拥 有,也会带来较大开销。为解决这一问题,Java 6中引入偏向锁技术,即一个锁偏向于第一个加锁的线程,该线程后续加锁操作不需要同步。大概的实现如下:一个锁最初为NEUTRAL状态,当第一个线程加 锁时,将该锁的状态修改为BIASED,并记录线程ID,当这一线程进行后续加锁操作时,若发现状态是BIASED并且线程ID是当前线程ID,则只设置 一下加锁标志,不需要进行CAS操作。其它线程若要加这个锁,需要使用CAS操作将状态替换为REVOKE,并等待加锁标志清零,以后该锁的状态就变成 DEFAULT,常用旧的算法处理。这一功能可用-XX:-UseBiasedLocking命令禁止。
2、锁粗化(Lock coarsening)
如果一段代码经常性的加锁和解锁,在解锁与下次加锁之间又没干什么事情,则可以将多次加加锁解锁操作合并成一对。这一功能可用-XX:-EliminateLocks禁止。
3、自适应自旋(Adaptive spinning)
一般在多CPU的机器上加锁实现都会包含一个短期的自旋过程。自旋的次数不太好决定,自旋少了会导致线程被挂起和上下文切换增加,自旋多了耗CPU。为此Java 6中引入自适应自旋技术,即根据一个锁最近自旋加锁成功概率动态调整自旋次数。
4、常用大内存分布的堆(large page heap)
在大内分页是x86/amd64架构上用来减小TLB(虚拟地址到物理地址翻译缓存)大小的TLB失配率。Java 6中的内存堆可以使用这一技术。
5、提高数组拷贝性能
对每种类型大小写一个定制的汇编数组拷贝程序。
6、后台进行代码优化
Background Compilation in HotSpot™ Client Compiler: 后台进行代码优化
7、线性扫描寄存器分配算法(Linear Scan Register Allocation):
一种新的寄存器分配策略,基于SSA(static single assignment),性能提高10%左右。常用的寄存器分配算法将寄存器分配看作图着色问题,时间复杂度是O(n^4),不适用于Java的JIT编译。原来的JVM里是根据一些本地启发式规则来分配寄存器,效果不太好,Java 6中使用的线性扫描寄存器算法能够达到与图颜色算法相似的效果,并且时间复杂度是线性的。
8、并行缩并垃圾收集器(Parallel Compaction Collector)
进行Full GC时使用并行垃圾收集(JDK 5里原来非Full GC是并行的但Full GC是串行的),使用-XX:+UseParallelOldGC开启这一功能
9、并行低停顿垃圾收集器(Concurrent Low Pause Collector)
显式调用gc(如System.gc)时也可以并行进行标记-清扫式垃圾收集,使用-XX:+ExplicitGCInvokesConcurrent开启。
10、Ergonomics in the 6.0 Java Virtual Machine
自动调整垃圾收集策略、堆大小等配置,这一功能在JDK 5中加入,JDK 6中得到显著增强,SPECjbb2005性能提高70%。
11、boot类装载器的优化
jre中增加一个描述package所在jar文件的元索引文件,加快classloader加载类性能,提高桌面Java应用启动速度(+15%)。内存占用也减少了10%
12、图形程序优化
在jvm启动之前显示splash。
OutOfMemoryError是内存溢出, 有多种情况会出现内存溢出.
1.java堆溢出java.lang.OutOfMemoryError: Java heap space.
2.java永久堆溢出,通常是反射,代理用的较多导致类生成过多,java.lang.OutOfMemoryError: PermGen space.
3.本地堆溢出,这可能是由于操作系统无法分配足够的内存,可能是系统已无内存,还可能是java进程内存空间耗尽,这里有点意思,一般32位系统进程只 有4G地址空间,而又因为java实现使用本地堆或内存映射区作为java堆的存储空间,再去除内核映射区,java使用的堆一般只有2G以内,而如果 java堆xmx占的过大,导致jni的本地堆过小,也会生成内存溢出.本地堆可以是jni用new, malloc,也可能是DirectBuffer等实例.
java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
这时候,如果java堆足够用的话, 减少xmx的值,反而会解决这种问题.
4.jni方法的溢出.而前者是由jvm检测的本地溢出,而此是在jni方法调用时,无法分配内存.
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
JDK7性能优化.
1.(Zero Based )Compressed OOPS
在64位CPU中, JVM的OOP(Ordinary object pointer)为64位, 简单的讲,OOP可以被认为为对象的引用,虽然java中基本类型位数是固定的, 但引用类型(简化的C语言指针)用于指向堆中的地址很自然的会被扩展成机器的字长. 32位系统最大可访问内存为4G,为了突破这个限制64位系统已经很常见,但是单单引用从32位转为64位,堆空间占用大概会增加一半,虽然内存已经很便宜, 但是内存带宽,CPU缓存代价是很昂贵的.
Compressed OOPS压缩可管理的引用到32位以降低堆的占用空间,在JVM执行时加入编/解码指令,类似于8086的段管理,其使用
<narrow-oop-base(64bits)> + (<narrow-oop(32bits)> << 3) + <field-offset>公式确定内存地址.
JVM在将对象存入堆时编码,在堆中读取对象时解码.
而Zero based compressed oops则进一步将基地址置为0(并不一定是内存空间地址为0, 只是JVM相对的逻辑地址为0,如可用CPU的寄存器相对寻址) 这样转换公式变为:
(<narrow-oop << 3) + <field-offset>
从而进一步提高了性能.不过这需要OS的支持.
如果java堆<4G,oops使用低虚拟地址空间,而并不需要编/解码而直接使用.
Zero based compressed oops针对不同的堆大小使用多种策略.
1.堆小于4G,无需编/解码操作.
2.小于32G而大于4G,使用Zero based compressed oops
3.大于32G, 不使用compressed oops.
Escape Analysis Improvements
当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸 (Escape),也就是说变量不仅仅在本方法内使用. Java对象一般被认为总是在堆中分配的, 这使得任何对象都需要进行垃圾回收.而大多数情况下,方法内的对象仅在本方法中使用,完全可以使用栈来存储,栈内变量释放是最自然,性能最好的,C中的 struct即在分配在栈中.如果实现引用逃逸分析,便可以把没有引用逃逸的对象分配在栈中,而且不必在语言上加入新的定义方法,引用逃逸分析是自动 了.JDK7已经开始缺省支持的逃逸分析了.另此还可以消除同步,如果其分析得知对象为非引用逃逸,则所有该对象的同步操作都可以被取消(当然这本是程序 员的任务,比如StringBuffer),另可优化对象的部分甚至全部都保存在CPU寄存器内.
NUMA Collector Enhancements
NUMA(Non Uniform Memory Access),NUMA在多种计算机系统中都得到实现,简而言之,就是将内存分段访问,类似于硬盘的RAID,Oracle中的分簇,JVM只不过对此加以应用而矣.
以上三个特性也能在有些JDK6中打开,具体需要看各版本的changenotes. java6中加入了诸如以下的性能优化手段:
轻量锁 使用cas机制减少锁的性能消耗.
偏向锁(biased locking)
锁粗化(lock coarsening)
由逸出(escape)分析产生的锁省略 逸出分析还能够分配内存在栈中,以减少内存回收的压力.
自适应自旋锁(adaptive spinning) 自旋锁只有在物理多CPU中才会效果.
锁消除(lock elimination)
在多核CPU中,锁的获取比单核系统耗费资源相对大的多, 因为在多核系统中,锁的获取需要CPU阻塞数据总线,高速缓存写回.
这样有时候, 我们在单核系统中,经常会得到StringBuffer与StringBuilder性能差不多的用例, 而且由于有了锁消除等技术, 有些情况在多核CPU中也会得到性能相差不多的情况.
据信Java7还将缺省支持OpenGL的加速功能.
在JDK1.5中加入了Class Data Sharing, 也就是把部分常用的java基本类,缓存在文件或共享内存中, 以供所有java进程使用.
从JRE1.5中,java程序启动时,如非使用-client|server指令显示指定,虚拟机会自动选择对应的VM,如在64位系统中,只实现了serverVM,所有的虚拟机都会使用server VM. 32位的系统则windows缺省使用clientVM,而Linux,solaris则根据CPU个数和内存来确定是否使用serverVM,如jre6以2CPU,2GB物理内存为界.
GC
衡量GC效率的参数主要有两个,一个是吞吐量(即效率),一个是停顿时间,另外还有footprint,就是占用的堆大小.
GC算法.
1.拷贝,将所有仍然生存的对象搬到另外一块内存后,整块内存就可回收。这种方法有效率,但需要有一定的空闲内存,拷贝也有开销.
2.跟踪收集器,跟踪收集成追踪从根节点开始的对象引用图。基本的追踪算法叫作“标记并清除”,也就是垃圾收集的两个阶段。标记阶段,垃圾收集器遍历引用 数,标记每一个遇到的对象。清除阶段,未被标记的对象被释放。可能在对象本身设置标记,要么就是用一个独立的位图来设置标记。 压缩(可选),垃圾收集同 时要应对碎片整理的任务。标记和清除通常使用两种策略来消除堆碎片:压缩和拷贝,这两种方法都是快速移动对象来减小碎片, 加在一起叫做mark-sweep-compact.
3.还有一种引用计数收集器,这种方法时堆中的每个对象都有一个引用计数,在引用赋值时加1,置空或作为基本类型的引用超出生命期(如方法退出而栈回收)时减1,其对多个对象的循环引用无能为力,但引用计数都不为0 ,还有引用数的增减带来额外开销,故已不再使用.
分代收集器
根据程序的统计, 大多数对象生命周期都很短,都很快被释放掉.但也有部分对象生命周期较长, 甚至永久有效. 对于拷贝算法来说,每次收集时,所有的活动对象都要移动来移动去。对于短生命的对象还好说,经常可以就地解决掉,可是对于长生命周期的对象就纯粹是个体力 劳动了,把它挪来挪去除消耗大量的时间,没有产生任何效益。分代收集能直接让长生命周期的对象长时间的呆在一个地方按兵不动。GC 的精力可以更多的花在收集短命对象上。
这种方法里,堆被分成两个或更多的子堆,每一个堆为一“代”对象服务。最年幼的那一代进行最频繁的垃圾收集。因为多数对象是短命的,只有很小部分的年 幼对象可以在经历第一次收集后还存活。如果一个最年幼的对象经历了好几次垃圾收集后仍是活着的,那这个对象就成为寿命更高的一代,它被转移到另外一个子堆 中去。年龄更高一代的收集没有年轻一代来得频繁。每当对象在所属的年龄代中变得成熟(多次垃圾收集后仍幸存)之后,就可以转移到更高年龄的一代中去。
分代收集一般在年轻堆中应用于拷贝算法,年老代应用于标记清除算法。不管在哪种情况下,把堆按照对象年龄分组可以提高最基本的垃圾收集的性能。
一般java中分代收集器将堆分为年轻代, 年老代和永久代. 年轻代的收回称为minorGC,因为在此期内,对象生命周期很较,故效率较高, 年老代称为FullGC,对应的效率较低,用时较长,应尽量减少FullGC的次数.
VM,
Client VM 适合桌面程序,启动快, 运行时间短, 故其不会预先装入太多的类,对类进行过多优化.
Server VM 适合服务程序,启动时间不重要,运行时间较长, 会预先装入大多基础类,对类进行优化.
GC种类.
Serial 串行回收器(缺省)
在GC运行时, 应用逻辑全部暂停,利用单线程通过"拷贝"进行年轻代的垃圾收集,单线程使用"标记-清除-压缩"进行年老代(tenured)垃圾回收. 吞吐率较高.适合单CPU硬件.
Parallel 并行回收器
针对年轻代使用多个GC线程进行"拷贝"垃圾收集,针对年轻代的GC运行时,程序暂停, 年老代依然是单线程使用"标记-清除-压缩"进行年老代垃圾回收,GC运行时, 应用同样暂停.在大内存,多处理器的机器上,可以考虑使用此Parallel GC(使用参数-XX:+UseParallelGC指定),这种GC在对YoungGen进行GC时,可以对多处理器加以利用,从而相对降低了停顿时间,但重点是提高了吞吐量,但是,在其对OldGen进行GC时,依然使用了和Serial GC同样的算法。所以在Jdk5U6中,又引入了Parallel Compacting Collector(使用参数-XX:+UseParallelOldGC指定),这种GC对OldGen的GC也可以受益于多处理器。由于对OldGen的GC远比YoungGen更耗时间,所以理论上这种Garbage Collector可以提供更优的性能,而且,值得注意的是,Parallel Compacting GC最终会取代Parallel GC。
Concurrent mark-sweep 并发回收器.
对于年轻代使用和多GC线程"拷贝"回收,此GC也需要暂停应用,但由于minorGC效率较高,故不会产生大的停顿,对于年老代使用与应用程序同时运行 的并发方式标记-回收机制,其将步骤再次分细,部分阶段(初始标记,重新标记)也会完全导致应用暂停,但时间较短,大部分时间都是应用程序与单GC线程并 发,降低了应用程序暂停的时间。这种GC使用了和Parallel GC一致的YoungGen的收集算法,而在对OldGen进行GC时,它采用了较为复杂的算法,提供了极短的停顿时间。但是,复杂的算法也造成了更大的 开销,而且这种 Parallel GC是non-compacting的,所以它使用一个空闲块链表来管理OldGen Heap,分配空间的开销也加大了.在某些场景中,较短的停顿时间比较大的吞吐量更加重要,这时可以考虑使用此GC,即所谓的CMS GC。
增量收集器(Train算法)已逐渐被弃用,-XincGC 在1.5中会选中并发GC.
在SUN J2SE 5.0中,引入了所谓Behavior-based Parallel Collector Tuning,这种调优方式基于三个Goal:
Maximum Pause Time Goal: 使用参数-XX:MaxGCPauseMillis=n指定,默认值为空。这个参数被指定后,三个内存区的GC停顿时间都会尽力的保持在n毫秒以内,如果无法满足,则相应的内存区会缩小,以缩短GC的停顿时间;
Throughput Goal: 使用参数-XX:GCTimeRatio=n指定,默认值为99,即GC时间占总的应用运行时间为1%。如果无法满足,相应的内存区会扩大,以提高应用在两次GC间的运行时间;
Footprint Goal: 由于眼下内存泛滥,所以这个Goal一般就不值得关注了;
这三个Goal的优先级为从上到下,即首先满足Maximum Pause Time Goal,再满足Throughput Goal,最后再满足Footprint Goal。
使用参数-Xloggc:file和-XX:+PrintGCDetails打印gclog,然后使用gcviewer对gclog进行查看,它的优势在于可以生成统计数据,吞吐量,最大的和最小的停顿时间,Full GC时间占整个GC时间的百分比等,都可以使用这个工具查看,但目前只支持到1.5。
JConsole是允许您监测各种各样的VM资源运行时使用情况的Java监视和管理控制台。实际在java5中, 需要加一个参数, 在java6中由于支持了attach API,jconsole会自动加载JVM内部的JMX代理.
jstat命令打印各种各样的VM统计数据,包括内存使用、垃圾回收时间、类加载和及时编译器统计。 jmap 命令允许您获得运行时的堆直方图和堆转储。jhat命令允许您分析堆转储。jstack命令允许您获得线程堆栈跟踪。这些诊断工具可以附加到任何应用程序,不需要以特别方式启动。