阿里不容許使用 Executors 建立線程池!那怎麼使用,怎麼監控?


做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄java

1、前言

五常大米好吃!ios

哈哈哈,是不你總買五常大米,其實五常和榆樹是挨着的,榆樹大米也好吃,榆樹仍是天下第一糧倉呢!可是五常出名,因此只認識五常。c++

爲何提這個呢,由於阿里不容許使用 Executors 建立線程池!其餘不少大廠也不容許,這麼建立的話,控制很差會出現OOM。git

,本篇就帶你學習四種線程池的不一樣使用方式、業務場景應用以及如何監控線程。github

2、面試題

謝飛機,小記!,上次從面試官那逃跑後,惡補了多線程,本身好像也內捲了,因此出門逛逛!面試

面試官:嗨,飛機,飛機,這邊!spring

謝飛機:嗯?!哎呀,面試官你咋來南海子公園了?編程

面試官:我家就附近,跑步來了。最近你咋樣,上次問你的多線程學了嗎?緩存

謝飛機:哎,看了是看了,記不住鴨!

面試官:嗯,不經常使用確實記不住。不過你能夠選擇跳槽,來大廠,大廠的業務體量較大!

謝飛機:我就糾結呢,想回家考教師資格證了,咱們村小學要教java了!

面試官:哈哈哈哈哈,一塊兒!

3、四種線程池使用介紹

Executors 是建立線程池的工具類,比較典型常見的四種線程池包括:newFixedThreadPoolnewSingleThreadExecutornewCachedThreadPoolnewScheduledThreadPool。每一種都有本身特定的典型例子,能夠按照每種的特性用在不一樣的業務場景,也能夠作爲參照精細化建立線程池。

1. newFixedThreadPool

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
}

// 測試結果
23:48:24.628 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 1 次執行完成
23:48:24.628 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 1 次執行完成
23:48:24.628 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 1 次執行完成
23:48:25.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 2 次執行完成
23:48:25.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 2 次執行完成
23:48:25.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 2 次執行完成
23:48:26.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 3 次執行完成
23:48:26.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 3 次執行完成
23:48:26.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 3 次執行完成
23:48:27.634 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 4 次執行完成
23:48:27.634 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 4 次執行完成
23:48:27.634 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 4 次執行完成
23:48:28.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 1 次執行完成
23:48:29.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 2 次執行完成
23:48:30.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 3 次執行完成
23:48:31.636 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 4 次執行完成

Process finished with exit code 0

圖解

圖 22-1 newFixedThreadPool 執行過程

  • 代碼new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
  • 介紹:建立一個固定大小可重複使用的線程池,以 LinkedBlockingQueue 無界阻塞隊列存放等待線程。
  • 風險:隨着線程任務不能被執行的的無限堆積,可能會致使OOM。

2. newSingleThreadExecutor

public static void main(String[] args) {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
}

// 測試結果
23:20:15.066 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 1 次執行完成
23:20:16.069 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 2 次執行完成
23:20:17.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 3 次執行完成
23:20:18.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 4 次執行完成
23:20:19.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 1 次執行完成
23:23:280.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 2 次執行完成
23:23:281.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 3 次執行完成
23:23:282.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 4 次執行完成
23:23:283.073 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 1 次執行完成
23:23:284.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 2 次執行完成
23:23:285.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 3 次執行完成
23:23:286.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 4 次執行完成
23:23:287.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 1 次執行完成
23:23:288.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 2 次執行完成
23:23:289.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 3 次執行完成
23:20:30.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 4 次執行完成

圖解

圖 22-2 newSingleThreadExecutor 執行過程

  • 代碼new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
  • 介紹:只建立一個執行線程任務的線程池,若是出現意外終止則再建立一個。
  • 風險:一樣這也是一個無界隊列存放待執行線程,無限堆積下會出現OOM。

