您的位置:首页 > 编程语言 > Java开发

JAVA程序性能优化

2015-04-11 00:00 253 查看
摘要: <JAVA程序性能优化>--葛一明 一本内容比较前面的程序优化书籍,可能因为内容太全面了,每一章其实没有做很深入的讲解。内容总得来说比较浅显适合有一定工作经验的JAVA程序员阅读。读这本书有个好处是可以系统地了解程序性能优化的体系。不过对于其中的细节,还需要深入研究。本篇博客主要是我根据这本书做的一些总结,加上一些自己的理解。

一、性能调优概述

性能调优概述本章主要是一些理论性的知识。但是同样非常重要,如果要全面地了解一项技术,全面的理论基础可以让对技术了解更加透彻。主要是了解一些关键词和专业术语。

1.1程序性能的体现

执行速度,处理请求和响应的速度

资源使用,合理的内存使用、合理的CPU占用

启动时间,从运行到可以正常处理业务所需的时间

负载承受能力,系统压力上升时,系统的执行速度、响应时间的上升曲线是否平缓

1.2性能的参考指标

执行时间

CPU时间

内存分配

磁盘吞吐量(I/O)

网络吞吐量

响应时间

1.3可能的系统瓶颈

磁盘I/O

网络操作

CPU

异常(这里提到异常的捕获和处理非常消耗资源,因此我们在代码设计中要注意规避不必要的异常)

数据库(个人认为数据库是最容易成为系统瓶颈的,尤其在系统访问量达到一定程度之后)

锁竞争(在多线程的程序设计中,合理的锁使用对性能影响至关重要)

内存(内存的大小设置如果合理,一般来说出现瓶颈的可能不大,即便出现了,也应该是代码的原因)

1.4性能调优的层次

1.4.1设计优化

设计优化处于所有调优手段的上层,好的系统设计可以避免系统后续出现的大部分问题。这里举了一个设计优化的例子:系统A需要等待事件E的发生才能触发一个行为,如果系统A通过轮询的方式监测事件E的发生,这无疑是很低效的。更好的做法是当事件E发生后,主动通知到系统A,系统的实时反应性就可以提交很多。这仅仅是一个小例子,系统设计涵盖的面很广,个人觉得《大型网站架构技术》这本书对系统设计做了比较全面的分析,虽然对于大部分业务系统来说,我们并不需要设计得与现在的阿里巴巴、腾讯的系统一样庞大,但是应该具有相同的架构思想,在系统性能问题出现时,才能够灵活地提出相应的解决方案。不提倡过度的系统设计,应该根据现有的业务和可能的业务扩展,设计出合理的系统架构。

1.4.2代码优化

代码优化更多的是体现一个程序员经验的部分。有一定开发经验的应该去读读<Effective Java>这本书,里面介绍了一些java开发需要注意的细节。帮助我们编写出更高质量的代码。同时可以多读开源的代码,从优秀开源源码中学习代码编写的技巧。代码优化比较繁杂,大到开源框架的选择,小到API的选择和使用,这里不详细赘述。

1.4.3JVM调优

说实话,我工作四年以来,还真没有遇到过JAVA性能的问题。也可能是因为做的开发工作比较简单吧。但是偏偏找工作的时候面试官还喜欢问性能调优之类的问题,无奈,学吧。JVM调优咋听起来好像是很高级的名词。其实也不过是一些JVM参数的选择、垃圾回收器的选择,跟代码优化一样,更多的是一些经验性的东西。虽然我们目前工作可能没有遇到相关的问题而用不上,但是提前做好知识储备总是好的。JVM调优后面有专门的章节会总结。

1.4.4.数据库调优

可以说,只要是个系统,就会有数据库。当然,这个可能有点绝对了。不过这也可以看出数据库的常见性。对于大部分应用来说,当访问量与并发量上去之后,系统的瓶颈基本都会出现在数据库层面上。数据库调优涵盖的面也是很庞大的,本书没有涉及到数据库性能调优的方面,但我也加了一些个人的理解在第四章。

1.4.5 操作系统调优

操作系统调优对我们普通程序员来说,貌似就太遥远了,不过还是简单了解一些专业术语吧。操作系统调优时可能涉及到的一些方面有

共享内存段(最大、最小值等)

信号量

最大文件句柄数

虚拟内存大小

磁盘块大小等

听起来都相当地高端,如果读者有兴趣去了解一下的话,相信以后面试会非常的牛叉。哈哈!

1.5调优手段和原则

