Chinaunix首页 | 论坛 | 博客
  • 博客访问: 4478422
  • 博文数量: 344
  • 博客积分: 2173
  • 博客等级: 上尉
  • 技术积分: 7846
  • 用 户 组: 普通用户
  • 注册时间: 2011-08-24 17:26
个人简介

凡是过往,皆为序章,2020加油。

文章存档

2020年(6)

2019年(2)

2018年(2)

2017年(34)

2016年(49)

2015年(53)

2014年(47)

2013年(72)

2012年(79)

分类: Java

2015-01-31 21:58:45

随着Java的普及和广泛应用,使得JVM的性能优化变得越来越重要。本文从JVM虚拟机的角度,特别是垃圾回收机制来剖析了 Java 应用程序设计需要注意的方面,并总结了几点在开发过程中非常容易被忽视的设计、编写代码的原则。

一、在优化JVM之前首先我们来认识一下JVM的体系结构

1、类装载子系统:通过类的全限定名(包名和类名,网络装载还包括 URL)将 Class 装载进运行时数据区;

2、方法区:Class 对于所有方法和 static 静态数据的定义存储在这里,它就像一张表或数组,让程序执行时在这里找到相应方法的 Java 字节码和静态数据;

3Java 堆:Java 对象的持久化存储区,从类实例化而来的对象存储在此,垃圾收集也在此进行,若是空间不够容纳当前所有对象,Out Of Memory 的异常将会抛出,对 Java 堆和垃圾收集的认识对应用性能调优很关键;

4Java 栈:Java 方法的字节码执行的地方,方法中局部变量的生命周期都在栈中,栈的大小是我们要考虑的一个关键点,它直接决定了方法调用的层数,这对递归程序来说尤为重要。我们所用的 JVM 都是基于 Java 栈的运行机制,而有一个例外的实现,Google 移动设备操作系统 Android 的虚拟机 Dalvik 则是基于寄存器的机制(Dalvik 虽然支持 Java 语言开发,但从虚拟机的角度看,并不符合 Java 标准),关于虚拟机实现时,栈和寄存器机制的比较,请参考论文“Virtual Machine Showdown: Stack Versus Registers”;

5、程序计数器:对于基于栈实现的 JVM,这几乎是唯一寄存器了,它用来指示当前 Java 执行引擎执行到哪条 Java 字节码,指针指向方法区的字节码;

6、本地方法栈:这是 Java 调用操作系统本地库的地方,用来实现 JNIJava Native InterfaceJava 本地接口);

7、执行引擎:JVM 的心脏,控制装入 Java 字节码并解析;

8、本地接口:连接了本地方法栈和操作系统库。

Java字节码是 JVM 的指令,所有 Java 平台虚拟机有各自的指令集,而大部分指令相同,共 200 条左右,Java Card 虚拟机由于支持的数据类型少,相应的指令较少。部分虚拟机实现商为了优化性能,增加了一些自己特有的指令,当对于 Java 程序员来说,是透明的。

二、其次我们再了解一下JVM垃圾收集机制

JVM采用了按代的垃圾收集机制。垃圾收集就是标识出虚拟机中不被用到的垃圾对象,删除以回收空间。按代垃圾收集算法主要分为三种:

1、复制算法,空间被分为等大的两块,从根开始访问每一个关联的活跃对象,将空间 A 的活跃对象全部复制到空间 B,然后一次性回收整个空间 A,优点:只访问活跃对象,将所有活动对象复制走之后就清空整个空间,不用去访问死对象,所以遍历空间的成本较小,缺点:需要巨大的复制成本和较多的内存;

2、标记清除算法,从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个内存区域,把所有没有标记活跃的对象进行回收处理,优点:不需要额外的空间,缺点:较长的 GC 暂停时间,较大的扫描时间开销,产生较多的空间碎片;

3、标记清除整理算法,综合上两种算法的优点,先标记活跃对象,然后将其合并成较大的内存块。

三、再次我们介绍一下JVM代的划分:

1、年轻代:新创建的对象分配在此,研究表明,大部分程序所产生的对象都在此消亡,几乎所有的收集器为年轻代使用复制算法,年轻代又被划分为 1 个伊甸园区和 2 个存活区用来实施复制算法;

2、年老代:从年轻代存活下来的对象被复制到年老代,主要实施标记清除或标记清除整理算法;

3、持久代:装载的类数据和信息存储于此,无可消亡对象。

Java虚拟机都提供了相应的选项来设置各个代所占用区的大小,无论是Java EE的服务器应用,还是 Java SE桌面应用或产品,都需要经过对运行时对象创建和消亡状态的分析,进行这些选项的合理设置,才能获得较好的性能提升,毕竟垃圾收集是一项耗时的工作。

