xmind格式学习笔记下载链接:https://github.com/bsr1983/note/blob/master/Java%E6%80%A7%E8%83%BD%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97.xmind
Java性能权威指南
导论
JVM调优标志
布尔标志
-XX:+FlagName 表示开启 -XX:-FlagName 表示关闭
附带参数的标志
-XX:FlagName=something,表示将标志flagName的值设置为something
Client和Server类虚拟机
Java的自动优化前提是机器被分为"Client"和"Server"。
Microsoft Windows上运行的任何32位JVM(无论机器上CPU的个数是多少),以及单CPU机器(不论是什么操作系统)上运行的任何32位JVM,都是client类机器。所有其他机器(包括所有64位JVM)都被认为是Server类。
常见的优化
借助性能分析来优化代码,重点关注性能分析中最耗时的操作
利用奥卡姆剃刀原则诊断性能问题
为应用中最常用的操作编写简单算法。
性能测试方法
原则1:测试真实应用
微基准测试
1.必须使用被测的结果
2.不要包括无关的操作
3.必须输入合理的参数
宏基准测试
介基准测试
快速小结
1.好的微基准测试既难写,价值又有限。如果你必须使用它,那可以用它来快速了解性能,但不要依赖它们。
测试完整应用是了解它实际运行的唯一途径。
在模块或者操作基本隔离性能——介基准测试——相对于全应用测试来说,是一种合理的折中途径,而不是替代方法。
原则2:理解批处理流逝时间、吞吐量和响应时间
批处理流逝时间
吞吐量测试
响应时间测试
负载生成器
Faban(http://faban.org/)
快速小结
1.Java性能测试中很少使用面向批处理的测试(或者任何没有热身期的测试),但这种测试可以产生何忧价值的结果。
2.其他可以测量吞吐量或响应时间的测试,则依赖负载是否以固定的速率加载(也就是说,给予模拟的客户端思考时间)。
原则3:用统计方法应对性能的变化
1.正确判定测试结果间的差异需要统计分析,通过统计分析才能确定这些差异是不是归因于随机因素
2.可以用严谨的t检验来比较测试结果,实现上述目的
3.t检验可以告知我们变动存在的概率,却无法高数我们哪种变动该忽略,而哪种该追查。如何在两者之间找到平衡,是性能调优工程的艺术魅力所在。
原则4:尽早频繁测试
自动化一切
测试一切
在真实系统上运行
快速小结
1.虽然频繁的性能测试很重要,但并非毫无代价,在日常开发周期中需要仔细斟酌。
2.自动化测试系统可以收集所有机器和程序的全部统计数据,这可以为查找性能衰减问题提供必不可少的线索。
Java性能调优工具箱
操作系统的工具和分析
CPU使用率
CPU运行队列
快速小结
1.检查应用性能时,首先应该审查CPU时间
2.优化代码的目的是提升而不是降低(更短时间内的)CPU使用率
3.在试图深入优化应用前,应该先弄清楚为何CPU使用率低。
磁盘使用率
快速小结
1.对于所有应用来说,监控磁盘使用率非常重要。即便不直接写磁盘的应用,系统交换仍然会影响他们的性能。
2.写入磁盘的应用遇到瓶颈,是因为写入数据的效率不高(吞吐量太低),或者是因为写入太多数据(吞吐量太高)。
网络使用率
快速小结
1.对于网络的应用来说,务必要监控网络以确保它不是瓶颈。
2.往网络写数据的应用遇到瓶颈,可能是因为写数据的效率太低(吞吐量太低),也可能是因为写入了太多的数据(吞吐量太高)
Java监控工具
JDK自带工具
jcmd
它用来打印Java进程所涉及的基本类、线程和VM信息。
% jcmd process_id command optinal_arguments
jcmd help 可以列出所有的命令。jcmd help 可以给出特定命令的语法。
jconsole
提供JVM活动的图形化视图,包括线程的使用、类的使用和GC活动。
jhat
读取内存堆转储,并有助于分析。这是事后使用的工具。
jmap
提供堆转储和其他JVM内存使用的信息。可以适用于脚本,但堆转储必须在事后分析工具中使用,
jinfo
查看JVM的系统属性,可以动态设置一些系统属性。可适用于脚本。
jstack
转储Java进程的栈信息。可适用于脚本。
jvisualvm
监视JVM的GUI工具,可用来剖析运行的应用,分析JVM堆转储(事后活动,虽然jvisualvm也可以试试抓取程序的堆转储)
基本的VM信息
运行时长
% jcmd process_id VM.uptime
系统属性
%jcmd _id VM.system_properties
%jinfo -sysprop process_id
这包括通过命令行-D标志设置的所有属性,应用动态添加的所有属性和JVM默认属性
JVM版本
%jcmd process_id VM.version
JVM命令行
jconsole的“VM摘要”页可以显示程序所用的命令行,或者用jcmd显示
%jcmd process_id VM.command_line
JVM调优标志
可以用一下方式获得对应生效的JVM调优标志
%jcmd process_id VM.flag [-all]
调优标志
想知道特定平台所设置的标志是什么,可以执行以下命令: %java other_options -XX:+PrintFlagsFinal -version name:=value 表示标志使用的是非默认值。可能原因: (1)标志值直接在命令行指定 (2)其他标志间接改变了该标志的值。 (3)JVM自动优化计算出来的默认值。 最后一列的含义: (1)product 表示在所有平台上的默认设置都是一致的 (2)pd product表示标志的默认值是独立于平台的 (3)manageable 运行时可以动态更改标志的值 (4)C2 diagnostic 为编译器工程师提供诊断输出,帮助理解编译器正以什么方式运作
获取进程中所有标志的值
%jinfo -flags process_id
jinfo带有-flags时可以提供所有标志的信息,否则只打印命令行所指定的标志。
jinfo检查单个标志的值
%jinfo -flag PrintGCDetails process_id
jinfo修改manageable的标志的值
%jinfo -flag -PrintGCDetails process_id #turn off PrintGCDetails
快速小结
1.jcmd可用来查找运行中的应用所在JVM的基本信息——包括所有调优标志的值
2.命令行上添加-XX:+Printflagsfinal 可输出标志的默认值
3.jinfo在检查(某些情况下可以更改)单个标志时很有用
线程信息
jconsole和jvisualvm可以实时显示应用中运行的线程的数量
%jstack process_id 显示了每个线程的栈的众多输出
%jcmd process_id Thread.print 显示了每个线程栈的众多输出
类信息
jconsole或jstat可以提供应用已使用类的个数。jstat还能提供类编译相关的信息
实时GC分析
jconsole可以用实时图显示堆得使用情况
jcmd可以执行GC操作
jmap可以打印堆得概况、永久代信息或者创建堆转储。
jstat可以为垃圾收集器正在执行的操作生成许多视图。
事后堆转储
jvisualvm的GUI界面可以捕获堆转储,也可以用命令行jcmd或jmap生成。
堆转储是堆使用情况的快照,可以用不同的工具进行分析,包括jvisualvm和jhat。
性能分析工具
采样分析器
性能分析的两种模式
数据采样
数据探查
快速小结
1.采样分析器是最常用的分析器
2.因为采样分析器的采样频率相对较低,所以引入的测量失真也较小。
3.不同的采样分析器各有千秋,针对不同应用各有所长。
探查分析器
探查分析器相比于采样分析器,侵入性更强,但它们可以给出关于程序内部所发生的更有价值的信息
探查分析器会在类加载时更改类的字节码(即插入统计调用次数的代码)。相比采样分析器,探查分析器更可能会将性能偏差引入应用。
快速小结
1.探查分析器可以给出更多的应用信息,但相对采样分析器,它对应用的影响更大。
2.探查分析器应该仅在小代码区域——一些类和包——中设置使用,以限制对应用性能的影响。
阻塞方法和线程时间线
快速小结
1.线程被阻塞可能是性能问题,也可能不是,有必要进一步调查它们为何被阻塞。
2.通过被阻塞的方法调用,或者分析线程的时间线,可以辨认出被阻塞的线程
本地分析器
本地分析器是指分析JVM自身的工具。这类工具可以看到JVM内部的工作原理,如果应用自身含有本地库,这类工具也能看到本地库代码的内部。
快速小结
1.本地性能分析器可以提供JVM和应用代码内部的信息。
2.如果本地性能分析器显示GC占用了主要的CPU时间,优化垃圾收集器就是正确的做法。然而,如果显示编译线程占用了明显的时间,则说明通常对应用性能没什么影响。
Java任务控制
Java Mission Control
JMC的程序(jmc)开启了一个窗口以显示当前机器上的JVM进程,你可以选择一个或多个进行监控,
Java飞行记录器
JMC的关键特性是Java飞行记录器(Java Flight Recorder,JFR)。JFR数据是JVM的历史事件,这些可以用来诊断JVM的历史性能和操作。 JFR的基本操作是开启一组事件,每当选择的事件发生时,就会保存相应的数据(保存在内存或者文件中)。数据流保存在循环缓冲中,所以只有最近的事件。
开启JFR
在Oracle JVM的商业版本中,JFR初始为关闭。为了开启它,可以在应用的启动命令行上添加标志-XX:+UnlockCommercialFeatures -XX:+flightRecorder。这会开启JFR特性,但直到记录过程自身开始时才会记录信息
1.通过JMC开启JFR
2.通过命令行开启JFR
控制记录应该在何时以及如何发生
JVM用-XX:+flightRecorderOptions=string 参数方式启东市,可以控制这些记录参数。 参数中的string是一列逗号分隔的名字-值对。
name=name 用以标识记录的名字
defaultrecording=表示初始时是否开启记录。默认为false。对于响应性分析,应该设为true
setting=path JFR设置文件的文件名
delay=time 记录开始前延迟的时间量
duration=time 记录持续的时间
filename=path 记录文件名
compress= 记录是否开启压缩(gzip)。默认为false
maxage=time 循环缓冲中保留记录的最长时间
maxsize=size 记录循环缓冲的最大尺寸
所有选项可在程序运行时(假设-XX:+flightRecorder已先指定),用jcmd来控制
开启飞行记录 %jcmd process_id JFR.start [options_list] option_list是一组用逗号分隔的名字-值对,控制记录如何进行。选项和使用命令行时的标志完全一致。
如果开始持续记录,可以在任何时候,通过以下命令将当前循环缓冲里的数据转储到文件中: %jcmd process_id JFR.dump [options_list]
name=name 在这个名字下的记录已经开始
recording=n JFR记录的编号
filename=path 转储文件的位置
对于给定的进程,可能开启了多个JFR记录,以下命令可以查看开启的记录: %jcmd process_id JFR.check [verbose]
进程放弃记录的命令: %jcmd process_id JFR.stop [options_list]
name=name 停止记录的名字
recording=n 停止记录的编号(可由JFR.check获得)
discard=boolean 如果为true,则丢弃数据而不是写到前面所提供的文件中(如果有的话)
filename=path 数据写到给定的路径上
选择JFR事件
当前的JFR支持77种事件,大多数是周期性事件:它们的周期以毫秒记, 其他事件仅当事件的持续时间超出阈值是才会触发。
JFR捕获的事件(包括事件和阈值)都定义在模板中(可以通过命令行的设置选项选择)。JFR自带了两个模块
默认模板:限制了事件使得开销效率1%
性能分析模板:大多数基于阈值的事件被设置为每10毫秒触发
快速小结
1.由于JFR内建于JVM,所以可以最大可能性地查看JVM内部
2.像其他工具一样,JFR给应用引入了一些开销。对于日常使用,可以开启JFR,以较低的开销收集大量的信息
3.JFR用于性能分析,但它在生产系统中也很有用,所以你可以检查那些导致失败的事件。
JIT编译器
概览
因为Java程序运行的是理想化的二进制代码,所以它能在代码执行时将其编译成平台特定的二进制代码。 由于这个编译是在程序执行时进行的,因此被称为“即时编译”(JIT)
快速小结
1.Java的设计结合了脚本语言的平台独立性和编译型语言的本地性能
2.Java文件被编译成中间语言(Java字节码),然后在运行时被JVM进一步编译成汇编语言
3.字节码编译成汇编语言的过程中有大量的优化极大地改善了性能
调优入门
编译器标志
-client -server -d64
分层编译开启形式-XX:+TieredCompilation
client编译器(C1)开启编译比server(C2)编译器要早
优化启动
快速启动常用client编译器
快速小结
分层编译的启动时间可以非常接近于client编译器的启动时间
优化批处理
快速小结
1.对计算量固定的任务来说,应该选择实际执行任务最快的编译器
2.分层编译是批处理任务合理的默认选择
优化长时间运行的应用
快速小结
1.对于长时间运行的应用,应该一直使用server编译器,最好配合分层编译
Java和JIT编译器版本
JIT编译器3种版本
32位client编译器(-client)
32位server编译器(-server)
64位server编译器(-d64)
快速小结
1.不同的Java支持不同的编译器
2.不同的操作系统和架构支持的编译器也不同
3.程序不必指定编译器,而是仰仗平台所支持的编译器
编译器中级调优
调优代码缓存
-XX:ReservedCodeCacheSize=N (对特定编译器来说,N为默认的值)标志可以设置代码缓存的最大值。代码缓存 的管理和大多数JVM内存一样,有初始值(由-XX:IniticalCodeCacheSize=N指定)。
代码缓存设为1GB,JVM就会保留1GB的本地内存空间。虽然这部分内存在需要时才会分配,但它仍然是被保留的,这意味着为了满足保留内存,你的机器必须有足够的虚拟内存。
通过jconsoleMemory(内存)面板的Memory Pool Code Cache图表,可以监控代码缓存
快速小结
1.代码缓存是一种有最大值的资源,它会影响JVM可运行的编译代码总量
2.分层编译很容易达到代码缓存默认配置的上限(特别是在Java7中)。使用分层编译时,应该监控代码缓存,必要时应该增加它的大小。
编译阈值
编译时基于两种JVM计数器的:方法调用计数器和方法中的循环回边计数器。回边实际上可看作是循环完成执行的次数,所谓循环完成执行,包括达到循环自身的末尾,也包括执行了像continue这样的分支语句。
标准编译
JVM执行某个Java方法时,会检查该方法的两种计数器总数,然后判定该方法是否适合编译。如果适合,该方法就进入编译队列。
由-XX:CompileThreshold=N标志触发。使用client编译器时,N的默认值是1500,使用server编译器时为10000.这个标志的阈值等于回边计数器加上方法调用计数器的总和
栈上替换(On-Stack Replacement,OSR)
如果循环真的很长,循环每完成一轮,回边计数器就会增加并被检测。如果循环的回边计数器超过阈值,那么这个循环(不是整个方法)就可以被编译。
OSR编译由3个标志触发: OSR trigger=(CompileThreshold*((OnstackReplacePercentage-InterpreterProfilePercentage)/100)) 所有编译器中的-XX:InterpreterProfilePercentage=N标志的默认值为33。client编译器-XX:OnStackReplacePercentage=N的默认值为933,所以在它开始OSR编译前,回边计数器需要达到13500。在server编译器中,由于OnStackReplacePercentage默认值为140,所以当回边计数器达到10700时才开始OSR编译。
每种计数器的值都会周期性减少(特别是当JVM达到安全点时)。实际上,计数器只是方法或循环最新热度的度量。
快速小结
1.当方法和循环执行次数达到某个阈值的时候,就会发生编译。
2.改变阈值会导致代码提早或推后编译
3.由于计数器会随着时间而减少,以至于“温热”的方法可能永远都打不到编译的阈值(特别是对server编译器来说)
检测编译过程
-XX:+PrintCompilation(默认为false)
如果开启PrintCompilation,每次编译一个方法(或循环)时,JVM就会打印一行被编译的内容信息。
用jstat检测编译
编译日志需要在程序启动时开启-XX:+PrintCompilation。如果程序启动时没有开启这个标志,可以用jstat了解编译器内部的部分工作情况
jstat有两个有关编译器信息的标志。
-compiler标志提供了关于多少方法被编译的概要信息 % jstat -compiler pid(线程id)
可以用-printcompilation标志获取最新被编译的方法。jstat借助一个可选参数反复执行操作,你可以看到随时间变化有哪些方法被编译了。 %jstat -printcompilation 5003 1000 每1000毫秒输出一次进程ID为5003的信息
快速小结
1.观察代码如何被编译的最好方法是开启PrintCompilation
2.PrintCompilation开启后所输出的信息可以用来确认编译是否和预期一样
高级编译器调优
编译线程
编译队列并不严格遵守先进先出的原则:调用次数多的方法有更高的优先级。
当使用client编译器时,JVM会开启一个编译线程;使用server编译器时,则会开启两个这样的线程。当启用分层编译时,JVM默认开始多个client和server线程,线程数依据一个略复杂的等式而定,包括目标平台CPU取双对数之后的数值。
编译器的线程数(3种编译器都是如此)可通过-XX:CICompilerCount=N标志来设置。这是JVM处理队列的线程总数;对分层编译来说,其中三分之一(至少一个)将用来处理client编译器队列,其余的线程(至少一个)用来处理server编译器队列。
-XX:+BackgroundCompilation标志,默认值为true。标志着编译队列的处理是异步执行的。如果设置为false,当一个方法适合编译,执行该方法的代码将一直等到它确实被编译之后才执行(而不是继续在解释器中执行)。用-Xbatch可以禁止后台编译
快速小结
1.放置在编译队列中的方法的编译会被异步执行。
2.队列病不是严格按照先后顺序的,队列中的热点方法会在其他方法之前编译。这是编译输出日志的ID为乱序的另一个原因
内联
编译器所做的最重要的优化是方法内联。
内联默认是开启的。可通过-XX:-Inline关闭。
如果从源代码编译JVM,那可以用-XX:+PrintInlining生成带调试信息的版本。这个参数会提供所有关于编译器如何进行内联决策的信息。
方法是否内联取决于它有多热以及它的大小。JVM依据内部计算来判定方法是否是热点方法(譬如,调用很频繁);是否是热点并不直接与任何调优参数相关。如果方法因调用频繁而可以内联,那只有在它的字节码小于325字节时(或-XX:MaxFreqInlineSize=N所设定的任意值)才会内联。否则,只有方法很小时,即小于35字节(或-XX:MaxInlineSize=N所设定的任意值)时才会内联。
快速小结
1.内联是编译器所能做的最有利的优化,特别是堆属性封装良好的面向对象的代码来说。
2.几乎用不着调节内联参数,且提倡这样做的建议往往忽略了常规内联和频繁调用内联直接的关系。当考察内联效应时,确保考虑这两种情况。
逃逸分析
如果开启逃逸分析(-XX:+DoEscapeAnalysis,默认为true),server编译器将会执行一些非常激进的优化措施。
快速小结
1.逃逸分析是编译器所能做的最复杂的优化,此类优化常常会导致微基准测试失败
2.逃逸分析常常会给不正确的同步代码引入"bug"
逆优化
逆优化意味着编译器不得不“撤销”之前的某些编译;结果是应用的性能降低——至少是直到编译器重新编译相应代码为止
有两种逆优化的情形:代码状态分别为"made not entrant"(代码被丢弃)和"made zombie"(产生僵尸代码)时
代码被丢弃
有两种原因导致代码被丢弃。可能是和类与接口的工作方式有关,也可能与分层编译的实现细节有关。
第二种导致代码被丢弃的原因是分层编译。在分层编译中,代码先由client编译器编译,然后由server编译器编译。当server编译器编译好代码后,JVM必须替换client编译器所编译的代码。它会将老代码标记为废弃,也用同样的办法替换新编译(也更有效)的代码。因此,当程序使用分层编译时,编译日志就会显示许多倍丢弃的方法。
逆优化僵尸代码
1.逆优化使得编译器可以回到之前版本的编译代码
2.先前的优化不再有效时,才会发生代码逆优化
3.代码逆优化时,会对性能产生一些小而短暂的影响,不过新编译的代码会尽快地再次热身
4.分层编译时,如果代码之前由client编译器编译而现在由server编译器优化,就会发生逆优化。
分层编译级别
程序使用分层编译时,编译日志中会输出代码所编译的分层级别。
编译级别
0:解释代码
1:简单C1编译代码
2:受限的C1编译代码
3:完全C1编译代码
4:C2编译代码
典型的编译日志可以显示,多数方法第一次编译的级别是3,即完全C1编译。(当然,所有方法都从级别0开始)如果方法运行得足够频繁,它就会编译成级别4(级别3的代码就会被丢弃)。最常见的情况是:client编译器从获取了代码如何使用的信息进行优化时才开始编译。
快速小结
1.分层编译可以在2种编译器和5种编译级别之间进行
2.不建议人为更改级别
小结
从调优角度看,简单的选择就是对所有应用都使用server编译器和分层编译,这将解决90%的与编译器相关的性能问题
(1)不用担心小方法——特别是getter和setter,因为它们很容易内联。
(2)需要编译的代码在编译队列中。队列中代码越多,程序达到最佳性能的时间越久。
(3)虽然代码缓存的大小可以(也应该)调整,但它仍然是有限的资源
(4)代码越简单,优化越多。分析反馈和逃逸分析可以使代码更快,但复杂的循环结构和大方法限制了它的有效性。
垃圾收集器入门
最主流的四个垃圾收集器
Serial收集器(常用于单CPU环境)
Throughput(或Parallel)收集器
Concurrent收集器(CMS)
G1收集器
垃圾收集器概述
所有应用线程都停止运行所产生的停顿被称为时空停顿(stop-the-world)。
垃圾收集由两步构成:查找不再使用的对象,以及释放这些对象所管理的内存。
分代垃圾收集器
根据情况将堆划分成不同的代(Generation)。这些代被称为“老年代”(Old Generation或Tenured Generation)和新生代(Young Generation)。新生代又进一步划分为不同的区段,分别称为Eden空间和Survivor空间
新生代是堆得一部分,对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间。不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方。这种操作被称为Minor GC.
所有的垃圾收集算法在对新生代进行垃圾回收时都存在"时空停顿”现象。
简单的垃圾收集算法直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理。这个过程被称为Full GC,通常导致应用程序线程长时间的停顿。
为批量应用选择垃圾收集器
1.如果CPU足够强劲,使用Concurrent收集器避免发生Full GC停顿可以让任务运行得更快
2.如果CPU有限,那么Concurrent收集器额外的CPU消耗会让批处理任务消耗更多的时间
快速小结
1.所有的GC算法都将堆划分成了老年代和新生代
2.所有的GC算法在清理新生代对象时,都使用了"时空停顿"方式的垃圾收集方法,通常这是一个能较快完成的操作。
GC算法
JVM提供了以下4种
1.Serial垃圾收集器
Serial收集器使用单线程清理堆得内容。
无论Minor GC还是Full GC,清理堆空间时,所有的应用线程都会被暂停。
启用:-XX:+UseSerialGC
在Serial收集器作为默认收集器的系统上,如果需要关闭Serial收集器,可以通过制定另一种收集器来实现。
2.Throughput垃圾收集器
Server级虚拟机的默认收集器
使用多线程回收新生代和老年代空间
常被称为Parallel收集器
在Minor GC和Full GC时会暂停所有的应用线程
使用-XX:+UseParallelGC、-XX:+UseParallelOldGC启用
CMS收集器
设计的初衷是为了消除Throughput收集器和Serial收集器Full GC周期中的长时间停顿。
使用新算法收集新生代对象(-XX:+UseParNewGC)
Full GC时不再暂停应用线程,而是使用若干个后台线程定期地堆老年代空间进行扫描,即时回收其中不再使用的对象
应用线程只在Minor GC以及后台线程扫描老年代时发生及其短暂的停顿。
通过-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC标志(默认情况下这两个标志都是禁用的)可以启用CMS垃圾收集器
G1垃圾收集器
设计初衷是尽量缩短处理超大堆(大于4GB)时产生的停顿。
G1收集算法将堆划分为若干个区域(Region),不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然采用暂停所有应用线程的方式,将存活对象移动到老年代或者Survivor空间,这些操作都使用多线程的方式完成。
通过标志-XX:+UseG1GC(默认值是关闭的)可以启动G1垃圾收集器。
强制进行GC
执行 jcmd <进程号> GC.run
jconsole连接到JVM在内存面板上单击“进行GC”按钮
可以通过参数-XX:+DisableExplicitGC显式地禁止System.gc()这种类型的GC;默认情况下该标志是关闭的
快速小结
1.四种垃圾收集算法采用了不同方法缓解GC对应用程序的影响
2.Serial收集器常用于仅有单CPU可用以及当前其他程序会干扰GC的情况(通常是默认值)
3.Throughput收集器在其他的虚拟机上市默认值,它能最大化应用程序的总吞吐量,但是有些操作可能遭遇较长的停顿。
4.CMS收集器能够在应用线程运行的同时并行地对老年代的垃圾进行收集。如果CPU的计算能力足以支撑后台垃圾收集线程的运行,该算法能避免应用程序发生Full GC。
5.G1收集器也能在应用线程运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生Ful GC的风险。G1的设计理念使得它比CMS更不容易遭遇Full GC。
选择GC算法
一方面取决于应用程序的特征,另一方面取决于应用的性能目标。
1.GC算法及批量任务
监控系统中定义的由CPU使用率触发的规则尤其重要:你需要确保100%的CPU使用率不是由Full GC所引起的临时性CPU暴涨,或者是由于后台并行处理线程所引起的持续时间更长(不过使用率稍低)的CPU高峰。在Java程序的世界里,这些峰值都是正常的状况。
快速小结
1.使用Throughput收集器处理应用程序线程的批量任务能最大程度地利用CPU的处理能力,通常能获得更好的性能。
2.如果批量任务并没有使用机器上所有可用的CPU资源,那么切换到Concurrent收集器往往能取得更好的性能。
2.GC算法和吞吐量测试
存在空闲CPU周期时,CMS收集器的性能更好。
可用的CPU周期无法支撑后台的CMS收集线程运行,所以CMS收集器发生了并发模式失效(Concurrent Mode Failure)。发生这种失效意味着JVM不得不退化到单线程的Full GC模式,所以那段时间内平均CPU的使用率骤降。
3.GC算法及响应时间测试
快速小结
1.衡量标准是响应时间或吞吐量,在Throughput收集器和Concurrent收集器之间做选择的依据主要是有多少空闲CPU资源能用于运行后台的并发线程。
2.通常情况下,Throughput收集器的平均响应时间比Concurrent收集器要差,但是在90%响应时间或99%响应时间这几项指标上,Throughput收集器比Concurrent收集器要好一些。
3.使用Throughput收集器或超负荷地进行大量Full GC时,切换到Concurrent收集器通常能获得更低的响应时间。
CMS收集器和G1收集器之间的抉择
一般情况下,堆空间小于4GB时,CMS收集器的性能比G1收集器好。
使用大型堆或巨型堆时,由于G1收集器可以分割工作,通常它比CMS收集器表现更好。
快速小结
1.选择Concurrent收集器时,如果堆较小,推荐使用CMS收集器
G1的设计使得它能够在不同的分区(Region)处理堆,因此它的扩展性更好,比CMS更易于处理超大堆的情况。
GC调优基础
1.调整堆的大小
调整堆大小的首要原则就是永远不要将堆的容量设置得比机器的物理内存还大。如果机器上运行着多个JVM实例,则这个原则适用于所有堆的总和
堆的大小由2个参数值控制:分别是初始值(通过-Xms N设置)和最大值(通过-Xmx N设置)
快速小结
1.JVM会根据其运行的机器,尝试估算合适的最大、最小堆的大小
2.除非应用程序需要比默认值更大的堆,否则在进行调优时,尽量考虑通过调整GC算法的性能目标,而非微调堆的大小来改善程序性能。
2.代空间的调整
所有用于调整代空间的命令行标志调整的都是新生代空间;新生代空间剩下的所有空间都被老年代占用。
多个标志都能用于新生代空间的调整
-XX:NewRatio=N 设置新生代与老年代的空间占用比率(默认值为2)
-XX:NewSize=N 设置新生代空间的初始大小
-XX:MaxNewSize=N 设置新生代空间的最大大小
-Xmn N 将NewSize和MaxNewSize设置为同一个值的快捷方法
Initial Young Gen Size=Initial Heap Size/(1+NewRatio)
快速小结
1.整个堆范围内,不同代的大小划分是由新生代所占用的空间控制的。
2.新生代的大小会随着整个堆大小的增大而增长,但这也是随着整个堆的空间比率波动变化的(依据新生代的初始值和最大值)。
3.永久代和元空间的调整
JVM载入类的时候,需要记录这些类的元数据,这部分数据被保存在一个单独的对空间中。
Java7里,被称为永久代(Permgen或Permanent Generation)
Java8中,被称为原空间(Metaspace)
永久代和元空间内保存的信息只对编译器或者JVM的运行时有用,这部分信息被称为“类的元数据”
使用元空间替换掉永久代的优势之一是我们不再需要对其进行调整
对于永久代而言,可以通过-XX:PermSize=N -XX:MaxPermSize=N调整大小
对于元空间,可以通过-XX:MetaspaceSize=N 和-XX:MaxMetaspaceSize=N调整大小
如果程序在启动时发生了大量的Full GC(因为需要载入数量巨大的类),通常都是由于永久代或元空间发生了大小调整,因此这种情况下为了改善启动速度,可以增大初始值。
使用jmap和-permstat参数(适用于Java7)或-clstats参数(适用于Java8)可以输出类加载相关的信息。
4.控制并发
除Serial收集器外的几乎所有收集器使用的算法都基于多线程。启动的线程数由-XX:ParallelGCThreads=N参数控制
ParallelGCThreads=8+((N-8)*5/8) N代表CPU的数目
快速小结
1.几乎所有的垃圾收集算法中基本的垃圾回收线程数都依据机器上的CPU数目计算得出
2.多个JVM运行于同一台2物理机上时,根据公式计算出的线程数可能过高,必须进行优化(减少)
5.自适应调整
使用-XX:-UseAdaptiveSizePolicy标志可以在全局范围内关闭自适应调整功能(默认情况下,这个标志是开启的)
如果你想了解应用程序运行时JVM的空间是如何调整的,可以设置-XX:+PrintAdaptiveSizePolicy标志。开启该标志后,一旦发生垃圾回收,GC的日志中会包含垃圾回收时不同代进行空间调整的细节信息
快速小结
1.JVM在堆的内部如何调整新生代及老年代的百分比是由自适应调整机制控制的
2.通常情况下,我们应该开启自适应调整,因为垃圾回收算法依赖于调整后的代的大小来达到它停顿时间的性能目标。
3.对于已经精细调优过的堆,关闭自适应调整能获得一定的性能提升。
垃圾回收工具
观察垃圾回收对应用程序的性能影响最好的方法就是尽量熟悉垃圾回收的日志,垃圾回收日志中包含了程序运行过程中每一次垃圾回收操作。
多种方法都能开启GC的日志功能
使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志(这两个日志实际上互为别名,默认情况下GC日志功能是关闭的)
使用-XX:+PrintGCDetails标志会创建更详细的GC日志
使用-XX:+PrintGCTimestamps或者-XX:+PrintGCDateStamps便于我们更精确地判断几次GC操作之间的时间。
使用-Xloggc:filename标志也能修改输出GC日志到某个文件。除非显式地使用-XX:+PrintGCDetails标志,否则使用-Xloggc会自动开启基本日志模式
通过-XX:+UseGCLogfileRotation -XX:NumberOfGCLogfiles=N -XX:GCLogfileSize=N标志可以控制日志文件的循环
GC Histogram(http://java.net/projects/gchisto)能够读入GC日志,根据日志文件中的数据生成对应的图表和表格。
jstat提供了9个选项,提供堆的各种数据;使用jstat -options选项能够列出所有的选项。最常用的一项是-gcutil,它能够输出消耗在GC上的时间,以及每个GC区域使用的百分比。其他的选择能够以KB为代为输出各GC空间的大小。
快速小结
1.GC日志是分析GC相关问题的重要线索;我们应该开启GC日志标志(即便是在生产服务器上)
2.使用PrintDetails标志能获得更详尽的GC日志信息
3.使用工具能很有效地帮助我们解析和理解GC日志的内容,尤其是在对GC中的数据归纳汇总时,它们非常有帮助
4.使用jstat能够动态地观察运行程序的垃圾回收操作。
垃圾收集算法
理解Throughput收集器
通常新生代的垃圾回收发生在Eden空间快用尽时。新生代的垃圾收集会把Eden空间中的所有对象挪走:一部分对象会被移动到Survivor空间,其他的会被移动到老年代。
老年代垃圾收集会回收新生代中的所有对象(包括Survivor空间中的对象)。只有哪些活跃引用的对象,或者已经经过压缩整理的对象(它们占据了老年代的开始部分)会在老年代中继续保持,其余的对象都会被回收
快速小结
1.Throughput收集器会进行两种操作,分别是Minor GC和Full GC
2.通过GC日志中的时间输出,我们可以迅速地判断出Throughput收集器的GC操作对应用程序的总体性能影响
堆大小的自适应调整和静态调整
Throughput收集器的自动调优几乎都是围绕停顿时间进行的,寻求堆的总体大小、新生代的大小以及老年代大小之间的平衡。
-XX:MaxGCPauseMillis=N 标志用于设定应用可承受的最大停顿时间。这个标志设定的值同时影响Minor GC和Full GC
-XX:GCTimeRatio=N 可以设置你希望应用程序在垃圾回收上花费多少时间(与应用程序的运行时间相比较)。
快速小结
1.采用动态调整是进行堆调优极好的入手点。对很多的应用程序而言,采用动态调整就已经足够,动态调整的配置能够有效地减少JVM的内存使用
2.静态地设置堆的大小也可能获得最优的性能。设置合理的性能目标,让JVM根据设置确定堆的大小是学习这种调优很好的入门课程。
理解CMS收集器
CMS收集器有3中基本的操作
1.会对新生代的对象进行回收(所有的应用线程都会被暂停)
2.会启动一个并发的线程对老年代空间的垃圾进行回收
如果有必要,CMS会发起Full GC
JVM会依据堆的使用情况启动并发回收。当堆的占用达到某个程度时,JVM会启动后台线程扫描堆,回收不用的对象。
如果使用CMS回收器,老年代空间不会压缩整理
并发回收
并发回收由“初始标记”阶段开始,这个阶段会暂停所有的应用程序线程
下一个阶段是“标记阶段”,这个阶段中应用程序可以持续运行,不会被中断。
然后是“预清理”阶段,这个阶段也是与应用程序线程的运行并发进行的
接下来是“重新标记”阶段,这个阶段涵盖了多个操作
使用可中断预清理阶段是由于标记阶段不是并发的,所有的应用线程进入标记阶段后都会被暂停
使用可中断预清理阶段的目的就是希望尽量缩短停顿的长度,避免连续的停顿。
接下来是另一个并发阶段——清除(sweep)阶段
接下来是并发重置(concurrent reset)阶段
并发回收可能出现的消息
1.并发模式失效(concurrent mode failure)
新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS垃圾回收就会退化成Full GC
2.promotion failed
老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败
快速小结
1.CMS垃圾回收有多个操作,但是期望的操作是Minor GC和并发回收(concurrent cycle)
2.CMS收集过程中的并发模式失败以及晋升失败的代价都非常昂贵
3.默认情况下CMS不会对永久代进行垃圾回收
针对并发模式失效的调优
CMS收集器使用两个配置 MaxGCPauseMllis=N和GCTimeRadio=N来确定使用多大的堆和多大的代空间
CMS收集器与其他垃圾收集方法一个显著的不同是除非发生Full GC,否则CMS的新生代大小不会调整
1.给后台线程更多的运行机会
-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly 同时使用能帮助CMS更容易进行决策
2.调整CMS后台线程
每个CMS后台进程都会100%地占用机器上的一颗CPU。如果应用程序发生并发模式失效,同时又有额外的CPU周期可用,可以设置-XX:ConcGCThreads=N标志,增加后台线程的数目。默认情况下,ConGCThreads的值是根据ParallelGCThreads标志的值计算得到的: ConcGCThreads=(3+ParallelGCThreads)/4
快速小结
1.避免发生并发模式失效是提升CMS收集器处理能力、获得高性能的关键
2.避免并发模式失效(如果有可能的话)最简单的方法是增大堆的容量
3.否则,我们能进行的下一个步骤就是通过调整CMSInitiatingOccupancyFraction参数,尽早启动并发后台线程的运行
4.另外,调整后台线程的数目对解决这个问题也有帮助
CMS收集器的永久代调优
默认情况下,Java7中的CMS垃圾收集线程不会处理永久代中的垃圾,如果永久代空间用尽,CMS会发起一次Full GC来回收其中的垃圾对象。除此之外,还可以开启-XX:+CMSPerGenSweepingEnabled标志(默认情况下,该标志的值为false),开启后,永久代中的垃圾使用与老年代同样的方式进行垃圾收集。
使用-XX:CMSInitiatingPermOccupancyFraction=N参数可以指定CMS收集器在永久代空间占用比达到设定值时启动永久代垃圾回收线程,这个参数默认值为80%
为了真正释放不再被引用的类,还需要设置-XX:+CMSClassUnloadingEnable标志,否则,即使启用了永久代垃圾回收也只能释放少量的无效对象,累的元数据并不会释放。
Java8中,CMS收集器默认就会收集元空间中不再载入的类。如果由于某些原因,你希望关闭这一功能,可以通过-XX:-CMSClassUnloadingEnable标志进行关闭(默认情况下这个标志是开启的,即该值为true)
增量式CMS垃圾收集
增量式的CMS垃圾收集在Java8中已经不推荐使用
如果系统确实只配备了极其有限的CPU,作为替代方案,可以考虑使用G1收集器——因为G1收集器的后台线程在垃圾收集的过程中也会周期性地暂停,客观上减少了与应用线程竞争CPU资源的情况。
指定-XX:+CMSIncrementalMode标示可以开启增量式CMS垃圾收集。通过改变标志-XX:CMSIncrementalSafetyFactor=N -XX:CMSIncrementalDutyCycleMin=N和-XX:CMSIncrementalPacing可以控制垃圾收集后台线程为应用程序线程让出多少CPU周期
增量式CMS垃圾收集依据责任周期(duty cycle)原则进行工作 ,这个原则决定了CMS垃圾收集器的后台线程在释放CPU周期给应用线程之前,每隔多长时间扫描一次堆
责任周期的时间长度是以新生代相邻两次垃圾收集之间的时间长度计算得出的;默认情况下,增量式CMS垃圾收集持续的时间是该时长的20%左右
快速小结
1.应用在CPU资源受限的机器上运行,同时又要求较小的停顿,这时使用增量式CMS收集器是一个不错的选择
2.通过责任周期可以调整增量式CMS收集器;增加责任周期的运行时间可以避免CMS收集器发生并发模式失效
理解G1垃圾收集器
G1垃圾收集器是一种工作在堆内不同分区上的并发收集器。分区(region)既可以归属于老年代,也可以归属于新生代(默认情况下,一个堆被划分为2048个分区);同一个代的分区不需要保持连续
这种只专注于垃圾最多分区的方式就是G1垃圾收集器名称的由来,即首先收集垃圾最多的分区
新生代进行垃圾回收时,整个新生代空间要么被回收,要么被晋升
G1垃圾收集器的收集活动主要包括4种操作
新生代垃圾收集
后台收集,并发周期
并发周期的第一阶段是初始—标记(initial-mark)阶段。这个阶段会暂停所有应用线程——部分源于初始—标记阶段也会进行新生代垃圾收集
接下来,G1收集器会扫描根分区(root region)
根分区扫描完成后,G1收集器就进入到并发标记阶段
紧接在标记阶段之后的是重新标记(remarking)阶段和正常的清理阶段
紧接着是一个额外的并发清理阶段
混合式垃圾收集
混合式垃圾回收周期会持续运行直到(几乎)所有标记的分区都被回收,这之后G1收集器会恢复常规的新生代垃圾回收周期
必要时的Full GC
并发模式失效
发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整后期,让它运行的更快(譬如,增加后台处理的线程数)
晋升失败
这种失败通常意味着混合式收集需要更迅速地完成垃圾收集;每次新生代垃圾收集需要处理更多老年代的分区
疏散失败
进行新生代垃圾收集时,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象
解决这个问题最简单的方式是增加堆的大小
巨型对象分配失败
快速小结
1.G1垃圾收集器包括多个周期(以及并发周期内的阶段)。调优良好的JVM运行G1收集器时应该只经历新生代周期、混合式周期和并发GC周期。
2.G1的并发阶段会产生少量的停顿
3.恰当的时候,我们需要对G1进行调优,才能避免Full GC周期的发生
G1垃圾收集器调优
G1垃圾收集器调优的主要目标是避免发生并发模式失败或者疏散失败,一旦发生这些失败就会导致Full GC
避免发生Full GC
通过增加总的堆空间大小或者调整老年代、新生代之间的比例来增加老年代空间的大小
增加后台线程的数目(假设我们有足够的CPU资源运行这些线程)
以更高的频率进行G1的后台垃圾收集活动
在混合式来及回收周期中完成更多的垃圾收集工作
G1收集器最主要的调优只通过一个标志进行:-XX:MaxGCPauseMillis=N(默认值为200)
如果G1收集器发生时空停顿的时长超过该值,G1收集器就会尝试各种方式进行弥补——譬如调整新生代与老年代的比例,调整堆得大小,更早地启用后台处理,改变晋升阈值,或者是在混合式垃圾收集周期中处理更多或者更少的老年代分区(这是最重要的方式)
调整G1垃圾收集的后台线程数
对于应用线程暂停运行的周期,可以使用ParallelGCThreads标志设置运行的线程数
对于并发运行阶段可以使用ConcGCThreads标志设置运行的线程数
调整G1垃圾收集器运行的频率
G1垃圾收集周期通常在堆得占用达到参数-XX:InitatingHeapOccupancyPercent=N设定的比率时启动,默认情况下该参数的值为45
调整G1垃圾收集器的混合式垃圾收集周期
混合式垃圾收集要处理的工作量取决于三个因素
第一个因素是有多少分区被发现大部分是垃圾对象
第二个因素是G1垃圾收集回收分区时的最大混合式GC周期数,通过参数-XX:G1MixedGCCountTatget=N可以进行调节,默认值为8;减少该参数值可以帮助解决晋升失败的问题(代价是混合式GC周期的停顿时间会更长);另一方面,如果混合式GC的停顿时间过长,可以增大这个参数的值,减少每次混合式GC周期的工作量
第三个影响因素是GC停顿可忍受的最大时长(通过MaxGCPauseMillis参数确定);增大MaxGCPauseMillis能在每次混合式GC中收集更多的老年代分区,而这反过来又能帮助G1收集器在更早的时候启动并发周期
快速小结
1.作为G1收集器调优的第一步,首先应该设定一个合理的停顿时间作为目标。
2.如果使用这个设置后,还是频繁发生Full GC,并且堆的大小没有扩大的可能,这是就需要针对特定的失败采用特定的方法进行调优
a.通过InitiatingHeapOccpancyPercent标志可以调整G1收集器,更频繁地启动后台垃圾收集线程
b.如果有充足的CPU资源,可以考虑调整ConcGCThreads标志,增加垃圾收集线程数
c.减少G1MixedGCCountTarget参数可以避免晋升失败
高级调优
晋升及Survivor空间
两种情况下对象会被移动到老年代
Survivor空间的大小实在太小。新生代垃圾收集时,如果目标Survivor空间被填满,Eden空间剩下的活跃对象会直接进入老年代。
对象在Survivor空间中经历的GC周期数有个上限,超过这个上限的对象也会被移动到老年代。这个上限值被称为晋升阈值(Tenuring Threshold)
Survivor空间的初始大小由-XX:InitialSruvivorRatio=N标志决定
survivor_space_size=new_size/(initial_surivor_ratio+2)
默认为8
Survivor空间的上限由-XX:MinSurvivorRatio=N设置
maximum_survivor_space_size=new_size/(min_survivor_ratio+2)
默认为3
可以使用SurvivorRatio参数将Survivor空间的大小设置为固定值,同时关闭UseAdaptiveSizePolicy标志
通过标志-XX:TargetSurvivorRatio=N可以设置Survivor空间中垃圾回收之后的空闲比率
通过-XX:InitialTenuringThreshold=N标志可以设置初始的晋升阈值
最大晋升阈值由-XX:MaxTenuringThreshold=N标志设定
可以使用-XX:+AlwaysTenure标志(默认为false)使得对象直接晋升到老年代,不会再存放于Survivor空间
-XX:+NeverTenure(默认值也是false),开启后只要Survivor空间有容量,就不会有对象被晋升到老年代
使用-XX:+PrintTenuringDistribution标志可以在GC日志中增加晋升的统计信息
快速小结
1.设计Survivor空间的初衷是为了让对象(尤其是已经分配的对象)在新生代停留更多的GC周期。这个设计增大了对象晋升到老年代之前被回收释放的几率
2.如果Survivor空间过小,对象会直接晋升到老年代,从而触发更多的老年代GC。
3.解决这个问题的最好方法是增大堆的大小(或者至少增加新生代),让JVM来处理Survivor空间的回收
4.有的情况下,我们需要避免对象晋升到老年代,调整晋升阈值或者Survivor空间的大小可以避免对象晋升到老年代
分配大对象
线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)
TLAB
默认情况下TLAB就是开启的,JVM管理着它们的大小以及如何使用
默认情况下,TLAB的大小由三个因素决定:应用程序的线程数、Eden空间的大小以及线程的分配率
使用-XX:-UseTLAB可以关闭TLAB
对于开源版本的JVM(不附带JFR),要监控TLAB的分配情况,最好的途径就是在命令行中添加-XX:+PrintTLAB标志。
调整TLAB的大小
使用-XX:TLABSize=N标志可以显式地制定TLAB的大小(默认为0)
使用-XX:-ResizeTLAB标志可以避免每次GC时都调整TLAB的大小
TLAB空间调整生效时,其容量的最小值可以使用-XX:MinTLABSize=N参数设置(默认为2KB)
快速小结
对需要分配大量大型对象的应用,TLAB空间的调整就变得必不可少
巨型对象(Humongous Objects)
对TLAB空间中无法分配的对象,JVM会尽量尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中分配空间
G1分区的大小
G1收集器将堆划分成了一定数量的分区,每个分区的大小都是固定的
分区的大小最小是1MB,最大不能超过32MB
G1分区的大小可以通过-XX:G1HeapRegionSize=N标志设置
使用G1收集器分配巨型对象
增大G1分区的大小,让其能够在一个分区内分配应用需要的所有对象能够提升G1收集器的效率
快速小结
1.G1分区的大小是2的幂,最小值为1MB
2.如果堆的初始大小跟最大值相差很大,这种堆会有大量的G1分区,在这种情况下,应该增大G1分区的大小。
3.如果要分配的对象超过了G1收集器分区容量的一般,对于这种应用程序,我们应该增大G1分区的容量,让G1分区能更好地适配这些对象。遵循这个原则,应用程序分配对象的大小至少应是512KB(因为G1分区的最小值为1MB)
AggressiveHeap标志
使用AggressiveHeap标志能方便地设置各种命令参数。这个标志只适用于64位的JVM
PLAB的大小
PLAB的全称是晋升本地分配缓冲(Promotion-Local Allocation Buffer),是垃圾回收清理代数据时基于线程分配的分区
编译策略
JVM发布时配备了多种JIT编译算法
关闭Full GC之前的新生代垃圾收集
将ScavengeBeforeFullGC标志设置为false意味着Full GC发生时,JVM不会对Full GC之前的新生代垃圾进行收集
将GC线程绑定到特定的CPU
快速小结
1.AggressiveHeap是个历史悠久的调优标志,设计初衷是为了在强大的机器上运行单一JVM时调整堆的各种参数。
2.这个标志设定的值并没有随着JVM技术的发展同步调整,因此它的有效性从长远来看是值得质疑的
全盘掌控堆空间的大小
堆的默认大小依据机器的内存配置确定,不过也可以通过参数-XX:MaxRAM=N设置
堆的最大容量是MaxRAM值的四分之一
如果机器的物理内存比MaxRAM的值小,默认堆的大小就是物理内存的1/4。但是相反的规则并不适用,即使机器配置了数百GB的内存,JVM能使用的最大堆容量也不会超过默认值32GB,及128GB的1/4.
最大堆的计算实际采用下面的公式 Default Xmx=MaxRAM/MaxRAMFraction
默认最大堆的大小也可以通过-XX:MaxRAMFraction=N标志值进行调整,MaxRAMFraction的默认值为4
-XX:ErgoHeapSizeLimit=N 默认值为0,否则,如果该标志设置的值比MaxRAM/MaxRAMFraction还小,就是用该参数设置的值
-XX:MinRAMFraction=N参数的默认值为2
堆的初始大小计算采用:Default Xms=MaxRAM/InitialRAMFraction
InitialRAMFraction默认值为64
指定的InitialRAMFraction小于-XX:OldSize=N的参数设定(该参数默认为4MB),堆的初始大小等于新生代和老年代大小之和
快速小结
1.大多数机器上堆的初始空间和最大空间默认值计算是比较直观的
2.达到堆大小的临界情况时,需要考虑的因素更多,计算也更加复杂
小结
堆内存的最佳实践
堆分析
堆直方图
堆直方图可使用命令 %jcmd jvm进程id GC.class_histogram 该命令的输出中仅包含活跃对象
在堆直方图中,Klass相关的对象往往会接近顶端,它们是加载类得到的元数据对象。
%jmap -histo process_id 该命令的输出中包含被回收的对象
%jmap -histo:live process_id 会在看到直方图之前强制执行一次Full GC
堆转储
用命令行生成堆转储
%jcmd process_id GC.heap_dump /path/to/head_dump.hprof
%jmap -dump:live,file=/path/to/heap_dump.hprof process_id
在jmap中包含live选项,这会在堆被转储之前强制执行一次Full GC;jcmd默认就会这么做,如果因为某些原因,你希望包含其他对象(即死对象),可以在jcmd命令的最后加上-all
打开堆转储文件
jhat
最原始的分析工具。它会读取堆转储文件,并运行一个小型的HTTP服务器,该服务器允许你通过一系列网页链接查看堆转储信息
jvisualvm
jvisualvm的监视(Monitor)选项卡可以从一个运行中的程序获得堆转储文件,可以打开之前生成的堆转储文件。
mat
开源的EclipseLink内存分析工具(EclipseLink Memory Analyzer Tool,mat)可以加载一个或多个堆转储文件并执行分析。它可以生成报告,向我们建议可能存在问题的地方;也可以用于浏览堆,并对堆实行类SQL的查询
一个对象的保留内存,是指回收该对象可以释放出的内存量
一个对象的浅(shallow)大小,是指该对象本身的大小。如果该对象包含一个指向另一个对象的引用,4字节或8字节的引用会计算在内,但是目标对象的大小不会包含进来。深(deep)对象则包含那些对象的大小,深大小与保留大小的区别在于那些存在共享的对象。
GC根是一些系统对象,其中保存着一些(通过一个较长的由其他对象组成的链条)指向问题中对象的静态和全局引用
快速小结
1.了解哪些对象正在消耗内存,是了解要优化代码中哪些对象的第一步
2.对于识别由创建了太多某一特定类型对象所引发的内存问题,直方图这一方法快速且方便
3.堆转储分析是追踪内存使用最强大的技术,不过要利用好,则需要一些耐心和努力
内存溢出错误
在下列情况下,JVM会抛出内存溢出错误(OutOfMemoryError)
JVM没有原生内存可用
永久代(在Java7和更早版本中)或元空间(在Java8中)内存不足
如果你正在编写的应用会创建并丢弃大量类加载器,一定要非常谨慎,确保类加载器本身能正确丢弃
Java堆本身内存不足——对于给定的堆空间而言,应用中活跃对象太多
如果应用存在内存泄漏,可以间隔几分钟,获得连续的一些堆转储文件,然后加以比较,mat内置了这一功能:如果打开了两个堆转储文件,mat有一个选项用来计算两个堆中的直方图之间的差别
JVM执行GC耗时太多
自动堆转储
-XX:+HeapDumpOnOutOfMemoryError
该标志默认为false,打开该标志,JVM会在抛出OutOfMemoryError时创建堆转储
-XX:HeapDumpPath=
该标志制定了堆转储将被写入的位置;默认会在应用的当前工作目录下生成java_pid.hprof文件,这里的路径可以指定目录(使用默认文件名),也可以指定要生成的实际文件的名字
-XX:+HeapDumpAfterFullGC
这会在运行一次Full GC之后生成一个堆转储文件
-XX:+HeapDumpBeforeFullGC
这会在运行一次Full GC之前生成一个堆转储文件
快速小结
1.有多种原因会导致抛出OutOfMemoryError,因此不要假设堆空间就是问题所在
2.对于永久代和普通的堆,内存泄漏时出现OutOfMemoryError时最常见的原因;堆分析工具可以帮助我们找到泄漏的根源
减少内存使用
减少对象大小
两种方式
1.减少实例变量的个数
2.减少实例变量的大小
对象对齐与对象大小
为了使对象大小是8字节的整数倍(对齐),总是会有填充操作。
JVM也会填充字节数不规则的对象,这样不管底层架构最适合什么样的地址边界,对象的数组都能优雅地适应
尽早清理
通过将变量的值设为null,实现尽早清理,从而使问题中的对象可以更快地被垃圾回收器回收
如果一个长期存活的类会缓存以及丢弃对象引用,那一定要仔细处理,以避免过时引用。否则,显式地将一个对象引用设置为null在性能方面基本没什么好处
延迟初始化
延迟初始化运行时性能
检查要进行延迟初始化的变量是不是已经被初始化了,未必总会有性能损失
快速小结
只有当常用的代码路径不会初始化某个变量时,才去考虑延迟初始化该变量
一般不会在线程安全的代码上引入延迟初始化,否则会加重现有的同步成本
对于使用了线程安全对象的代码,如果要采用延迟初始化,应该使用双重检查锁。
不可变对象和标准化对象
像这类不可变对象的单一化表示,就被称为对象的标准化(canonical)版本
要标准化某个对象,创建一个Map来保存该对象的标准化版本。为防止内存泄漏,务必保证用弱引用处理Map中的对象。
快速小结
1.不可变对象为标准化(canonicalization)这种特殊的生命周期管理提供了可能性。
2.通过标准化去掉不可变对象的冗余副本,可以极大减少应用消耗的堆内存
字符串的保留
String类提供了自己的标准化方法:intern()方法
保留字符串的表是保存在原生内存中的,它是一个大小固定的Hashtable
大小固定的Hashtable
从概念上讲,一个Hashtable包含一个数组,它会保存一些条目(数组中的每个元素叫作一个桶)。当要将一个对象保存到Hashtable中时,可以用该对象的哈希值堆桶的数目取余,以此确定对象在数组中的存储位置。这种情况下,两个哈希值不同的对象很有可能被映射到同一个桶中,每个桶实际就是一个链表,其中按顺序存储了映射到该桶的条目。当两个对象映射到一个桶时,这就叫“冲突”
从Java7中开始,这个表的大小可以在JVM启动时使用-XX:StringTableSize=N(如前面所介绍的,默认值为1009或60013)。如果某个应用会保留大量字符串,就应该增加这个值。如果这个值是个素数,字符串保留表的效率最高。
如果想看看字符串表的执行过程,可以使用-XX:PrintStringTableStatistics参数(这个标志要求JDK7u6或更新版本,默认为false)运行应用
某个应用中已经分配的保留字符串个数(及其总大小),可以使用如下的jmap命令获得
%jmap -heap process_id
快速小结
1.如果应用中有大量字符串是一样的,那通过保留实现字符串重用收效很大。
2.要保留很多字符串的应用可能需要调整字符串保留表的大小
对象生命周期管理
对象重用
对象重用通常有两种实现方式:对象池和线程局部变量
JDK提供了一些常见的对象池:线程池和软引用。软引用本质上是一大池可重用对象。同时Java EE依赖对象池来连接数据库和其他资源,而EJB的整个生命周期都是围绕对象池的概念构建的。
对象池
线程池的大小可能很难正确地设置,他们讲对象管理的负担又抛给程序员了;程序员不能简单地将对象丢出作用域,而必须记得将其返还到对象池中。
线程局部变量
生命周期管理
线程局部变量要比池中管理对象更容易,成本更低
线程局部对象在线程内总是可用的,不需要显式地归还
基数性(Cardinality)
线程局部变量通常会伴生线程数与保存的可重用对象数之间的一一对应关系。
同步
线程局部变量不需要同步,因为它们只能用于一个线程之内;而且线程局部的get()方法相当快
快速小结
1.对象重用通常是一种通用操作,我们并不鼓励使用它,但是这种技术可能适合初始化成本高昂,而且数量比较少的一组对象。
2.在使用对象池还是使用线程局部变量这两种技术之间,应该有所取舍。一般而言,假设线程和可重用对象直接存在一一对应关系,则线程局部变量更容易使用
弱引用、软引用、其他引用
术语说明
引用(Reference)
引用(或者说对象引用)可以是任何类型的引用:强引用、弱引用、软引用等。指向一个对象的普通引用实例变量就是一个强引用。
非确定引用(Indefinite reference)
本书使用这个术语来区分强引用和其他特殊引用(比如软引用或弱引用)。一个非确定引用其实是一个对象实例(比如,SoftReference类的一个实例)
所引对象
非确定引用的工作方式是,在非确定引用类的实例内,嵌入另一个引用(几乎总是嵌入一个强引用)。被封装的对象称作“所引对象”
与对象池或线程局部变量相比,非确定引用的优势在于,它们最终会被垃圾回收器回收。
非确定引用的缺点是堆垃圾收集器的效率会有轻微影响。
非确定引用和其他任何对象一样:它们也消耗内存,而且其他变量也是通过强引用引用它们
垃圾收集器要回收非确定引用,至少需要两个GC周期
GC日志与引用处理
当运行一个使用了大量非确定引用的对象时,可以考虑添加-XX:+PrintReferenceGC标志(默认为false)。这样就能看懂处理这些引用花了多少时间
1.软引用
如果问题中的对象以后有很大机会重用,可以使用软引用,但是如果该对象近期一直没有使用到(计算时也会考虑堆还有多少内存可用),垃圾收集器会回收它。
软引用本质上是一个比较大的、最近最久未用(LRU)的对象池。获得较好性能的关键是确保它们会被及时清理
如果对象的数目不是特别大,软引用就会工作得很好。否则,就要考虑用更传统的、固定大小的对象池来实现一个LRU缓存。
2.弱引用
当问题中的所引对象会同时被几个线程使用时,应该考虑弱引用。否则,弱引用很可能会被垃圾收集器回收;只有弱引用的对象在每个GC周期都可以回收。
当强引用被移除时,弱引用会立即释放
引用对象就和其他Java对象一样:在年轻代中创建,最终会被提升到老年代。如果弱引用本身仍在年轻代中,而弱引用的所引对象被释放了,则弱引用可以快速释放(下一次Minor GC时)
非确定引用集合
JDK提供了两个保存非确定引用的集合类:WeakHashMap和WeakIdentityMap
3.终结器(Finalizer)和最终引用(Final Reference)
每个Java类都有一个从Object类继承而来的finalize()方法;在对象可以被来及收集器回收时,可以用这个方法来清理数据。
终结器实际上是非确定引用的一种特殊情况:JVM使用了一个私有的引用类(java.lang.ref.finalize,它又是java.lang.ref.finalReference的子类)来记录finalize()方法的对象。当一个具有finalize()方法的对象被分配时,JVM会分配两个对象:一个是该对象本身,另一个是一个以该对象为所引对象的finalizer引用
通常,如果使用终结器是不可避免的,那么一定要确保尽量减少该对象访问的内存。
对于使用终结器,还有一种替代方案,至少可以避免部分问题。特别是,这种方案支持在正常的GC操作期间释放所引对象。这是通过使用另一种非确定引用实现的,而非隐式地使用finalizer引用。有时推荐使用另一种非确定引用类型:PhantomReference(虚引用)类
终结器队列
终结器队列是一个引用队列,用于当索引对象可以被GC回收时处理Finalizer引用
可以通过如下命令让JVM处理终结器队列: %jcmd process_id GC.run_finalization
要监控Finalizer队列的命令如下: %jmap -finalizerinfo process_id
快速小结
1.非确定引用(包括软引用、弱引用、虚引用和最终引用)会改变Java对象正常的生命周期,与池或对象局部变量相比,它可以以对GC更友好的方式实现对象重用。
2.当应用对某个对象感兴趣,而且该对象在应用中的其他地方有强引用时,才应该使用弱引用
3.软引用保存可能长期存在的对象,提供了一个简单的、对GC友好的LRU缓存
4.非确定引用自身会消耗内存,而且会长时间抓住其他对象的内存,应该谨慎使用
原生内存最佳实践
内存占用
概述
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.多个过滤器有些开销,所以要确保编写好用的过滤器
小结