性能調優讀書筆記(下篇)

一、並行程序開發優化

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、併發數據結構
  1. 併發 List

    CopyOnWriteArrayList:適用於讀多寫少的場景
  2. 併發 Set

    CopyOnWriteArraySet:適用於讀多寫少的場景,如果有併發寫的情況,也可使用 Collections.synchronizedSet(Set s)方法得到一個線程安全的 Set
  3. 併發 Map
    ConcurrentHashMap
  4. 併發 Queue
    JDK 提供了兩套實現,一套是 ConcurrentLinkedQueue 爲代表的高性能隊列,一個是以 BlockingQueue 接口爲代表的阻塞隊列
  5. 併發 Deque(雙端隊列)
    LinkedBlockingDeque
6、鎖的性能優化
  1. 避免死鎖
  2. 減小鎖的作用範圍
  3. 減小鎖的粒度
  4. 讀寫分離鎖代替獨佔鎖
  5. 鎖分離
  6. 重入鎖(ReentrantLock)和內部鎖(Synchronized)
  7. 鎖粗化:不斷地獲取和釋放鎖也很耗資源,比如在 for 循環裏使用 synchronized
  8. 自旋鎖:線程沒有取得鎖時不被掛起,轉而去執行一個空循環。
  9. 鎖消除: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();
    }
}

  1. 鎖偏向:如果程序沒有競爭,則取消之前獲得鎖的線程的同步操作。通過設置-XX:+UseBiasedLocking 可以設置啓用偏向鎖
  2. 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 內存分配參數
  1. 最大堆內存:可以用-Xmx 參數指定。是指新生代和老年代的大小之和。
  2. 最小堆內存:JVM 啓動時,所佔據的操作系統內存大小,可以用-Xms 指定。
  3. 設置新生代:新生代一般設置未 1/4 到 1/3 之間。用參數-Xmn 指定
  4. 持久代(方法區):-XX:PermSize 可以設置初始大小,-XX:MaxPermSize 可以設置最大值
  5. 線程棧:可以使用-Xss 設置大小。-Xss1M 表示每個線程擁有 1M 的空間。
  6. 堆的比例分配:-XX:SurvivorRatio:eden/s0=eden/s1。 -XX:NewRatio=老年代/新生代
  7. 參數總結:
3、垃圾回收

1、引用計數法

如果有引用,計數器就加 1,當引用失效時,計數器就減 1.這種方法無法處理循環引用,不適用與 JVM 的回收。

2、標記-清除算法

第一階段,標記所有從根節點開始可達的對象。第二階段,清理所有未標記的對象。缺點是回收後的空間是不連續的。

3、複製算法

將原有的空間分爲兩塊,將正在使用的那個空間的活對象複製到未使用的那個空間,在垃圾回收時,只需要清理正在使用的內存空間的所有對象,然後交換兩個內存空間的角色。缺點是要將系統內存摺半。

4、標記壓縮算法

標記完成之後,將所有的存活對象壓縮到內存的一端,然後清除邊界外所有的空間。避免了碎片的產生。

5、增量算法

一次性的垃圾清理會造成系統長時間停頓,那麼就可以讓垃圾收集線程和應用程序線程交替執行。在垃圾回收的過程中間斷性的執行應用程序代碼。

6、分代

對新生代使用複製算法,老年代使用標記-壓縮算法。

7、串行收集器

  1. -XX:+UseSerialGC 新生代,老年代都使用串行回收器
  2. -XX:+UseParNewGC 新生代使用並行,老年代使用串行
  3. -XX:+UseParallelGC 新生代使用並行,老年代使用串行
  4. -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 大小。

  1. 可以通過-XX:TargetSurvivorRatio 提高 from 區的利用率
  2. 通過-XX:SurvivorRatio 設置更大的 from 區

2、大對象直接進入老年代

在大部分情況下,新對象分配在新生代是合理的,但是,對於大對象,很可能擾亂新生代,使得大量小的新生代對象因空間不足移到老年代。

  1. 使用-XX:PretenureSizeThreshold 設置大對象進入老年代的閾值。當對象大小超過這個值時,對象直接分配在老年代。

3、設置對象進入老年代的年齡

  1. 可以通過設置-XX:MaxTenuringThreshold 閾值年齡的最大值。

4、穩定與震盪的堆大小

