原生内存最佳实践
内存占用
概述
JVM也会为内部操作分配一些内存,这些非堆内存就是原生内存
应用中也可以分配原生内存(通过JNI调用malloc()和类似方法,或者是使用New I/O,即NIO时)
JVM使用的原生内存和堆内存的总量,就是一个应用总的内存占用(Footprint)
测量内存占用
之所以存在已分配内存和保留内存之分,使用JVM(及所有程序)管理内存的方式导致的
线程栈是个例外。JVM每次创建线程时,操作系统会分配一些原生内存来保存线程栈,向进程提交更多内存(至少要等到线程退出)。线程栈是在创建时全部分配的。
内存占用最小化
堆
可以将堆的最大值设置为一个较小的值(或者设置GC调优参数,比如控制堆不会被完全占满),以此限制程序的内存占用
原生NIO缓冲区
调用allocateDirect()方法非常昂贵,所以应该尽可能重用直接字节缓冲区。
直接字节缓冲区所分配的内存总量,可以通过设置-XX:MaxDirectMemorySize=N标志来设定。从Java7开始,这个标志的默认值为0,意味着没有限制。
快速小结
1.JVM总的内存占用堆性能影响很大,特别是当机器上的物理内存有限时。在做性能测试时,内存占用通常应该是要监控的一个方面
2.从调优的角度看,要控制JVM内存占用,可以限制用于直接字节缓冲区、线程栈和代码缓存的原生内存(以及堆)的使用量
原生内存跟踪
从Java8开始,借助-XX:NativeMemoryTracking=off|summary|detail这个选项,JVM支持我们一窥它是如何分配原生内存。
原生内存跟踪(Native Memory Tracking,NMT)默认是关闭的(off模式)。如果开启了概要模式(summary)或者详情模式(detail),可以随时通过jcmd命令获得原生内存的信息: %jcmd process_id VM.native_memory summary
如果JVM是使用-XX:+PrintNMTStatistics参数(默认为false)启动的,他会在程序推出时打印原生内存分配信息。
NMT提供了两类关键信息
总提交大小
进程的总提交大小,是该进程将要消耗的实际物理内存量
每部分的提交大小
当需要调优堆、代码缓存或元空间等不同部分的最大值时,了解此类内存在JVM中实际使用了多少非常有用
NMT跟踪
如果JVM在启动时启用了NMT,可以使用如下命令确定内存的基线使用情况: %jcmd process_id VM.native_memory baseline
利用如下命令,可以比较JVM当前的内存分配情况与基线的差别: %jcmd process_id VM.native_memory summary.diff
快速小结
1.在Java8中,原生内存跟踪(NMT)提供了JVM所使用的原生内存的详细信息。从操作系统的角度看,其中包含JVM堆(对OS而言,堆也是原生内存的一部分)
2.对大多数分析而言,NMT的概要模式足够了。它支持我们确定JVM提交了多少内存(以及这些内存用于干什么了)
针对不同操作系统优化JVM
大页
一般用“页”这个术语来讨论内存分配和交换。页是操作系统管理物理内存的一个单一。也是操作系统分配内存的最小单元
所有的页映射都保存在一个全局页表中(操作系统可以扫描这个表,找到特定的映射),最常用的映射保存在TLB(Translation Lookaside Buffers)中。TLB保存在一个快速的缓存中,所以通过TLB表项访问页要比通过页表访问快的多。
Java支持-XX:+UseLargepages选项
1.Linux大页
大页的大小与计算机的处理器和内核启动参数有关,不过最常见的是2MB
2.Linux透明大页
如果启用了透明大页,就不需要指定UseLargePages标志。如果显式地设置了该标志,JVM会使用传统的大页;如果没有配置传统的大页,则使用标准页。
3.Windows大页
只有服务器版的Windows才支持大页
4.大页的大小
为支持Solaris,Java支持通过-XX:LargePageSizeInBytes=N标志来设置要分配的大页大小,该标志默认值为0,这意味着JVM应该选择特定于处理器的大页大小
快速小结
1.使用大页通常可以明显提升应用的速度
2.在大多数操作系统上,必须显式开启大页支持
压缩的oop
JVM可以使用压缩的oop来弥补额外的内存消耗。
oop代表的是“ordinary object pointer”,即普通对象指针,JVM将其用作对象引用的句柄。
这里有两点启示
第一,对于大小在4GB和32GB之间的堆,应该使用压缩的oop,压缩的oop可以使用-XX:+UseCompressedOops标志启用;在Java7和更新的版本中,只要堆的最大值小于32GB,压缩的oop默认就是启用的。
第二,使用了31GB的堆,并启用了oop的程序,通常要快于使用了33GB的堆空间。
快速小结
1.压缩的oop会在最有用的时候默认开启
2.使用了压缩oop的31GB的堆,与稍微大一些,但因为堆太大而无法使用压缩oop的堆相比,性能通常要好一些
线程与同步的性能
线程池与ThreadPoolExecutor
调节线程池的大小堆获得最好的性能至关重要
所有线程池的工作方式本质上是一样的
有一个队列,任务被提交到这个队列中。一定数量的线程会从该队列中取任务,然后执行。任务的结果可以发回客户端,或保存到数据库中,或保存到某个内部数据结构中,等等。但是在执行完任务后,这个线程会返回任务队列,检索另一个任务并执行(如果没有更多任务要执行,该线程会等待下一个任务)。
设置最大线程数
设置最小线程数
空闲时间应该以分钟计,而且至少在10分钟到30分钟之间。
线程池任务大小
不管是哪种情况,如果达到了队列数限制,再添加任务就会失败。ThreadPoolExecutor有一个rejectedExecution方法,用于处理着各种情况(默认会抛出RejectedExecutionException)
设置ThreadPoolExecutor的大小
根据所选的任务队列的类型,ThreadPoolExecutor会决定何时启动一个新线程。有以下3种可能
SynchronousQueue
如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则新任务会启动一个线程
无界队列
如果ThreadPoolExecutor搭配的是无界队列(比如LinkedBlockedingQueue),则不会拒绝任何任务(因为队列大小没有限制)
有界队列
如果队列已满,而又有新任务加进来,此时才会启动一个新线程。这里不会因为队列已满而拒绝该任务,相反,会启动一个新线程,新线程会运行队列中的第一个任务,为新来的任务腾出空间。
快速小结
1.又是对象池也是不错的选择,线程池就是情形之一:线程初始化的成本很高,线程池使得系统上的线程数容易控制。
2.线程池必须仔细调优。盲目向池中添加新线程,在某些情况下对性能会有不利影响
3.在使用ThreadPoolExecutor时,选择更简单的选项通常会带来最好的,最能预见的性能
ForkJoinPool
ForkJoinPool类是为配合分治算法的使用而设计的:任务可以递归地分解为子集。这些子集可以并行处理,然后每个子集的结果被归并到一个结果中。
一般而言,如果任务是均衡的,使用分段的Thread性能更好,而如果任务是不均衡的,则使用ForkJoinPool性能更好
自动并行化
Java8向Java中引入了自动并行化特定种类代码的能力
设置ForkJoinTask池的大小和设置其他任何线程池同样重要。默认情况下,公共池的线程数等于机器上的CPU数。这个值可以通过设置系统属性-Djava.util.concurrent.ForkJoinPool.common.parallelsm=N来指定
快速小结
1.ForkJoinPool类应该用于递归、分治算法
2.应该花些心思来确定,算法中的递归任务合适结束最为合适。创建太多的任务会降低性能,但如果任务太少,而任务所需的执行时间又长短不一,也会降低性能
3.Java8中使用了自动并行化的特性会用到一个公共的ForkJoinPoll实例。我们可能需要根据实际情况调整这个实例的默认线程大小
线程同步
同步的代价
同步代码堆性能有两方面的影响。其一,应用在同步块上所花的时间会影响该应用的可伸缩性。第二,获取同步锁需要一些CPU周期,所以也会影响性能。
快速小结
1.线程同步有两个性能方面的代价:限制了应用的可伸缩性,以及获取锁是有开销的。
2.同步的内存语义、基于CAS的设置和volatile关键字对性能可能会有很大影响,特别是在有很多寄存器的大型机上。
避免同步
在通常情况下,在比较基于CAS的设施和传统的同步时,可以使用如下的指导原则
1.如果访问的是不存在竞争的资源,那么基于CAS的保护要稍快于传统的同步(虽然完全不使用保护会更快)
2.如果访问的资源存在轻度或适度的竞争,那么基于CAS的保护要快于传统的同步(而且往往是快得多)
3.随着所访问的资源的竞争越来越剧烈,在某一时刻,传统的同步就会成为更高效的选择。在实践中,这只会出现在运行着大量线程的非常大型的机器上
4.当被保护的值有多个读取,但不会写入时,基于CSA的保护不会收到竞争的影响
Java8和存在竞争时的原子类
java.util.concurrent.atomic包中的类使用了基于CAS的原语,而非传统的同步
快速小结
1.避免对同步对象的竞争,是缓解同步对性能影响的有效方式之一
2.线程局部变量不会受竞争之苦;对于保存实际不需要在多个线程间共享的同步对象,它们非常理想。
3.对于确实需要共享的对象,基于CAS的工具也是避免传统的同步方式之一
伪共享
特定的原生分析器(profiler)可能会提供给定代码行的每指令周期数(Cycle Per Instruction,CPI)的相关信息;如果某个循环内的一条简单指令的CPI非常高,可能预示着代码正在等待将目标内存的信息重新加载到CPU缓存中
@Contended注解
Java8有个新特性,即能减少指定字段上的竞争。其实现方式是使用一个新的注解(@sun.misc.Contended)来标记应该由JVM自动填充的变量
默认情况下,除了JDK内部的类,JVM会忽略该注解。要支持应用代码使用该注解,应该使用-XX:-RestrictContended标志,它默认为true(意味着该注解仅限于JDK类使用)。另一方面,要关掉JDK中的自动填充,应该设置-XX:-EnableContended标志,它也默认为true。这将减少Thread和ConcurrentHashMap类的大小。
快速小结
1.对于会频繁地修改volatile变量或退出同步块的代码,伪共享对性能影响很大
2.伪共享很难检测。如果某个循环看上去非常耗时,可以检查该代码,看看是否与伪共享出现时的模式相匹配。
3.最好通过将数据移到局部变量中、稍后再保存来避免伪共享。作为一种替代方案,有时可以使用填充将冲突的变量移到不同的缓存行中。
JVM线程调优
调节线程栈大小
每个线程都有一个原生栈,操作系统用它来保存该线程的调用栈信息
要改变线程的栈大小,可以使用-Xss=N标志
耗尽原生内存
没有足够的原生内存来创建线程,也可能会抛出OutOfMemoryError,这意味着可能出现了以下3种情况之一
1.在32位的JVM上,进程所占空间达到了4GB的最大值(或小于4GB,取决于操作系统)
2.系统实际已经耗尽了虚拟内存
3.在Unix风格的系统上,用户创建的进程数已经达到了配额限制。这方面单独的线程会被看作一个进程。
快速小结
1.在内存比较稀缺的机器上,可以减少线程栈大小。
在32位的JVM上,可以减少线程栈大小,以便在4GB进程空间限制的条件下,稍稍增加堆可以使用的内存。
偏向锁
即锁可以偏向于对它访问最为频繁的线程
偏向锁背后的理论依据是,如果一个线程最近用到了某个锁,那么线程下一次执行由同一把锁保护的代码所需的数据可能仍然保存在处理器的缓存中
使用-XX:-UseBiasedLocking选项禁用偏向锁
自旋锁
在处理同步锁的竞争问题时,JVM有两种选择。对于想要获得锁而陷入阻塞的线程,可以让它进入忙循环,执行一些指令,然后再次检查这个锁。也可以把这个线程放入一个队列,在锁可用时通知它(使得CPU可供其他线程使用)
UseSpinning标志
之前Java版本支持一个-XX:+UseSpinning标志,该标志可以开启或关闭自旋锁。在Java7及更高版本中,这个标志已经没用了
线程优先级
操作系统会为机器上运行的每个线程计算一个“当前”(current)优先级。当前优先级会考虑Java指派的优先级,但是还会考虑很多其他的因素,其中最重要的一个是:自线程上次运行到现在所持续的时间。这可以确保所有的线程都有机会在某个时间点运行。不管优先级高低,没有线程会一直处于“饥饿”状态,等待CPU。
在某种程度上,可以通过将任务指派给不同的线程池并修改那些池的大小来解决。
监控线程与锁
在对应用中的线程和同步的效率作性能分析时,有两点需要注意:总的线程数(既不能太大,也不能太小)和线程花在等待锁或其他资源上的时间
查看线程
在jconsole的Threads面板上可以实时观察程序执行期间线程数的增减
jconsole可以打印每个单独线程的栈信息
查看阻塞线程
要确定线程的CPU周期都耗在哪儿了,则需要使用分析器(profiler)
1.被阻塞线程与JFR
可以深入到JFR捕获的事件中,病寻找那些引发线程阻塞的事件(比如等待获取某个Monitor,或是等待读写Socket,不过写的情况比较少见)
2.被阻塞线程与JStack
jstack、jcmd和其他工具可以提供虚拟机中每个线程状态相关的信息,包括线程是在运行、等待锁还是等待I/O等
在查看线程栈时,有两点需要注意,第一,JVM只能在特定的位置(safepoint,安全点)转储一个线程的栈。第二,每次只能针对一个线程转储出栈信息,所以可能会看到彼此冲突的信息
快速小结
1.利用系统提供的线程基本信息,可以对正在运行的线程的数目有个大致了解
2.就性能分析而言,当线程阻塞在某个资源或I/O上时,能够看到线程的相关细节就显得比较重要
3.JFR使得我们可以很方便地检查引发线程阻塞的事件
4.利用jstack,一定程度上可以检查线程是阻塞在什么资源上
Java EE性能调优
Web容器的基本性能
改善Web容器性能的基本途径
减少输出
减少空格
合并CSS和JavaScript资源
压缩输出
不要使用JSP动态编译
快速小结
1.在Java EE应用所实际运行的网络基础设施上对它们进行测试
2.外部网络相对内部网络来说仍然是慢的。限制应用缩写的数据量会取得很好的性能
HTTP会话状态
1.HTTP会话状态的内存占用
HTTP会话数据通常存活时间很长,所以很容易塞满堆内存,也常常容易导致GC运行太频繁的问题
2.HTTP会话状态的高可用
一旦你更改了会话状态中的对象值,都应该调用setAttribute(),确保你的应用服务器配置成只复制更改的数据
快速小结
1.会话状态会对应用服务器的性能造成重大影响。
2.尽可能少地在会话状态中保留数据,尽可能缩短会话的有效期,以减少会话状态对垃圾收集的影响。
3.仔细查看应用服务器的调优规范,将非活跃的会话数据移出堆
4.开启会话高可用时,需要确保将应用服务器配置成只在状态属性变化时进行会话复制
线程池
应用服务器通常不只有一个线程池。
应用服务器中的线程池可以依据不同的请求量分成若干优先级
EJB会话Bean
调优EJB对象池
因为EJB对象创建(和销毁)的代价很高,所以它们通常保存在对象池中。
只有在应用服务器池中还有可用的EJB对象时,性能才会提高,所以必须将应用服务器中的EJB对象数配置成应用同时使用的EJB数。
快速小结
1.EJB池是对象池的典型范例:初始化代价高,数量相对较少,所以池化更为有效。
2.通常来说,EJB池的大小包括稳定值和最大值。对于特定的环境,两种值都需要调优,但从长期来看,为了降低对垃圾收集器的影响,应该更注重稳定值的调优。
调优EJB缓存
与会话关联的状态Bean并没有保存在EJB池中,而是保存在EJB缓存中。因此,必须对EJB缓存进行调优,以便容纳应用中同时活跃的最大会话量。
快速小结
1.EJB缓存仅用于状态会话Bean与HTTP会话关联的时候
2.应该充分优化EJB缓存,以避免钝化
本地和远程实例
快速小结
即便在同一个服务器中,调用EJB远程接口也对性能有很大的影响
XML和JSON处理
数据大小
和HTML数据一样,程序中的数据也能从减少空格和压缩中获得巨大的益处
解析和编组概述
依据程序的上下文和输出结果,这个过程被称为编组(marshal)或解析。反过来——从数据生成XML或JSON串——则被称为解组(unmarshal)
一般来说,处理这些数据涉及四种技术
标识符解析器(Token parser)
解析器遍查输入数据中的标识符,当发现标识符时则回调相应对象上的方法。
拉模式解析器(Pull parser)
输入的数据与解析器关联,程序从解析器中请求(或拉取)标识符
文档模型(Document model)
输入数据被转换成文档风格的对象,以便程序在查找数据片段时可以遍历。
对象呈现(Object representation)
通过与输入数据对应的预定义类,可以将数据转换成一个或多个Java对象
快速小结
1.Java EE应用中有很多办法处理程序所需要的数据
2.虽然这些技术给开发人员提供了很多功能,但数据处理本身的代价也增加了。不要因此影响你在应用选择正确处理数据的方法
选择解析器
拉模式解析器
从开发中的角度来看,拉模式的解析器最容易使用。在XML的世界中,广为人知的拉模式解析器就是StAX(Streaming API for XML)解析器。JSON-P只提供拉模式解析器
推模式解析器
标准的XML解析器是SAX(Simple API for XML)解析器。SAX解析器是一种推模式解析器:读入数据,当发现token时,就会执行类中处理token的回调方法。
其他解析机制的实现和解析器工厂
XML和JSON规范定义了解析器的标准接口。JDK提供了XML解析器的参考实现,JSON-P项目则提供了JSON解析器的参考实现。
快速小结
1.选择的解析器是否合适,对应用的性能有巨大的影响。
2.推模式的解析器通常比拉模式的快
3.查找解析器工程的算法非常好使;如果可能的话,应该通过系统属性直接指定工厂而不是用现有的实现。
4.在不同的时间点上,最快的解析器实现的赢家可能会不同。适当的时候,应该从备选的解析器查找。
XML验证
解析器可依据一个schema(意为“模式”)对XML数据进行验证,拒绝语法不正确的文档——指缺少某些必要的信息,或者包含了不该有的信息的文档。
XML验证是依据一个或多个schema或DTD文件进行的。虽然DTD的验证更快,但XML schema更灵活,现在是XML世界的主流
快速小结
1.如果业务需要进行schema验证,那就用它,只是要留意,验证对解散数据的性能会带来显著的损耗。
2.总是重用schema,以将验证对性能的影响降至最低
文档模型
快速小结
1.使用DOM和JsonObject比用简单解析器要强大得多,但构造模型所花的时间长度会很显著
2.过滤模型数据比构造默认模型要花费更多的世界,但对于长时间运行或者很大的文档来说,仍然是值得的。
Java对象模型
处理文本数据的最后一种选择是在解析相关的数据之后创建一组Java类实例。
快速小结
1.JAXB将XML文档生成Java对象,以最简单的编程模型访问和使用数据
2.创建JAXB对象的代价比创建DOM对象昂贵
3.JAXB写XML数据的速度要快于DOM对象
对象序列化
Java进程间交换数据,通常就是发送序列化后的对象状态。
transiend字段
将字段标为transient,默认就不会序列化了
覆盖默认的序列化
writeObject()和readObject()可以全面控制数据的序列化
压缩序列化数据
追踪对象复制
快速小结
1.数据的序列化,特别是Java EE中的序列化,有可能是很大的性能瓶颈
2.将变量标记为transient可以加快序列化,并减少传输的数据量。这些做法都可以极大地提高性能,除非接收方重建数据需要花费很长时间。
3.其他writeObject()和readObject()方法的优化也可以显著加快序列化。但请小心,因为这容易出错,而且不留神就会引入bug。
4.通常在序列化时进行压缩都有益处,即使数据不在慢速网络上传输
Java EE网络API
调整传输数据的大小
传输的数据量应该尽量小,无论是压缩或去冗,或其他技术
数据库性能的最佳实践
JDBC
JDBC驱动程序
快速小结
1.花时间评估挑选出最适合你的应用程序的JDBC驱动程序
2.最合适的驱动程序往往依特定的部署环境而有所不同。对同样的应用程序,在一个部署环境中可能要使用JDBC驱动程序,在另一个部署环境中则要采用不同的JDBC驱动程序,才能有更好的性能
3.如果可以选择,尽量避免使用ODBC和JDBC1型的驱动程序
预处理语句和语句池
大多数情况下,代码中若要进行JDBC调用,推荐使用PreparedStatement,尽量避免直接使用Statement。
设置语句池(statement pool)
预处理语句池在JDBC3.0中首次引入,它提供了一个方法(即ConnectionPoolDataSource类的setMaxStatements()方法)用于开启和禁用语句池。如果传递给setMaxStatements()方法的参数是0,语句池就被禁用。
快速小结
1.Java应用程序通常都会重复地运行同样的SQL语句,这些情况下,重用预处理语句池能极大地提升程序的性能。
2.预处理语句必须依单个连接进行池化。大多数的JDBC驱动程序和Java EE框架都默认提供这一功能
3.预处理语句会消耗大量的堆空间。我们需要仔细调优语句池的大小,避免由于对大量大型对象池化而引起GC方面的问题
JDBC连接池
对于连接池而言,首要的原则是应用的每个线程都持有一个连接。对应用服务器而言,则是初始时将线程池和连接池的大小设置为同一值。对单一的应用程序,则是依据应用程序创建的线程数调整连接池的大小。
快速小结
1.数据库连接对象初始化的代价是昂贵的。所以在Java语言中,它们通常都会采用池技术进行管理——要么是通过JDBC驱动程序自身管理,要么在Java EE和JPA框架中进行管理。
2.跟其他的对象池一样,对连接池的调优也是非常重要的,我们需要确保连接池不会对垃圾收集造成负面的影响。为了达到这个目标,调优连接池,避免堆数据库自身的性能产生负面影响也是非常有必要的。
事务
基本的事务隔离模式
TRANSACTION_SERIALIZABLE
这是最昂贵的事务模式;它要求在事务进行期间,事务涉及的所有数据都被锁定。通过主键访问数据以及通过WHERE子句访问数据都属于这种情况;使用WHERE子句时,表被锁定,避免事务进行期间有新的满足WHERE语句的记录被加入。序列化事务每次查询时看到的数据都是一样的
TRANSACTION_REPEATABLE_READ
这种模式下要求事务进行期间,所有访问的数据都被锁定。不过,其他的事务可以随时在表中插入新的行。这种模式下可能会发生“幻读”(phantom read),即事务再次执行带有WHERE子句的查询语句时,第二次可能会得到不同的数据
TRANSACTION_READ_COMMITTED
使用这种模式时,事务运行期间只有正在写入的行会被锁。这种模式肯呢个会发生“不可重复读”(nonrepeatable read),即在事务进行中,一个时间点读到的数据到另一个时间点再次读取时,就变得完全不同了
TRANSACTION_READ_UNCOMMITTED
这是代价最低的事务模式。事务运行期间不会施加任何锁,因此一个事务可以同时读取另一个事务写入(但尚未提交)的数据。这就是著名的“脏读”(dirty read);由于首次的事务可能会回滚(意味着写入操作实际并未发生),因此可能会导致一系列的问题,因为一旦发生这种情况,第二次的事务就是对非法数据进行操作。
如果两个数据源之间极少有机会发生碰撞,则使用乐观锁工作是最好的
快速小结
1.事务会从两个方法影响应用程序的性能:事务提交是非常昂贵的,与事务相关的锁机制会限制数据库的扩展性
2.这两个方面的效果是相互制约的:为了提交事务而等待太长时间会增大事务相关锁的持有时间。尤其是对严格语义的事务来说,平衡的方式是使用更多更频繁的提交来取代长时间地持有锁
3.JDBC中为了细粒度地控制事务,可以默认使用TRANSACTION_READ_UNCOMMITTED隔离级,然后显式地按需锁定数据。
结果集的处理
通过PreparedStatement对象的setFetchSize()方法可以控制这些行为,它能通知JDBC驱动程序一次返回多少行数据
如果next()方法的性能是不是地非常慢(或者结果集的首次查询方法性能很差),你可能就需要考虑增大提取缓冲区的大小
快速小结
1.需要查询处理大量数据的应用程序应该考虑增大数据提取缓冲区的大小
2.我们总是面临着一个取舍,即在应用程序中载入大量的数据(直接导致垃圾收集器面临巨大的压力),还是频繁地进行数据库调用,每次获取数据集的一部分。
JPA
为JPA特别定制的字节码处理方法并不存在。通常情况下,这是作为编译过程的一部分完成的——实体类完成编译后(在它们被载入到JAR文件、或者由JVM开始运行之前),它们被传递给与具体实现相关的后处理器(postprocessor),对字节码进行“增强”,最终生成一个替换类,这个类按照需要进行了优化
事务处理
Java EE中,JPA事务是应用服务器的Java事务API(Java Transaction API,JTA)实现的组成部分。这种设计提供了两种实现事务的选择:可以由应用服务器来处理边界(使用容器管理事务,即Container Managed Transactions,CMT),或者由应用程序通过编程显式地控制事务边界(使用用户管理事务,即User-Managed Transaction,UMT)
XA事务
XA事务是使用了多个数据库资源,或者同时使用了数据库及其他事务资源(比如JMS)的事务
快速小结
1.采用UMT显式地管理事务的边界通常能提升应用程序的性能
2.默认的Java EE编程模型——Servlet或者Web Service通过EJB访问JPA实体——很容易支持这种模式
3.还有另一种替代方案,即可以按照应用程序的事务需要,将JPA划分到不同的方法中处理
对JPA的写性能进行优化
尽量减少写入的字段
优化数据库写操作性能的一个比较通用的方式是只更新那些已经变化的字段。
快速小结
1.JPA应用和JDBC应用一样,受益于对数据库写操作的次数限制(有时还需要权衡是否持有事务锁)
2.语句缓存可以在JPA层面实现,也可以在JDBC层面实现。不过,我们应该首先使用JDBC层面的缓存
3.批量的JPA更新可以通过声明(在presistence.xml文件中)实现,也可以通过编程方式(通过调用flush()方法)实现
对JPA的读性能进行优化
读取更少的数据
查询实体时,被声明为延迟载入的字段会从查询载入数据的SQL语句中益处。
我们很少在基本类型的简单列上使用该声明,不过如果实体包含大型的BLOB或者CLOB对象,就需要考虑是否使用这种声明了。
提取组(Fetch Groups)
如果实体有些字段被定义为延迟载入(lazy load),通常它们会在需要访问时一次一个地被载入
使用提取组,我们可以制定哪些延迟载入的字段可以作为一个群组,一旦这个群组中的一个成员被访问,整个群组都会被载入。
在查询中使用JOIN
JPQL不允许你指定返回对象的哪些字段。
对实体关系(Entity Relationship)而言,无论它们被注解为主动载入还是延迟载入,都可以使用联合查询。如果join应用于延迟载入关系的实体,且注释为延迟载入的实体满足查询条件,这部分实体也汇总数据库中返回,且这部分实体在将来使用时,不需要再次访问数据库。
批处理和查询
JPA的实现几乎都结合语句缓存池使用了带绑定参数的预处理语句。没有任何规定禁止JPA实现使用类似匿名或即时查询应用的逻辑,只不过这种情况实现的难度会更大,而JPA实现可能仅仅是每次创建一个新的语句(即一个Statement对象)
快速小结
1.JPA会进行多种优化,以限制(或增加)一次读取操作所返回的数据量
2.JPA实体中,如果一个大型字段(比如BLOB类型的字段)很少被使用,就应该延迟载入
3.JPA实体之间存在关系时,相关的数据可以主动载入或者延迟载入,具体的选择取决于应用程序的需要
4.采用主动载入关系时,可以使用命名查询生成一个使用JOIN的SQL语句。应注意的是,这会影响JPA缓存,因此并不总是最好的注意
5.使用命名查询读取数据比普通的查询要快很多,因为JPA实现为命名查询构造PreparedStatement更容易
JPA缓存
实体管理器提交事务时,本地缓存中的所有数据会合并到全局缓存中。全局缓存对应用程序的所有实体管理器而言是共享的。全局缓存也被称为二级缓存(L2 Cache)或者是二层缓存(Second-Level Cache);而实体管理器中的缓存被称为一级缓存(L1 Cache)或是一层缓存(First-Level Cache)
一个替代的方案是使用软引用或者弱引用,作为JPA实现的L2缓存
快速小结
1.JPA的L2缓存会自动对应用的实体进行缓存
2.L2缓存不会对查询返回的实体进行缓存。长期来看,这种方式有利于避免查询
3.除非使用的JPA实现支持查询缓存,否则使用JOIN查询的效果通常会对程序的性能造成负面的效果,因为这种操作没有充分利用L2缓存
JPA的只读实体
JPA规范并未直接定义只读实体,但是很多JPA供应商提供了该功能。通常情况下,只读实体比(默认的)读写实体性能更好,因为JPA实现很明确地知道它不需要跟踪实体状态,不必再事务中注册实体,也不必对实体上锁,等待
JPA规范中定义了如何在Java EE容器中支持只读实体的事务:可以在事务之外运行一个通过@TransactionAttributeType.SUPPORTS注释的业务方法(假定该方法调用的同时没有事务在运行)
小结
合理调优访问数据库的JDBC和JPA是影响中间层应用性能最重要的因素之一。请牢记下面的最佳实践
通过合理配置JDBC或者JPA,尽可能地实现批量读取和写入
优化应用使用的SQL语句。对于JDBC应用,这都是一些基本、标准的SQL命令。对于JPA应用,你还需要考虑L2缓存的影响
尽量减少锁的使用。如果数据不太容易发生冲突,推荐使用乐观锁(Optimistic Locking);如果数据经常发生冲突,推荐使用悲观锁(Pessimistic Locking)
请务必使用预处理语句池(Prepared Statement Pool)
请务必合理设置连接池的大小
合理地设置事务的范围:由于锁在整个事务期间都需要保持,所以在不影响应用程序扩展性的前提下,尽可能把事务的范围设置得大一些
Java SE API技巧
缓冲式I/O
对于使用二进制数据的文件I/O,记得使用一个BufferedInputStream或BufferedOutputStream来包装底层的文件流,对于使用字符(字符串)数据的文件I/O,记得使用一个BufferedReader或BufferedWriter来包装底层的流
InputStream.read()和OutputStream.write()方法操作的是一个字符。由于所访问的资源不同,这些方法有可能非常慢。而在fileInputStream上调用read()方法,更是慢点难以形容:每次调用该方法,都要进入内核,去取一个字节的数据。在大多数操作系统上,内核都会缓冲I/O,因此,很幸运,该场景不会在每次调用read()方法时触发一次磁盘读取操作。但是这种缓存保存在内核中,而非应用中,这就意味着每次读取一个字节时,每个方法调用还是会涉及一次代价高昂的系统调用。
写数据也是如此:使用write()方法向fileOutputStream发送一个字节,也需要一次系统调用,将该字节存储到内核缓冲区中。最后(当问及关闭或者刷新时),内核会把缓冲区中的内容写入磁盘。
当在字节和字符之间转换时,操作尽可能大的一段数据,性能最佳。如果提供给编解码器的是单个的字节或字符,性能会很差
快速小结
1,围绕缓冲式I/O有一些很常见的问题,这是由简单输入输出流类的默认实现引发的。
2.文件和Socket的I/O必须正确地缓冲,对于像压缩和字符串编码等内部操作,也是如此
类加载
自定义的类加载器默认是不支持并行的。如果希望自己的类加载器也能并行使用,必须采取一些措施。措施总共分为两步
首先,确保类加载器的层次结构中没有任何回环
第二,在定义类加载器类时,在静态初始化部分将其注册为可以并行的
在编写类加载器时,建议重写findClass()方法。如果自定义的类加载器重写的是loadClass方法,而非findClass()方法,则一定要确保在每个类加载器实例内,对于每个类名,defineClass()方法只调用一次
快速小结
1.在存在多个类加载的复杂应用(特别是应用服务器)中,让这些类加载器支持并行,可以解决系统类加载器或者启动类加载器上的瓶颈问题
2.如果应用是在单线程内,则通过一个类加载器加载很多类,关掉Java7支持并行的特性可能会有好处。(可以使用-XX:+AlwaysLockClassLoader标志,默认为false)
随机数
Java7提供了3个标准的随机数生成器类:java.util.Random、java.util.concurrent.ThreadLocalRandom以及java.scurity.SecureRandom。
快速小结
1.Java默认的Random类的初始化的成本很高,但是一旦初始化完毕,就可以重用
2.在多线程代码中,应该首选ThreadLocalRandom类
3.SecureRandom类表现出的性能也是随意的和完全随机的。在对用到这个类的代码做性能测试时,一定要认真规划
Java原生接口
如果想编写尽可能快的代码,要避免使用JNI
尽可能避免从Java调用C。跨JNI边界(边界是描述跨语言调用的术语)成本非常高,这是因为,调用一个现有的C库首先需要一些胶水代码,需要花时间通过胶水代码创建新的、粗粒度的接口,一下子要多次进入C库。
当有数组被固定在内存中时,垃圾收集器就无法运行——所以JNI代码中代价最高的错误之一就是在长期运行的代码中固定了一个字符串或数组
快速小结
1.JNI并不能解决性能问题。Java代码几乎总是比调用原生代码跑的快
2.当使用JNI时,应该限制从Java到C的调用次数;跨JNI边界的调用成本很高。
3.使用数组或字符串的JNI代码必须固定这些对象;为避免影响垃圾收集器,应该限制固定对象的时间
异常
基本上,代码仅应该通过抛出异常来说明发生了意料之外的情况。遵循良好的代码设计原则,意味着Java代码不会因异常处理而变慢
异常会涉及获取该异常发生时的栈轨迹信息。这一操作代价可能会很高,特别是在栈轨迹很深时
可以使用-XX:-StackTraceInThrowable标志(默认为true)来禁止生成栈轨迹信息
快速小结
1.处理异常的代价未必会很高,不过还是应该在适合的时候才用
2.栈越深,处理异常的代价越高
3.对于会频繁创建的系统异常,JVM会将栈上的性能损失优化掉
4.关闭异常中的栈轨迹信息,有时可以提高性能,不过这个过程往往会丢失一些关键信息
字符串的性能
字符串保留
没有必要在堆中为所有这些对象都分配空间;因为字符串是不可变的,所以重用现有的字符串往往更好
字符串编码
Java的字符串采用的是UTF-16编码,而其他地方多是使用其他编码,所以将字符串编码到不同字符集的操作很常见。对于Charset类的encode()和decode()方法而言,如果一次只处理一个或几个字符,它们会非常慢;无比完整缓存一些数据,再进行处理
网络编码
在编码静态字符串(来自JSP文件等地方)时,Java EE应用服务器往往会特殊处理
永远不要使用连接来构造字符串,除非能在逻辑意义上的一行代码内完成;也不要在循环内使用字符串连接,除非连接后的字符串不会用于下一次循环迭代。
快速小结
1.一行的字符串连接代码性能很不错
2.对于多行的字符串连接操作,一定要确保使用StringBuilder
日志
对于应用日志,需要记住3个基本原则
1.协调好要打日志的数据和所选级别(Level)之间的关系。
2.使用细粒度的Logger实例
3.在向代码引入日志时,应该注意,很容易编写出来意想不到的副作用的日志代码,即使这个日志并没有开启。
快速小结
1.为了帮助用户找出问题,代码应该包含大量日志,但是这些日志默认都应该是关闭的
2.如果Logger实例的参数需要调用方法或者分配对象,那么在调用该实例之前,不要忘了测试日志级别
Java集合类API
同步还是非同步
默认情况下,几乎所有的Java集合类都是非同步的(主要的例外是Hashtable、Vector及与其相关的类)
设定集合的大小
集合与内存使用效率
快速小结
1.仔细考虑如何访问集合,并为其选择恰当的同步类型。不过,在不存在竞争的条件下访问使用了内存保护的集合(特别是使用了基于CAS的保护的集合),性能损失会极小,有时候,保证安全性才是上策
2.设定集合的大小对性能影响很大:集合太大,会使得垃圾收集器变慢,集合太小,又会导致大量的大小调整与复制
AggressiveOpts标志
AggressiveOpts标志(默认为false)会影响一些基本Java操作的行为。其目标是实验性地引入一些优化;随着时间的推移,原来由这个标志启用的优化有望成为JVM的默认设置
替代实现
启用AggressiveOpts标志的主要影响是,它会为JDK重的一些基本的类引入不同的替代实现:尤其是java.math包中的BigDecimal、BigInteger和MutableBigDecimal类;java.text中的DecimalFormat、DigitalList和NumberFormat类,java.util包中的HashMap、LinkedHashMap和TreeMap类
其他标志
设置AggressiveOpts标志会开启Autofill标志(它在JDK7到7u4这几个版本中默认为false)。这个标志开启后,编译器会对循环进行更好的优化。类似地,AggressiveOpts标志还会开启DoEscapeAnalysis标志
快速小结
1.AggressiveOpts标志会在一些基本的类中开启某些优化。大多数情况下,这些类要快于它们所替代的类,不过可能有意想不到的副作用
Java8中去掉了这些类
Lambda表达式和匿名类
在JDK8中,Lambda表达式的代码会创建一个静态方法,这个方法通过一个特殊的辅助类来调用。而匿名类是一个真正的Java类,有单独的class文件,并通过类加载器加载
快速小结
1.如果要在Lamabda表达式和匿名类之间做出选择,则应该从方便编程的角度出发,因为性能上没什么差别
2.Lambda表达式并没有实现为类,所以有个例外情况,即当类加载行为对性能影响很大时,Lambda表达式略胜一筹
流和过滤器的性能
延迟遍历(Lazy Traversal)
Stream的第一个性能优势是它们被实现为了延迟的数据结构。
快速小结
1.过滤器因为支持在迭代过程中结束处理,所以有很大的性能优势
2.即使都要处理整个数据集,一个过滤器还是要比一个迭代器稍微快些
3.多个过滤器有些开销,所以要确保编写好用的过滤器
小结