3. newCachedThreadPool

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
    
    // 測試結果
    23:25:59.818 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 1 次執行完成
    23:25:00.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 2 次執行完成
    23:25:01.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 3 次執行完成
    23:25:01.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 3 次執行完成
    23:25:01.824 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 3 次執行完成
    23:25:01.824 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 3 次執行完成
    23:25:02.824 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 4 次執行完成
    23:25:02.824 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 4 次執行完成
    23:25:02.825 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 4 次執行完成
    23:25:02.825 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 4 次執行完成
}

圖解

圖 22-3 newCachedThreadPool 執行過程

  • 代碼new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
  • 介紹:首先 SynchronousQueue 是一個生產消費模式的阻塞任務隊列,只要有任務就須要有線程執行,線程池中的線程能夠重複使用。
  • 風險:若是線程任務比較耗時,又大量建立,會致使OOM

4. newScheduledThreadPool

public static void main(String[] args) {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    executorService.schedule(() -> {
        logger.info("3秒後開始執行");
    }, 3, TimeUnit.SECONDS);
    executorService.scheduleAtFixedRate(() -> {
        logger.info("3秒後開始執行,之後每2秒執行一次");
    }, 3, 2, TimeUnit.SECONDS);
    executorService.scheduleWithFixedDelay(() -> {
        logger.info("3秒後開始執行,後續延遲2秒");
    }, 3, 2, TimeUnit.SECONDS);
}

// 測試結果
23:28:32.442 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行
23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,之後每2秒執行一次
23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒
23:28:34.441 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,之後每2秒執行一次
23:28:34.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒
23:28:36.440 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,之後每2秒執行一次
23:28:36.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒

圖解

![圖 22-4 newScheduledThreadPool 執行過程

  • 代碼public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()); }
  • 介紹:這就是一個比較有意思的線程池了,它能夠延遲定時執行,有點像咱們的定時任務。一樣它也是一個無限大小的線程池 Integer.MAX_VALUE。它提供的調用方法比較多,包括:scheduleAtFixedRatescheduleWithFixedDelay,能夠按需選擇延遲執行方式。
  • 風險:一樣因爲這是一組無限容量的線程池,因此依舊又OOM風險。

4、線程池使用場景說明

何時使用線程池?

說簡單是當爲了給老闆省錢的時候,由於使用線程池能夠下降服務器資源的投入,讓每臺機器儘量更大限度的使用CPU。

😄那你這麼說確定沒辦法升職加薪了!

因此若是說的高大上一點,那麼是在符合科特爾法則阿姆達爾定律 的狀況下,引入線程池的使用最爲合理。啥意思呢,還得簡單說!

假如:咱們有一套電商服務,用戶瀏覽商品的併發訪問速率是:1000客戶/每分鐘,平均每一個客戶在服務器上的耗時0.5分鐘。根據利特爾法則,在任什麼時候刻,服務端都承擔着1000*0.5=500個客戶的業務處理量。過段時間大促了,併發訪問的用戶擴了一倍2000客戶了,那怎麼保障服務性能呢?

  1. 提升服務器併發處理的業務量,即提升到2000×0.5=1000
  2. 減小服務器平均處理客戶請求的時間,即減小到:2000×0.25=500

因此:在有些場景下會把串行的請求接口,壓縮成並行執行,如圖 22-5

圖22-5 多線程接口查詢使用

可是,線程池的使用會隨着業務場景變化而不一樣,若是你的業務須要大量的使用線程池,並不是常依賴線程池,那麼就不可能用 Executors 工具類中提供的方法。由於這些線程池的建立都不夠精細化,也很是容易形成OOM風險,並且隨着業務場景邏輯不一樣,會有IO密集型和CPU密集型。

最終,你們使用的線程池都是使用 new ThreadPoolExecutor() 建立的,固然也有基於Spring的線程池配置 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