四、我们再了解一下JVM垃圾收集按频率:

1、次收集(Minor Collection):频繁发生在年轻代,收集快速消亡的对象;

2、主收集(Major Collection):年轻代和年老代的全范围收集,频率很低。

垃圾收集运行时,同一个CPU 上的所有其它线程都将会被阻塞,所以对于 Java 应用程序来说,整个世界似乎停滞了,当整个标记、清除、整理周期完成后,所有应用程序线程得以继续,许多 JVM 实现的垃圾收集机制对多 CPU 的机器环境进行优化,通过同步来实现垃圾收集线程和应用程序线程的并发,使程序获得很好的总体性能。

四、最后我们看看如何调整优化虚拟机参数

通过设置虚拟机参数来配置垃圾收集器的行为和堆中不同区的大小分配。不同虚拟机的实现,参数选项不尽相同。

了解了垃圾收集以及它对性能的影响后,我们可以根据应用程序的特点来设置 GC 的策略进行有效的优化。相关参数是 -Xgcpolicy:[optthruput | optavgpause | gencon | subpool]

-Xgcpolicy:optthruput,针对吞吐量优化,这是虚拟机默认的 GC 策略,适用于两种极端情况:应用需要尽可能快的在短时间内运行结束,或应用长时间运行,且运行过程中的吞吐量没有比较固定的大小和分布;

-Xgcpolicy:optavgpause,针对 GC 导致的停顿优化,通过并发地执行一部分垃圾收集,在高吞吐量和短 GC 停顿之间进行折中。应用程序停顿的时间更短。适用于应用具有比较规则和突发的吞吐量周期分布;

-Xgcpolicy:gencon,分代并发进行 GC,以不同方式处理短期存活的对象和长期存活的对象。采用这种策略时,具有许多短期存活对象的应用程序会表现出更短的停顿时间,同时仍然产生很好的吞吐量;

-Xgcpolicy:subpool,子池优化,采用与默认策略相似的算法,但是采用一种比较适合多处理器计算机的分配策略。适用于在多核环境下运行的具有较高对象创建速率的多线程应用。

除了设置 GC 策略,最常设置的堆大小参数有:-Xms,设置堆的初始大小;-Xmx,设置堆空间的最大值;-Xmn,设置年轻代空间大小;-Xmo,设置年老代空间大小。我们需要根据实际的机器环境和应用本身的特点来设置合理的值。

五、开发过程中我们容易忽视的设计、编程原则和习惯及优化的建议

对虚拟机工作机制的了解能够使我们有把握写出更优雅、更高效的 Java 代码。下面是几条值得参考的设计、编程原则和习惯。

1、及时更新虚拟机

除了由于某些产品兼容性的需要必须使用过去某个虚拟机版本外,建议将开发环境和最终产品部署的虚拟机运行时环境要求更新至最新版本。最新的版本意味着最新的 API,更好的实现优化。

2、良好的面向对象设计和架构,应用设计模式

这点对于 Java 应用的性能、重用和可维护性尤为重要,设计模式是由大师们总结出的解决典型问题的通用架构,用对象来描述问题域,用设计模式来组织对象之间的行为。在设计和解决局部问题时,首先要看看抽象出来的问题是否和某个设计模式的目标问题一致。此外,尽可能多的了解虚拟机所支持的 API,看所需的功能是否已有现成的实现可供调用,虚拟机平台实现的 API 大都具有良好的性能。

3、关心 Java

前面了解到,对于基于栈的 Java 虚拟机,方法的调用和执行伴随着压栈和出栈操作。每个线程有各自独立的栈,由虚拟机来管理栈的大小,但我们应该对它的大小有个概念。栈的大小是把双刃剑,如果太小,可能会导致栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果过大,就会影响到可创建栈的数量,如果是多线程的应用,就会导致内存溢出。通过 -Xss可以设置 Java 栈的最大值,默认值为 256K。不建议设置该选项为其他值,好的方案是,通过优化程序来减少递归层数、避免过大的循环、减少方法的调用层次,让你的程序尽量“扁平”,用尽可能好的对象间的关系来取代少数对象间深层次的方法调用。

4、加强对象管理,不放任自流。

过分依赖垃圾收集有时候会出现严重的性能问题,特别对于在程序运行中伴随着大量大对象创建的情况。好的习惯是显式的释放不用对象的引用,在下一垃圾收集周期中被回收,这一点常常被 Java 程序员忽视,遗留的引用会导致 GC 无法回收这些逻辑上消亡的对象,从而导致内存泄露问题的产生。

阅读(4916) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~