性能調優讀書筆記(下篇)
一、並行程序開發優化
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(Set
s)方法得到一個線程安全的 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 集合:無鎖的線程安全集合框架。包下載地址://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 是一款性能測試和壓力測試工具。
下載路徑://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
//www.eclipse.org/mat/
2、深堆和淺堆
淺堆:一個對象結構所佔用的內存大小。
深堆:一個對象被 GC 後,可以真實釋放的內存大小
6、JProfile
略