调优手段方法,无非就是确定调优目标,反复测试,最终达到目标。不过很有意思的是,书中提到了:不到万不得已的时候,不要做性能优化。 这句话其实在很多书中都提到过。虽然这么说,但是不影响我们学习程序优化的作用,学习如何优化,可以帮助我们站在更高的层次,设计出更合理的系统。

二、系统设计优化

2.1设计模式

讲到设计优化,怎么可能会不谈设计模式呢?这里我们只简单介绍一些常见的设计模式,如果要了解更多设计模式的知识,还是需要看专业的书籍。常见的设计模式有

单例模式(还用说么?我们认识的第一个设计模式就是单例模式了,但是单例模式还是有很多学问的,有懒汉式、恶汉式,考虑线程安全和延迟加载的情况下还有一种最优的单例模式叫:内部类单例模式 有兴趣的读者可以自行百度)

代理模式(很常见也很简单的设计模式,但是非常实用,深入研究的话可以分为静态代理模式和动态代理模式,其中动态代理模式在开源框架中非常地常用,Spring、Mybatis、Hibernate都有它们的影子,我们要了解到动态代理与性能调优之间的关系,在JVM章节中我们还会聊到,永久区与动态代理的关系)

装饰者模式(看起来很陌生,但是我们肯定经常使用到。就是在I/O族的API中。装饰者模式可以帮助我们将核心组件与装饰组件分离开来,最典型的应用就是性能组件与功能组件分离,例如I/O中的Buffer缓冲)

观察者模式

享元模式

等等。。。设计模式包含的内容还是挺多的,了解设计模式可以帮助我们写出更加可扩展、高效的代码。

2.2常用的优化组件

2.2.1缓冲

正如上面说到的装饰者模式应用一样,缓冲组件的一个实现方式就是用装饰者模式。其思想可以衍生至异步的思想。我们知道,为了提高系统的吞吐量,我们可以对特定业务采用异步的方式。异步的实现可以是数据库、MQ、文件甚至是内存。帮助我们解决业务逻辑处理时间过长,导致的请求等待问题。读者可以了解一下I/O的缓冲实现,核心思想是I/O操作是非常消耗系统内存的。因此频繁的磁盘写入可能会让我们的程序性能变差,如果设置一个缓冲区,当写入的数据达到一定大小时,我们再写入磁盘。以达到节省系统资源的目的,这个思想与我们数据库操作批量写入的思想是一致的。

2.2.2缓存

缓存对我们系统来说至关重要,可以解决很多的问题。最直观的好处就是加快系统的响应速度,我们可以将数据库、文件、甚至是网络远程的数据缓存到本地服务器的内存中,这个访问的速度提升是显而易见的。一个系统的数据库系统如果严格按照三大范式进行设计,我们的配置表也会被分的非常细,在数据查询时有时甚至要关联四五个表,才能查出我们需要的数据。此时有两种常见的解决办法:1、冗余和反范式的系统设计2、将配置表缓存 两种做法各有优劣。可以根据我们的业务进行选择。

单服务器简单的缓存,如果不想引入缓存框架,我们可以简单地用HashMap来实现,如果缓存的数据过多,可以选择用WeakHashMap来实现内存敏感的高速缓存,但是目前其实已经有很多非常优秀的缓存框架了,我们为什么要重复发明轮子呢?可以使用EHCache进行缓存,如果有分布式和高可用的需求,可以用Redis、Memcached、Tair缓存框架。

2.2.3复用

这里说的复用非常笼统,但是传递一个思想,就像我们写代码一样。我们追求代码的整洁,不写重复代码最好的办法就是复用。我们的系统组件也可以进行复用,这也是为什么Maven极力推荐我们用基于模块的开发的原因。本书介绍了一些复用的技术:对象池、数据库连接池等,其实个人认为不要刻意地使用这些复用技术,我们可以用目前已有的框架和技术,就像JDK API一样,专业的人写出来的东西肯定是更好的。经过更加详细测试的。

三、JAVA程序优化

3.1 代码优化

笔者认为,代码优化应该包括以下方面:1.代码风格、代码规范的统一(便于开发和后续维护)2.选用更佳的API(要求程序员们多了解更多的API库,选用更适合的API)3.考虑程序的扩展性(抽象、复用)4.并发程序的开发

书中介绍了JAVA的四种引用类型,虽然在开发过程中用不上,不过还是有了解的必要

强引用(我们见得最多的就是强引用 new)

弱引用(SoftReference 只要内存够大,就不会回收)