可你想過嗎,一樣一個接口在有活動時候怎麼辦、有大促時候怎麼辦,可能你當時設置的線程池是合理的,可是一到流量很是大的時候就很不適合了,因此若是能動態調整線程池就很是有必要了。並且使用 new ThreadPoolExecutor() 方式建立的線程池是能夠經過提供的 set 方法進行動態調整的。有了這個動態調整的方法後,就能夠把線程池包裝起來,在配合動態調整的頁面,動態更新線程池參數,就能夠很是方便的調整線程池了。

5、獲取線程池監控信息

你收過報警短信嗎?

收過,半夜還有報警機器人打電話呢!崴,你的系統有個機器睡着了,快起來看看!!!

因此,若是你高頻、高依賴線程池,那麼有一個完整的監控系統,就非重要了。總不能線上掛了,你還不知道!

可監控內容

方法 含義
getActiveCount() 線程池中正在執行任務的線程數量
getCompletedTaskCount() 線程池已完成的任務數量,該值小於等於taskCount
getCorePoolSize() 線程池的核心線程數量
getLargestPoolSize() 線程池曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否滿過,也就是達到了maximumPoolSize
getMaximumPoolSize() 線程池的最大線程數量
getPoolSize() 線程池當前的線程數量
getTaskCount() 線程池已經執行的和未執行的任務總數

1. 重寫線程池方式監控

若是咱們想監控一個線程池的方法執行動做,最簡單的方式就是繼承這個類,重寫方法,在方法中添加動做收集信息。

僞代碼

public class ThreadPoolMonitor extends ThreadPoolExecutor {

    @Override
    public void shutdown() {
        // 統計已執行任務、正在執行任務、未執行任務數量
        super.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        // 統計已執行任務、正在執行任務、未執行任務數量
        return super.shutdownNow();
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        // 記錄開始時間
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 記錄完成耗時
    }
    
    ...
}

2. 基於IVMTI方式監控

這塊是監控的重點,由於咱們不太可能讓每個須要監控的線程池都來重寫的方式記錄,這樣的改形成本過高了。

那麼除了這個笨方法外,能夠選擇使用基於JVMTI的方式,進行開發監控組件。

JVMTI:JVMTI(JVM Tool Interface)位於jpda最底層,是Java虛擬機所提供的native編程接口。JVMTI能夠提供性能分析、debug、內存管理、線程分析等功能。

基於jvmti提供的接口服務,運用C++代碼(win32-add_library)在Agent_OnLoad裏開發監控服務,並生成dll文件。開發完成後在java代碼中加入agentpath,這樣就能夠監控到咱們須要的信息內容。

環境準備

  1. Dev-C++
  2. JetBrains CLion 2018.2.3
  3. IntelliJ IDEA Community Edition 2018.3.1 x64
  4. jdk1.8.0_45 64位
  5. jvmti(在jdk安裝目錄下jdk1.8.0_45\include裏,把include整個文件夾複製到和工程案例同層級目錄下,便於 include 引用)

配置信息:(路徑相關修改成本身的)

  1. C++開發工具Clion配置
    1.配置位置;Settings->Build,Execution,Deployment->Toolchains
    1. MinGM配置:D:\Program Files (x86)\Dev-Cpp\MinGW64
  2. java調試時配置
    1. 配置位置:Run/Debug Configurations ->VM options
    2. 配置內容:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

2.1 先作一個監控例子

Java工程

public class TestLocationException {

    public static void main(String[] args) {
        Logger logger = Logger.getLogger("TestLocationException");
        try {
            PartnerEggResourceImpl resource = new PartnerEggResourceImpl();
            Object obj = resource.queryUserInfoById(null);
            logger.info("測試結果:" + obj);
        } catch (Exception e) {
            //屏蔽異常
 }
    }
}

class PartnerEggResourceImpl {
    Logger logger = Logger.getLogger("PartnerEggResourceImpl");
    public Object queryUserInfoById(String userId) {
        logger.info("根據用戶Id獲取用戶信息" + userId);
        if (null == userId) {
            throw new NullPointerException("根據用戶Id獲取用戶信息,空指針異常");
        }
        return userId;
    }
}