1、一般來說穩定的堆大小是對回收有利的,獲得一個穩定的堆大小的方法就是設置-Xms 和-Xmx 的大小一致。穩定的堆空間可以減少 GC 的次數,但是會增加每次 GC 的時間。
基於這樣的考慮,JVM 提供了壓縮和擴展堆空間的參數。

  1. -XX:MinHeapFreeRatio 設置堆空間最小空閒比例,默認是 40,當堆空間內存小於這個數值時,JVM 會擴展堆空間。
  2. -XX:MaxHeapFreeRatio 設置堆空間的最大空閒比例,默認是 70,當堆空間內存大於這個數值時,JVM 會壓縮堆空間。
  3. 注意:當設置-xms 和-xmx 一樣時,這兩個參數會失效。

5、吞吐量優先案例

在擁有 4G 內存和 32 核 CPU 的計算機上,進行吞吐量的優化

6、使用大頁案例

在 Solaris 系統中,可以支持大頁的使用,大的內存分頁可以增強 CPU 的內存尋址能力。

  1. -XX:LargePageSizeInBytes:設置大頁的大小。

7、降低停頓案例

爲了降低停頓,應儘可能將對象預留在新生代,因爲新生代的 GC 成本遠小於老年代。

5、實用 JVM 參數

1、JIT 編譯參數

JIT 編譯器可以將字節代碼編譯成本地代碼,從而提高執行效率。

  1. -XX:CompileThreshold 爲 JIT 編譯的閾值,當函數的調用次數超過他,JIT 就將字節碼編譯成本地機器碼。
  2. -XX:+CITime 可以打印出編譯的耗時
  3. -XX:+PrintCompilation 可以打印 JIT 編譯信息

2、堆快照

在性能優化中,分析堆快照是必不可少的環節。

  1. 使用-XX:+HeapDumpOnOutOfMemoryError 參數可以在程序發生 OOM 異常時導出當前堆快照
  2. 使用-XX:HeapDumpPath 可以指定堆快照保存的位置。(例:
-Xmx20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://log

  1. 使用 Visual VM 工具可以分析堆文件。

3、錯誤處理

可以在系統發生 OOM 時,運行第三方腳本。如重置系統

  1. -XX:OnOutOfMemoryError=c:\reset.bat

4、獲取 GC 信息

  1. -XX:+PrintGC
  2. -XX:+PrintGCDetails 打印更詳細的 GC 信息
  3. -XX:+PrintHeapAtGC 打印堆的使用情況

5、類和對象跟蹤

  1. -XX:+TraceClassLoading 用於跟蹤類加載情況
  2. -XX:+TraceClassUnloading 用於跟蹤類卸載情況
  3. -XX:+PrintClassHistogram 開關打印運行時實例的信息,開關打開後,當按下 Ctrl+Break 時,會打印系統內類的統計信息

6、控制 GC

  1. -XX:+DisableExplicitGC 用於禁止顯示的 GC
  2. -Xnoclassgc 禁止類的回收
  3. -Xincgc 增量式的 GC

7、使用大頁

  1. -XX:+UseLargePages 啓用大頁
  2. -XX:+LargePageSizeInBytes 指定大頁的大小

8、壓縮指針

在 64 位虛擬機上,應用程序佔的內存要遠大於 32 位的,因爲 64 位系統擁有更寬的尋址空間,指針對象進行了翻倍。

  1. +XX:+UseCompressedOops 打開指針壓縮,減少內存消耗(但是壓縮和解壓指針性能會影響)
6、實戰案例

1、tomcat 優化

  1. 在 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

  1. 下載解壓之後,進入 bin 目錄,點擊 jmeter.bat 啓動。

  2. 右鍵添加線程組

  3. 右鍵添加請求

  4. 添加結果視圖

這裏有很多結果視圖,都可以選來試試。

  1. 點擊運行

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 命令行工具
  1. jps
  2. jstat
  3. jinfo 查看 jvm 參數
  4. jmap 生成堆快照和對象的統計信息 jmap -histo 2972 >c:/s.txt
    生成當前程序的堆快照:jmap -dump:format=b,file=c:\heap.hprof 2972
  5. jhat 分析堆快照文件
  6. jstack 導出 java 程序的線程堆棧,可以打印鎖的信息 jstack -l 149864>d:/a.txt
  7. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章