软引用(WeakReference GC时就会被回收)

虚引用(PhantomReference 随时可能被回收,可以用来跟踪垃圾回收过程)

3.2 并发程序设计

并发程序是复杂的,入门推荐《并发编程实战》一书,这里简单介绍并发编程的知识点

Future模式异步计算

Master-Worker模式

生产者-消费者模式

JDK线程池框架(Executor框架 newFixedThreadPool newCachedThreadPool newSingleThreadExecutor newScheduledThreadPool)

并发数据结构(并发List/Set CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentHashMap ConcurrentLinkedQueue BlockingQueue)

volatile

synchronized

ReentranLock/ReadWriteLock

Semaphore

ThreadLocal



无锁的并行计算(原子操作、CAS算法)

四、数据库调优

4.1 数据库设计调优

三大范式设计

反范式设计(为了解决多表关联查询的问题)

4.2 数据库调优

分库

分表(垂直切分、水平切分)

备份(历史表)

主备(Master-Slave)

读写分离

数据库集群

4.3 应用层数据库调优

使用缓存解决多表关联

批量写入代替频繁写入

查询、排序使用索引

只选择所需的行和列

五、JVM调优

5.1Java虚拟机内存模型

5.1.1程序计数器

线程私有的,记录下一条要执行的指令。如果当前线程正在执行一个Java方法,则程序计数器记录正在执行的JAVA字节码地址。

5.1.2虚拟机栈

线程私有空间,又称线程栈,和java线程在同一时间创建,保存局部变量,部分结果,并参与方法的调用和返回,可以用-Xss设置栈大小。栈的大小决定了函数方法的递归可达深度。可能抛出的异常有:1.StackOverflowError 2.OutOfMemoryError

5.1.3本地方法栈

与线程栈类似,只是本地方法栈管理的是本地方法。会抛出同样的异常

5.1.4 Java堆

可以简单分为新生代和老年代,新生代又细分为eden、s0、s1,eden存放新建对象,s0,s1存放至少经历了一次垃圾回收的对象。

5.1.5方法区

与堆内存一样,是被所有线程共享的,主要保存的是类元数据,也称之为永久区,方法区中还含有字符串常量池的管理。

5.2JVM内存分配参数

5.2.1堆内存设置

最大堆内存和最小堆内存设置 -Xmx -Xms,当内存使用触及到-Xms时会触发Full GC

5.2.2新生代设置

-Xmn设置新生代大小

5.2.3持久代设置

-XX:PermSize 设置持久代大小,对于大量使用动态代理的系统,应该设置较大的持久区,一般来说64M就已经足以满足大部分系统需求了,128M已经是很大了。如果仍然溢出,应该考虑查询原因

5.2.4线程栈设置

-Xss设置线程栈的大小

5.3垃圾回收基础

5.3.1垃圾回收算法与思想

引用计数法 (无法解决循环引用的问题)

标记清除法(会造成内存碎片问题)

复制算法(需要较大的内存支持,但是可以解决内存碎片问题,适用于新生代的垃圾回收算法)

标记压缩算法(适用于老年代的垃圾回收算法,压缩的成本较高)

增量算法(可以减少系统的停顿时间,但是由于垃圾线程与系统线程切换频繁,造成系统吞吐量下降,CMS垃圾回收器)

分代(其实上述算法没有哪一种能够完全替代其他算法,因此现代垃圾回收器都是使用分代的思想进行垃圾回收的)

5.3.2垃圾收集器类型

串行垃圾回收器(串行垃圾回收器一次只使用一个线程进行垃圾回收)

并行垃圾回收器(并行垃圾回收器一次使用多个线程进行垃圾回收)

并发垃圾回收器(并发垃圾回收器,垃圾回收线程与系统线程并行,以减少系统停顿时间)

独占式垃圾回收器(一旦运行就Stop the world,等待垃圾回收完成)

5.3.3GC策略指标

吞吐量

垃圾回收器负载

停顿时间

垃圾回收频率

反应时间

堆分配

5.3.4主流垃圾回收器

新生代串行收集器(单线程串行、独占式的垃圾回收器 回收过程中程序线程是暂停的 -XX:+UseSerialGC可以开启新生代串行垃圾回收器,老年代当JVM是Client模式时它是模式的垃圾回收器)

老年代串行收集器(使用的标记压缩算法,同样是一个串行的、独占式的垃圾回收器,且回收老年代需要比新生代更长的时间 -XX:+UseSerialGC可以设置新生代老年代都使用该垃圾回收器)