c++監控

#include <iostream>
#include <cstring>
#include "jvmti.h"

using namespace std;

//異常回調函數
static void JNICALL
callbackException(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID methodId, jlocation location,
jobject exception, jmethodID catch_method, jlocation catch_location) {
// 得到方法對應的類
jclass clazz;
jvmti_env->GetMethodDeclaringClass(methodId, &clazz);

// 得到類的簽名
char *class_signature;
jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);

//過濾非本工程類信息
string::size_type idx;
string class_signature_str = class_signature;
idx = class_signature_str.find("org/itstack");
if (idx != 1) {
return;
}

//異常類名稱
char *exception_class_name;
jclass exception_class = env->GetObjectClass(exception);
jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);

// 得到方法名稱
char *method_name_ptr, *method_signature_ptr;
jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);

//獲取目標方法的起止地址和結束地址
jlocation start_location_ptr;    //方法的起始位置
jlocation end_location_ptr;      //用於方法的結束位置
jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);

//輸出測試結果
cout << "測試結果 - 定位類的簽名:" << class_signature << endl;
cout << "測試結果 - 定位方法信息:" << method_name_ptr << " -> " << method_signature_ptr << endl;
cout << "測試結果 - 定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
cout << "測試結果 - 異常類的名稱:" << exception_class_name << endl;

cout << "測試結果-輸出異常信息(能夠分析行號):" << endl;
jclass throwable_class = (*env).FindClass("java/lang/Throwable");
jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
(*env).CallVoidMethod(exception, print_method);

}


JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *gb_jvmti = nullptr;
    //初始化
    vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
    // 建立一個新的環境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_signal_thread = 1;
    caps.can_get_owned_monitor_info = 1;
    caps.can_generate_method_entry_events = 1;
    caps.can_generate_exception_events = 1;
    caps.can_generate_vm_object_alloc_events = 1;
    caps.can_tag_objects = 1;
    // 設置當前環境
    gb_jvmti->AddCapabilities(&caps);
    // 建立一個新的回調函數
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    //異常回調
    callbacks.Exception = &callbackException;
    // 設置回調函數
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    // 開啓事件監聽(JVMTI_EVENT_EXCEPTION)
    gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
    return JNI_OK;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
}

測試結果

在 VM vptions 中配置:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

十二月 16, 2020 23:53:27 下午 org.itstack.demo.PartnerEggResourceImpl queryUserInfoById
信息: 根據用戶Id獲取用戶信息null
java.lang.NullPointerException: 根據用戶Id獲取用戶信息,空指針異常
	at org.itstack.demo.PartnerEggResourceImpl.queryUserInfoById(TestLocationException.java:26)
	at org.itstack.demo.TestLocationException.main(TestLocationException.java:13)
測試結果-定位類的簽名:Lorg/itstack/demo/PartnerEggResourceImpl;
測試結果-定位方法信息:queryUserInfoById -> (Ljava/lang/String;)Ljava/lang/Object;
測試結果-定位方法位置:0 -> 43
測試結果-異常類的名稱:Ljava/lang/NullPointerException;
測試結果-輸出異常信息(能夠分析行號):
  • 這就是基於JVMTI的方式進行監控,這樣的方式能夠作到非入侵代碼。不須要硬編碼,也就節省了人力,不然全部人都會進行開發監控內容,而這部份內容與業務邏輯並沒有關係。

2.2 擴展線程監控

其實方法差很少,都是基於C++開發DLL文件,引入使用。不過這部分代碼會監控方法信息,並採集線程的執行內容。

