性能调优读书笔记(下篇)
一、并行程序开发优化
1、Future 设计模式
public class Client {
public Data request(final String queryStr){
final FutureData future=new FutureData();
new Thread(){
public void run(){
RealData realData=new RealData(queryStr);
future.setRealData(realData);
}
}.start();
return future;
}
}
public interface Data {
String getResult();
}
public class FutureData implements Data {
//FutureData是RealData的包装
protected RealData realData=null;
protected boolean isReady=false;
public synchronized void setRealData(RealData realData){
if(isReady)
return;
this.realData=realData;
isReady=true;
notifyAll();
}
@Override
public synchronized String getResult() {
while (!isReady){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return realData.result;
}
}
public class RealData implements Data {
protected final String result;
public RealData(String para){
//RealData的构造很慢
StringBuffer sb=new StringBuffer();
for (int i=0;i<10;i++){
sb.append(para);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
result=sb.toString();
}
@Override
public String getResult() {
return result;
}
}
2、Master-Worker 模式
Master-Worker 的好处是可以将大任务分为若干个小任务,并行执行。
public class Master {
//任务队列
protected Queue<Object> workQueue=new ConcurrentLinkedQueue<>();
//Woker线程队列
protected Map<String,Thread> threadMap=new HashMap<>();
//子任务的结果集
protected Map<String,Object> resultMap=new ConcurrentHashMap<>();
//是否所有子任务都结束了
public boolean isComplete(){
for (Map.Entry<String,Thread> entry:threadMap.entrySet()){
if(entry.getValue().getState()!=Thread.State.TERMINATED){
return false;
}
}
return true;
}
public Master(Worker worker,int countWorker){
worker.setWorkQueue(workQueue);
worker.setResultMap(resultMap);
for (int i = 0; i <countWorker ; i++) {
threadMap.put(Integer.toString(i),new Thread(worker,Integer.toString(i)));
}
}
//提交一个任务
public void submit(Object object){
workQueue.add(object);
}
//返回子任务结果集
public Map<String,Object> getResultMap(){
return resultMap;
}
//开始运行所有的Worker进程
public void execute(){
for (Map.Entry<String,Thread> entry:threadMap.entrySet()){
entry.getValue().start();
}
}
}
public class Worker implements Runnable {
//任务队列,用于取得子任务
protected Queue<Object> workQueue;
//子任务处理结果集
protected Map<String,Object> resultMap;
public void setWorkQueue(Queue<Object> workQueue) {
this.workQueue = workQueue;
}
//子任务的处理逻辑,在子类中具体实现
public Object handle(Object input){
return input;
}
public void setResultMap(Map<String, Object> resultMap) {
this.resultMap = resultMap;
}
@Override
public void run() {
while (true){
Object input=workQueue.poll();
if(input==null)break;
Object result=handle(input);
resultMap.put(Integer.toString(input.hashCode()),result);
}
}
}
public class PlusWorker extends Worker {
@Override
public Object handle(Object input) {
if(input instanceof Integer){
Integer i=(Integer)input;
return i*i*i;
}
return 0;
}
}
测试代码:
@Test
public void test26(){
long start=System.currentTimeMillis();
Master master=new Master(new PlusWorker(),5);
for (int i = 0; i <100 ; i++) {
master.submit(i);
}
master.execute();
long re=0;
Map<String,Object> resultMap=master.getResultMap();
while (resultMap.size()>0||!master.isComplete()){
Set<String> keys = resultMap.keySet();
String key=null;
for (String k:keys){
key=k;
break;
}
Integer i=null;
if(key!=null)
i=(Integer)resultMap.get(key);
if(i!=null)
re+=i;
if(key!=null)
resultMap.remove(key);
}
System.out.println(re);
System.out.println(System.currentTimeMillis()-start);
}
3、优化线程池大小
Ncpu:CPU 的数量
Ucpu:目标 Cpu 的使用率 0<=Ucpu<=1
W/C:等待时间和计算时间的比率
最优线程池大小为:
Nthreads=NcpuUcpu(1+W/C);
4、扩展 ThreadPoolExecutor
ThreadPoolExecutor 是一个可以扩展的线程池,它提供了 beforeExecutor()和 afterExecutor()和 terminated()3 个方法进行扩展。
5、并发数据结构
- 并发 List
CopyOnWriteArrayList:适用于读多写少的场景 - 并发 Set
CopyOnWriteArraySet:适用于读多写少的场景,如果有并发写的情况,也可使用 Collections.synchronizedSet(Sets)方法得到一个线程安全的 Set - 并发 Map
ConcurrentHashMap - 并发 Queue
JDK 提供了两套实现,一套是 ConcurrentLinkedQueue 为代表的高性能队列,一个是以 BlockingQueue 接口为代表的阻塞队列 - 并发 Deque(双端队列)
LinkedBlockingDeque
6、锁的性能优化
- 避免死锁
- 减小锁的作用范围
- 减小锁的粒度
- 读写分离锁代替独占锁
- 锁分离
- 重入锁(ReentrantLock)和内部锁(Synchronized)
- 锁粗化:不断地获取和释放锁也很耗资源,比如在 for 循环里使用 synchronized
- 自旋锁:线程没有取得锁时不被挂起,转而去执行一个空循环。
- 锁消除:JVM 在即时编译时通过多运行上下文的扫描,去除不可能有共享资源竞争的锁。如方法内部的 StringBuffer。
逃逸分析和锁消除分别可以使用-XX:+DoEscapeAnalysis 和-XX:+EliminateLocks 开启(锁消除必须工作在-server 模式下)
实例如下:
分别在-server -XX:-DoEscapeAnalysis -XX:-EliminateLocks 和-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 下运行程序
public class LockTest {
private static final int CIRCLE=20000000;
public static void main(String[] args) {
long start=System.currentTimeMillis();
for (int i = 0; i <CIRCLE ; i++) {
createStringBuffer("java","Performance");
}
long bufferCost=System.currentTimeMillis()-start;
System.out.println("CreateStringBuffer:"+bufferCost+"ms");
}
public static String createStringBuffer(String s1,String s2){
StringBuffer sb=new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
}
- 锁偏向:如果程序没有竞争,则取消之前获得锁的线程的同步操作。通过设置-XX:+UseBiasedLocking 可以设置启用偏向锁
- Amino 集合:无锁的线程安全集合框架。包下载地址:https://sourceforge.net/projects/amino-cbbs/files/cbbs/
相关集合类有 LockFreeList 和 LockFreeVector,LockFreeSet,LockFreeBSTree。另外该框架还实现了两个 Master-Worker 模式。一个静态的和动态的。分别需要实现 Doable 和 DynamicWorker 接口。
/**
* Amino框架实现了Master-woker模式
*/
public class Pow3 implements Doable<Integer,Integer> {
//业务逻辑
@Override
public Integer run(Integer integer) {
return integer*integer*integer;
}
public static void main(String[] args) {
MasterWorker<Integer,Integer> mw=MasterWorkerFactory.newStatic(new Pow3());
List<MasterWorker.ResultKey> keyList=new Vector<>();
for (int i = 0; i <100 ; i++) {
keyList.add(mw.submit(i));
}
mw.execute();
int re=0;
while (keyList.size()>0){
MasterWorker.ResultKey k=keyList.get(0);
Integer i=mw.result(k);
if(i!=null){
re+=i;
keyList.remove(0);
}
}
System.out.println(re);
}
}
7、协程
为了增加系统的并发性,人们提出了线程,随着应用程序日趋复杂,并发量要求越来越高,线程也变得沉重了起来。为了使系统有更高的并行度,便有了协程的概念。如果说线程是轻量级进程,那么协程就是轻量级的线程。
相关框架:kilim。
介绍:略。
二、JVM 调优
1、虚拟机内存模型
1、程序计数器
每一个线程都有一个独立的程序计数器,用于记录下一条要执行的指令,各个线程互不影响。
2、Java 虚拟机栈
它和 Java 线程在同一时间创建,保存方法的局部变量,部分结果,并参与方法的调用和返回。
- 当线程在计算过程中请求的栈深度大于最大可用的栈深度,抛出 StackOverflowError 异常。
- 如果 Java 栈可以可以动态扩展,在扩展的过程中没有足够的空间支持,则抛出 OutOfMemoryError
- -Xss1M 该命令可以调整栈的深度。
public void test28(){ //gc无法回收,因为b还在局部变量表中
{
byte[] b=new byte[6*1024*1024];
}
System.gc();
System.out.println("first explict gc over");
}
public void test29(){ //gc可以回收,因为变量a复用了b的字
{
byte[] b=new byte[6*1024*1024];
}
int a=0;
System.gc();
System.out.println("first explict gc over");
}
3、本地方法栈
本地方法栈和 java 虚拟机栈的功能很像,本地方法栈用于管理本地方法的调用。
4、Java 堆
几乎所有的对象和数组都是在堆中分配空间的。
5、方法区
主要保存类的元数据,所有线程共享的。其中最为重要的是类的类型信息,常量池,静态变量,域信息,方法信息。在 Hot Spot 虚拟机中,方法区也被称为永久区。
2、JVM 内存分配参数
- 最大堆内存:可以用-Xmx 参数指定。是指新生代和老年代的大小之和。
- 最小堆内存:JVM 启动时,所占据的操作系统内存大小,可以用-Xms 指定。
- 设置新生代:新生代一般设置未 1/4 到 1/3 之间。用参数-Xmn 指定
- 持久代(方法区):-XX:PermSize 可以设置初始大小,-XX:MaxPermSize 可以设置最大值
- 线程栈:可以使用-Xss 设置大小。-Xss1M 表示每个线程拥有 1M 的空间。
- 堆的比例分配:-XX:SurvivorRatio:eden/s0=eden/s1。 -XX:NewRatio=老年代/新生代
- 参数总结:
3、垃圾回收
1、引用计数法
如果有引用,计数器就加 1,当引用失效时,计数器就减 1.这种方法无法处理循环引用,不适用与 JVM 的回收。
2、标记-清除算法
第一阶段,标记所有从根节点开始可达的对象。第二阶段,清理所有未标记的对象。缺点是回收后的空间是不连续的。
3、复制算法
将原有的空间分为两块,将正在使用的那个空间的活对象复制到未使用的那个空间,在垃圾回收时,只需要清理正在使用的内存空间的所有对象,然后交换两个内存空间的角色。缺点是要将系统内存折半。
4、标记压缩算法
标记完成之后,将所有的存活对象压缩到内存的一端,然后清除边界外所有的空间。避免了碎片的产生。
5、增量算法
一次性的垃圾清理会造成系统长时间停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。在垃圾回收的过程中间断性的执行应用程序代码。
6、分代
对新生代使用复制算法,老年代使用标记-压缩算法。
7、串行收集器
- -XX:+UseSerialGC 新生代,老年代都使用串行回收器
- -XX:+UseParNewGC 新生代使用并行,老年代使用串行
- -XX:+UseParallelGC 新生代使用并行,老年代使用串行
- -XX:+UseConcMarkSweepGC 新生代使用并行,老年代使用 CMS(-XX:ParallelGCThreads 可以指定线程数量,Cpu 小于 8 时,就设置 cput 的数量,大于 8 时设置为(3+5*cpucount/8))
8、CMS 收集器
使用的是标记清除算法,并行的垃圾收集器
9、G1 收集器
基于标记-压缩算法,目标是一款服务器的收集器,在吞吐量和停顿控制上,要优于 CMS 收集器。
观察 GC 情况:
package com.mmc.concurrentcystudy.test;
import java.util.HashMap;
public class StopWorldTest {
public static class MyThread extends Thread{
HashMap map=new HashMap();
@Override
public void run() {
try{
while (true){
if(map.size()*512/1024/1024>=400){ //防止内存溢出
map.clear();
System.out.println("clean map");
}
byte[] b1;
for (int i = 0; i <100 ; i++) {
b1=new byte[512]; //模拟内存占用
map.put(System.nanoTime(),b1);
}
Thread.sleep(1);
}
}catch (Exception e){}
}
}
public static class PrintThread extends Thread{ //每毫秒打印时间信息
public static final long starttime=System.currentTimeMillis();
@Override
public void run() {
try{
while (true){
long t=System.currentTimeMillis()-starttime;
System.out.println(t/1000+"."+t%1000);
Thread.sleep(100);
}
}catch (Exception e){}
}
}
public static void main(String[] args) {
MyThread t=new MyThread();
PrintThread p=new PrintThread();
t.start();
p.start();
}
}
4、常用调优案例
1、将新对象预留在新生代
避免新对象因空间不够进入了年老代,可以适当增加新生代的 from 大小。
- 可以通过-XX:TargetSurvivorRatio 提高 from 区的利用率
- 通过-XX:SurvivorRatio 设置更大的 from 区
2、大对象直接进入老年代
在大部分情况下,新对象分配在新生代是合理的,但是,对于大对象,很可能扰乱新生代,使得大量小的新生代对象因空间不足移到老年代。
- 使用-XX:PretenureSizeThreshold 设置大对象进入老年代的阈值。当对象大小超过这个值时,对象直接分配在老年代。
3、设置对象进入老年代的年龄
- 可以通过设置-XX:MaxTenuringThreshold 阈值年龄的最大值。
4、稳定与震荡的堆大小
1、一般来说稳定的堆大小是对回收有利的,获得一个稳定的堆大小的方法就是设置-Xms 和-Xmx 的大小一致。稳定的堆空间可以减少 GC 的次数,但是会增加每次 GC 的时间。
基于这样的考虑,JVM 提供了压缩和扩展堆空间的参数。
- -XX:MinHeapFreeRatio 设置堆空间最小空闲比例,默认是 40,当堆空间内存小于这个数值时,JVM 会扩展堆空间。
- -XX:MaxHeapFreeRatio 设置堆空间的最大空闲比例,默认是 70,当堆空间内存大于这个数值时,JVM 会压缩堆空间。
- 注意:当设置-xms 和-xmx 一样时,这两个参数会失效。
5、吞吐量优先案例
在拥有 4G 内存和 32 核 CPU 的计算机上,进行吞吐量的优化
6、使用大页案例
在 Solaris 系统中,可以支持大页的使用,大的内存分页可以增强 CPU 的内存寻址能力。
- -XX:LargePageSizeInBytes:设置大页的大小。
7、降低停顿案例
为了降低停顿,应尽可能将对象预留在新生代,因为新生代的 GC 成本远小于老年代。
5、实用 JVM 参数
1、JIT 编译参数
JIT 编译器可以将字节代码编译成本地代码,从而提高执行效率。
- -XX:CompileThreshold 为 JIT 编译的阈值,当函数的调用次数超过他,JIT 就将字节码编译成本地机器码。
- -XX:+CITime 可以打印出编译的耗时
- -XX:+PrintCompilation 可以打印 JIT 编译信息
2、堆快照
在性能优化中,分析堆快照是必不可少的环节。
- 使用-XX:+HeapDumpOnOutOfMemoryError 参数可以在程序发生 OOM 异常时导出当前堆快照
- 使用-XX:HeapDumpPath 可以指定堆快照保存的位置。(例:
-Xmx20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://log
)
- 使用 Visual VM 工具可以分析堆文件。
3、错误处理
可以在系统发生 OOM 时,运行第三方脚本。如重置系统
- -XX:OnOutOfMemoryError=c:\reset.bat
4、获取 GC 信息
- -XX:+PrintGC
- -XX:+PrintGCDetails 打印更详细的 GC 信息
- -XX:+PrintHeapAtGC 打印堆的使用情况
5、类和对象跟踪
- -XX:+TraceClassLoading 用于跟踪类加载情况
- -XX:+TraceClassUnloading 用于跟踪类卸载情况
- -XX:+PrintClassHistogram 开关打印运行时实例的信息,开关打开后,当按下 Ctrl+Break 时,会打印系统内类的统计信息
6、控制 GC
- -XX:+DisableExplicitGC 用于禁止显示的 GC
- -Xnoclassgc 禁止类的回收
- -Xincgc 增量式的 GC
7、使用大页
- -XX:+UseLargePages 启用大页
- -XX:+LargePageSizeInBytes 指定大页的大小
8、压缩指针
在 64 位虚拟机上,应用程序占的内存要远大于 32 位的,因为 64 位系统拥有更宽的寻址空间,指针对象进行了翻倍。
- +XX:+UseCompressedOops 打开指针压缩,减少内存消耗(但是压缩和解压指针性能会影响)
6、实战案例
1、tomcat 优化
- 在 catalina.bat 中增加打印 GC 日志,以便调试:
set CATALINA_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails
2.当发现产生了 GC 时,看下是否需要增加新生代大小
set CATALINA_OPTS=%CATALINA_OPTS% -Xmx32M -Xms32M
3.如果发现有人使用了显示的 GC 调用,可以禁止掉 set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DesableExplicitGC
4.扩大新生代比例 set CATALINA_OPTS=%CATALINA_OPTS% -XX:NewRatio=2
5.给新生代使用并行回收 set CATALINA_OPTS=%CATALINA_OPTS% -XX:UseParallelGC
6.当确保 class 安全的时候,可以关闭 class 校验 set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none
2、JMeter 介绍和使用
JMeter 是一款性能测试和压力测试工具。
下载路径:https://jmeter.apache.org/download_jmeter.cgi
-
下载解压之后,进入 bin 目录,点击 jmeter.bat 启动。
-
右键添加线程组
-
右键添加请求
-
添加结果视图
这里有很多结果视图,都可以选来试试。
- 点击运行
3、案例
确认堆大小(-Xmx,-Xms),合理分配新生代和老年代(-XX:NewRatio,-Xmn,-XX:SurvivorRatio),确定永久区大小(-XX:PermSize,-XX:MaxPermSize),选择垃圾收集器,除此之外,禁用显示的 GC,禁用类元素回收,禁用类验证对性能也有提升。
实战的调优:
三、Java 性能调优工具
1、Linux 命令行工具
1、top 命令
2、sar 命令
3、vmstat 命令
统计 CPU、内存情况
4、iostat 命令
统计 IO 信息
5、pidstat
可以监控线程的
2、JDK 命令行工具
- jps
- jstat
- jinfo 查看 jvm 参数
- jmap 生成堆快照和对象的统计信息 jmap -histo 2972 >c:/s.txt
生成当前程序的堆快照:jmap -dump:format=b,file=c:\heap.hprof 2972 - jhat 分析堆快照文件
- jstack 导出 java 程序的线程堆栈,可以打印锁的信息 jstack -l 149864>d:/a.txt
- jstatd 有些工具(jps,jstat)可以支持对远程计算机的监控,这就需要 jstatd 的配合
开启 jstatd:
1、在 d 盘新建了个文件 jstatd.all.policy。内容为
grant codebase "file:D:/Java/jdk1.8.0_112/lib/tools.jar" {
permission java.security.AllPermission;
};
2、执行开启
jstatd -J-Djava.security.policy=D:/jstatd.all.policy
3、新开一个 cmd 窗口,执行 jps localhost:1099 即可远程监听
8.hprof 工具,程序运行时加上-agentlib:hprof=cpu=times,interval=10 可以导出函数执行时间。还可以使用-agentlib:hprof=heap=dump,format=b,file=d:/core.hprof 导出文件
3、JConsole 工具
4、Visual VM 工具
1、BTrace 插件,可在不修改代码情况下给代码加日志
/* BTrace Script Template */
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TracingScript {
/* put your code here */
private static long startTime=0;
//方法开始时调用
@OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile") //监控任意类的writeFile方法
public static void startMethod(){
startTime=timeMillis();
}
@OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile",location=@Location(Kind.RETURN)) //方法返回时触发
public static void endM(){
print(strcat(strcat(name(probeClass()),"."),probeMethod()));
print("[");
print(strcat("Time taken:",str(timeMillis()-startTime)));
println("]");
}
}
实例 2:
/* BTrace Script Template */
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TracingScript {
/* put your code here */
@OnMethod(clazz="/.*Test/",location=@Location(value=Kind.LINE,line=27)) //,监控Test结尾的类,指定程序运行到第27行触发
public static void onLine(@ProbeClassName String pcn,@ProbeMethodName String pmn,int line){
print(Strings.strcat(pcn,"."));
print(Strings.strcat(pmn,":"));
println(line);
}
}
实例 3:每秒执行
@BTrace
public class TracingScript {
/* put your code here */
@OnTimer(1000) //每秒钟运行
public static void getUpTime(){
println(Strings.strcat("l000 msec:",Strings.str(Sys.VM.vmUptime()))); //虚拟机启动时间
}
@OnTimer(3000)
public static void getStack(){
jstackAll(); //导出线程堆栈
}
}
实例 4:获取参数
@BTrace
public class TracingScript {
/* put your code here */
@OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile")
public static void any(String filename){
print(filename);
}
}
5、MAT 内存分析工具
1、下载 mat
https://www.eclipse.org/mat/
2、深堆和浅堆
浅堆:一个对象结构所占用的内存大小。
深堆:一个对象被 GC 后,可以真实释放的内存大小
6、JProfile
略