并行收集器(采用的算法和实现与新生代串行收集器一样,但是它是多线程的,在多CPU的情况下表现比串行收集器更好-XX:+UseParNewGC新生代使用并行收集器、老年代使用串行回收器)

新生代并行回收收集器(Parallel Scavenge) 表面上与串行收集器一样也是单线程的、独占式的垃圾回收器,也是使用复制算法,但它非常关注系统吞吐量

老年代并行回收收集器(这是一种多线程并发的收集器,使用标记压缩算法,JDK1.6以后才能使用 -XX:+UseParallelOldGC可以新生代和老年代都使用并行回收收集器,对吞吐量敏感)

CMS收集器(CMS垃圾回收器主要关注系统的停顿时间,它是使用多线程并行的垃圾回收器,会有两次停顿时间,使用标记清除法,会有内存碎片问题)

G1收集器(使用标记压缩算法,1.7以后才能使用,在吞吐量和停顿控制上都优于CMS垃圾回收器-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC)

5.3.5常用的调优案例和方法

5.3.5.1 将新生对象留在新生代

由于Full GC的成本要远高于Minor GC,因此,将对象分配在新生代是一项明智的做法。因此设置一个合理的新生代空间或者设置新生代空间的利用率非常重要

5.3.5.2 大对象进入老年代

出于5.3.5.1的考虑,我们应该将大对象分配在老年代,防止占用过多的新生代内存。在软件开发的过程中,我们应该避免使用短命的大对象。可以通过设置大对象直接进入老年代的阀值,将大对象提前分配到老年代

5.3.5.3 稳定与震荡的堆大小

获得稳定堆大小的方法是设置-Xms和-Xmx相同。可以减少GC的次数,但是每次GC的成本会增加。

5.3.5.4 吞吐量优先

吞吐量优先的方案可以减少系统的执行垃圾回收总时间,可以通过设置关系系统吞吐量的的并行回收器。

5.3.5.5 使用大页

使用大页可以增强CPU的内存寻址能力,从而提高系统性能

5.3.5.6 降低停顿

可以通过使用关注系统停顿时间的CMS垃圾回收器,为了减少Full GC的次数,应该尽量将对象预留在新生代。新生代使用并行垃圾回收器,老年代使用CMS垃圾收集器

5.3.6实用JVM参数

JIT编译参数
可以优化函数被调用次数过多的代码,-XX:+PrintCompilation

堆快照
在程序发生OOM退出系统时,导出应用程序的当前堆快照。使用-XX:+HeapDumpOnOutOfMemoryError,指定保存的堆快照位置后可以用Visual VM进行分析

GC信息
通过-verbose:gc或者-XX:+PrintGCDetails打印出GC信息

类和对象跟踪
通过设置-XX:+TraceClassLoading 可以打印出类加载的信息,可以看到类从哪个jar中加载,用于定位jar包冲突问题

使用大页
-XX:+UseLargePages

六、性能调优工具

6.1Linux命令行工具

6.1.1top命令

查看系统各个进程的资源占用状态和系统负载、资源使用情况

6.1.2sar命令

定期对CPU、内存进行采样

6.1.3vmstat命令

统计CPU、内存使用情况

6.1.4iostat命令

统计I/O信息

6.1.5pidstat命令

监控进程的运行情况

6.2JDK命令行工具

6.2.1 jps命令

非常实用的命令,可以取代ps -ef|grep java 快速列出所有的java应用

6.2.2 jstat命令

查看堆信息的详细情况

6.2.3 jinfo命令

查看正在运行的java程序的扩展参数,甚至支持运行时修改部分参数

6.2.4 jmap命令

可以生成java应用程序的堆快照和对象统计信息,可以通过Visual VM来分析快照文件

6.2.5 jhat命令

用于分析JAVA应用程序的堆快照内容

6.2.6 jstack命令

用于导出java应用程序的线程堆栈,可以用来定位死锁的问题

6.2.7 jstatd命令

用于查看远程服务的情况

6.3其他工具

6.3.1 JConsole工具

JConsole工具是JDK自带的图形化性能监测工具,通过JConsole工具,可以查看Java程序的运行情况,监控堆信息、永久区使用情况、类加载情况等。

6.3.2 Visual VM工具

Visual VM是一个功能更加强大的多合一故障诊断和性能监控的可视化工具,使用Visual VM可替代绝大部分JDK工具甚至JConsole,JDK7以后,它已经作为JDK的内置工具发布了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息