static void JNICALL callbackMethodEntry(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID method) {
    // 得到方法對應的類
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(method, &clazz);

    // 得到類的簽名
    char *class_signature;
    jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);

    //過濾非本工程類信息
    string::size_type idx;
    string class_signature_str = class_signature;
    idx = class_signature_str.find("org/itstack");

    gb_jvmti->RawMonitorEnter(gb_lock);

    {
        //must be deallocate
        char *name = NULL, *sig = NULL, *gsig = NULL;
        jint thr_hash_code = 0;

        error = gb_jvmti->GetMethodName(method, &name, &sig, &gsig);
        error = gb_jvmti->GetObjectHashCode(thr, &thr_hash_code);

        if (strcmp(name, "start") == 0 || strcmp(name, "interrupt") == 0 ||
            strcmp(name, "join") == 0 || strcmp(name, "stop") == 0 ||
            strcmp(name, "suspend") == 0 || strcmp(name, "resume") == 0) {

            //must be deallocate
            jobject thd_ptr = NULL;
            jint hash_code = 0;
            gb_jvmti->GetLocalObject(thr, 0, 0, &thd_ptr);
            gb_jvmti->GetObjectHashCode(thd_ptr, &hash_code);

            printf("[線程監控]: thread (%10d) %10s (%10d)\n", thr_hash_code, name, hash_code);
        }
    }

    gb_jvmti->RawMonitorExit(gb_lock);
}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {

    // 初始化
    jvm->GetEnv((void **) &gb_jvmti, JVMTI_VERSION_1_0);
    // 建立一個新的環境
    memset(&gb_capa, 0, sizeof(jvmtiCapabilities));
    gb_capa.can_signal_thread = 1;
    gb_capa.can_get_owned_monitor_info = 1;
    gb_capa.can_generate_method_exit_events = 1;
    gb_capa.can_generate_method_entry_events = 1;
    gb_capa.can_generate_exception_events = 1;
    gb_capa.can_generate_vm_object_alloc_events = 1;
    gb_capa.can_tag_objects = 1;
    gb_capa.can_generate_all_class_hook_events = 1;
    gb_capa.can_generate_native_method_bind_events = 1;
    gb_capa.can_access_local_variables = 1;
    gb_capa.can_get_monitor_info = 1;
    // 設置當前環境
    gb_jvmti->AddCapabilities(&gb_capa);
    // 建立一個新的回調函數
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(jvmtiEventCallbacks));
    // 方法回調
    callbacks.MethodEntry = &callbackMethodEntry;
    // 設置回調函數
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));

    gb_jvmti->CreateRawMonitor("XFG", &gb_lock);

    // 註冊事件監聽(JVMTI_EVENT_VM_INIT、JVMTI_EVENT_EXCEPTION、JVMTI_EVENT_NATIVE_METHOD_BIND、JVMTI_EVENT_CLASS_FILE_LOAD_HOOK、JVMTI_EVENT_METHOD_ENTRY、JVMTI_EVENT_METHOD_EXIT)
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_NATIVE_METHOD_BIND, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, (jthread) NULL);

    return JNI_OK;
}
  • 從監控的代碼能夠看到,這裏有線程的 start、stop、join、interrupt 等,並能夠記錄執行信息。
  • 另外這裏監控的方法執行回調,SetEventCallbacks(&callbacks, sizeof(callbacks)); 以及相應事件的添加。

6、總結

  • 若是說你所經歷的業務體量很小,那麼幾乎並不須要如此複雜的技術棧深度學習,甚至幾乎不須要擴展各種功能,也不須要監控。但終究有一些須要造飛機的大廠,他們的業務體量龐大,併發數高,讓本來可能就是一個簡單的查詢接口,也要作熔斷、降級、限流、緩存、線程、異步、預熱等等操做。
  • 知其然纔敢用,若是對一個技術點不是太熟悉,就不要胡亂使用,不然遇到的OOM並非那麼好復現,尤爲是在併發場景下。固然若是大家技術體系中有各類服務,好比流量復現、鏈路追蹤等等,那麼還好。
  • 又扯到了這,一個堅持學習、分享、沉澱的男人!好了,若是有錯字、內容不許確,歡迎直接懟給我,我喜歡接受。但不要欺負我哦哈哈哈哈哈!

7、系列推薦

相關文章
相關標籤/搜索