經典 OOM 問題|pthread_create

1、背景

近期版本上線後收到很多用戶反饋(大可能是華爲用戶)崩潰,日誌上整體表現爲 pthread_create (1040KB stack) failed: XXX。html

java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory 1 java.lang.Thread.nativeCreate(Native Method) 2 java.lang.Thread.start(Thread.java:743) 3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941) 4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009) 5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151) 6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 7 java.lang.Thread.run(Thread.java:774) 複製代碼
java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Try again 1 java.lang.Thread.nativeCreate(Native Method) 2 java.lang.Thread.start(Thread.java:733) 3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975) 4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043) 5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185) 6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 7 java.lang.Thread.run(Thread.java:764) 複製代碼

2、問題分析

2.1 初步推斷

Android的內存管理策略

OOM 並不等於 RAM 不足,這和 Android 的內存管理策略有關。java

咱們知道,內存分爲虛擬地址和物理地址。經過 malloc 或 new 分配的內存都是虛擬地址空間的內存。虛擬地址空間比物理的地址空間要大的多。在較多進程同時運行時,物理地址空間有可能不夠,這該怎麼辦?android

Linux 採用的是 「進程內存最大化」 的分配策略,用 Swap 機制來保證物理內存不被消耗殆盡,把最近最少使用的空間騰到外部存儲空間上,僞裝仍是存儲在 RAM 裏。c++

雖然 Android 基於 Linux,可是在內存策略上有本身的套路 —— 沒有交換區。git

Android 的進程分配策略是每一個進程都有一個內存佔用限制,這個具體大小由手機具體配置決定。目的就是爲了讓更多的進程都保留在 RAM 中,這樣每一個進程被喚起的時候能夠避免外部存儲到內部存儲的數據讀寫的消耗,加快更多的 App 恢復的響應速度,也避免了流氓 App 搶佔全部內存。隨之而然,Android 採用了本身的 LowMemoryKill 策略來控制RAM中的進程。若是 RAM 真的不足,MemoryKiller 就會殺死一些優先級比較低的進程來釋放物理內存。github

因此觸發OOM,只多是使用的虛擬內存地址空間超過度配的閾值。shell

那 Android 爲每一個應用分配多少內存呢?這個因手機而異,以手頭的測試機舉例,系統正常分配的內存最多爲 192 M;當設置 largeHeap 時,最多可申請 512M。編程

2.2 代碼分析

那這個溢出是怎麼被系統拋出的?經過 Android 源碼能夠看到,是由 runtime/thread.cc內拋出的異常。c#

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) 複製代碼

線程建立有如下兩個關鍵的步驟:數組

  • 第一列中的建立線程私有的結構體JNIENV(JNI執行環境,用於C層調用Java層代碼)
  • 第二列中的調用posix C庫的函數pthread_create進行線程建立工做

而這兩步均有可能拋出OOM,基本定位 —— 建立線程致使了OOM

Android 建立線程源碼與OOM分析 該文分析了建立線程的原理,其實就是調用mmap分配棧內存(虛擬內存),再經過 Linux 的 mmap 調用映射到用戶態虛擬內存地址空間。建立線程過程當中發生OOM是由於進程內的虛擬內存地址空間耗盡了。

那何時會虛擬內存地址空間不足呢 ?

方向一:fd 過多

Linux 系統中一切皆文件,網絡是文件,打開文件、新建 tcp 鏈接也是文件,都會佔用 fd。fd是一種資源,是資源就會有限制。每一個進程最大打開文件的數目有一個上限。

而fd的增長的時機有:

  • 建立socket網絡鏈接
  • 打開文件
  • 建立HandlerThread
  • 建立NIO的Channel(讀寫各佔用一個fd)
  • 經過命令:ls -l /proc//fd/ 來查看某個進程打開了哪些文件
  • cat /proc/<pid>/limits 命令查看進程的fd限制,或其它限制 如Max open files
  • lsof -p <pid> |wc -l 查看進程全部的fd總數

如上圖,Max open files表示每一個進程最大打開文件的數目,進程每打開一個文件就會產生一個文件描述符fd(記錄在/proc/pid/fd中)

驗證也很簡單,經過觸發大量的網絡鏈接或者文件打開,每打開一個 socket 都會增長一個 fd。

private Runnable increaseFDRunnable = new Runnable() {
      @Override
      public void run() {
          try {
              for (int i = 0; i < 1000; i++) {
                  new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status"));
              }
              Thread.sleep(Long.MAX_VALUE);
          } catch (InterruptedException e) {
              //
          } catch (FileNotFoundException e) {
              //
          }
      }
  };
複製代碼
方向二:線程過多

已用邏輯空間地址能夠查看 /proc//status 中的 VmPeak / VmSize

無非兩個緣由: 一、進程的棧內存超過了虛擬機的最大內存數; 二、線程數達到了系統最大限制數;

排查工具

  • profilter CPU 查看當前全部線程列表

  • 使用CPU分析器監視CPU使用狀況和線程活動使用過重了?沒法分類統計?可使用 adb shell ps -T -p ,還可使用 | grep xxx 過濾,使用wc -l來統計線程數量。

  • 直接dump進程內存,來查看內存狀況:

    adb shell dumpsys meminfo [pacakgename]
    複製代碼
  • 也能夠查看線程等彙總數據:

    adb shell
    cat /proc/19468/status
    複製代碼

Linux在 /proc/sys/kernel/threads-max 中有描述線程限制,能夠經過命令cat /proc/sys/kernel/threads-max 查看,華爲在線程限制上很是嚴苛,在 7.0+ 手機上已將最大線程數修改爲了 500。

那麼是哪裏代碼致使了線程爆發呢?咱們使用 watch1s打印一下當前的線程數再經過頁面交互來定位問題,觀察看看哪類的線程名字在增多。

watch -n 1 -d 'adb shell ps -T | grep XXX | wc -l'
複製代碼

觀察後發現,線程總數在進入直播間時,垂手可得就達到了 290多,並且有大量 RxCachedThreadSchedule 線程(也就是 Rx 的Scheduler.io調度器)被建立,IO線程數暴漲到 46。停留在直播間一段時間,線程數只增不減,並不會過時清理。

2.3 驗證推斷,定位緣由

寫個demo來驗證,用 Kotlin 協程和 RxJava IO 調度器,模擬密集併發IO的環境

for (i in 0..100) {
            GlobalScope.launch(Dispatchers.IO) {
                delay(100)
                Log.e("IOExecute", "協程 - 當前線程:"
                      + Thread.currentThread().name)
            }
        }
複製代碼

for (i in 0..100) {
            ThreadExecutor.IO.execute {
                Thread.sleep(100)
                Log.e("IOExecute", "RxJava IO - 當前線程:"
                      + Thread.currentThread().name)
            }
        }
複製代碼

看起來 IO 線程沒有複用,有點奇怪,咱們知道 Rx 的調度器其實就是封裝的線程池,咱們也早已對線程池的流程倒背如流。以下圖:

難道是工做隊列滿了?難道是線程無上限?難道是飽和策略有問題?

疑點

  1. 初進直播間,密集IO,沒有複用,線程突增

  2. 停留超過 keepAliveTime,IO線程沒有銷燬

源碼探尋

那到底哪裏出了問題呢?本着挖掘機專業畢業的精神,咱們來看看Scheduler.io的源碼定位緣由,看源碼前,咱們先提出疑問和設想,帶着問題看源碼纔不容易迷失方向:

疑問

  • 工做隊列是怎麼管理的,容量多大?
  • 線程池策略是什麼?何時新建線程?何時銷燬?

咱們先來看看 RxJava 線程模型圖,理清楚類之間的關係:Scheduler 是 RxJava 的線程任務調度器,Worker 是線程任務的具體執行者。不一樣的Scheduler類會有不一樣的Worker實現,由於Scheduler類最終是交到Worker中去執行調度的。

能夠看到,Schedulers.io()中使用了靜態內部類的方式來建立出了一個單例IoScheduler對象出來,這個IoScheduler是繼承自Scheduler的。

@NonNull
static final Scheduler IO;

@NonNull
public static Scheduler io() {
    //1.直接返回一個名爲IO的Scheduler對象
    return RxJavaPlugins.onIoScheduler(IO);
}

static {
    //省略無關代碼
    
    //2.IO對象是在靜態代碼塊中實例化的,這裏會建立按一個IOTask()
    IO = RxJavaPlugins.initIoScheduler(new IOTask());
}

static final class IOTask implements Callable<Scheduler> {
    @Override
    public Scheduler call() throws Exception {
        //3.IOTask中會返回一個IoHolder對象
        return IoHolder.DEFAULT;
    }
}

static final class IoHolder {
    //4.IoHolder中會就是new一個IoScheduler對象出來
    static final Scheduler DEFAULT = new IoScheduler();
}
複製代碼

IoScheduler 的父類 Scheduler 在 scheduleDirect()、schedulePeriodicallyDirect() 方法中建立了 Worker,而後會分別調用 worker 的 schedule()、schedulePeriodically() 來執行任務。

public abstract class Scheduler {
    
    //檢索或建立一個表明操做串行執行的新{@link Scheduler.Worker}。工做完成後,應使用{@link Scheduler.Worker#dispose()}取消訂閱。 return一個Worker,它表明要執行的一系列動做。
    @NonNull
    public abstract Worker createWorker();

    @NonNull
    public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
        final Worker w = createWorker();

        final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

        DisposeTask task = new DisposeTask(decoratedRun, w);

        w.schedule(task, delay, unit);

        return task;
    }

    @NonNull
    public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, @NonNull TimeUnit unit) {
        final Worker w = createWorker();
        //省略無關代碼
        Disposable d = w.schedulePeriodically(periodicTask, initialDelay, period, unit);
        //省略無關代碼
    }
}

複製代碼

前面咱們說到,不一樣的Scheduler類會有不一樣的Worker實現,咱們看看 IoScheduler 這個實現類對應的 Worker 是什麼:

final AtomicReference<CachedWorkerPool> pool;

public Worker createWorker() {
    //就是new一個EventLoopWorker,傳一個 CachedWorkerPool 對象(Worker緩存池)
    return new EventLoopWorker(pool.get());
}

static final class EventLoopWorker extends Scheduler.Worker {
    private final CompositeDisposable tasks;
    private final CachedWorkerPool pool;
    private final ThreadWorker threadWorker;

    final AtomicBoolean once = new AtomicBoolean();
    
    //構造方法
    EventLoopWorker(CachedWorkerPool pool) {
        this.pool = pool;
        this.tasks = new CompositeDisposable();
        //從緩存Worker池中取一個Worker出來
        this.threadWorker = pool.get();
    }

    @NonNull
    @Override
    public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) {
        //省略無關代碼
        
        //Runnable交給threadWorker去執行
        return threadWorker.scheduleActual(action, delayTime, unit, tasks);
    }
}
複製代碼

接下來是Worker緩存池的操做:

CachedWorkerPool的get()
static final class CachedWorkerPool implements Runnable {
    ThreadWorker get() {
        if (allWorkers.isDisposed()) {
            return SHUTDOWN_THREAD_WORKER;
        }
        while (!expiringWorkerQueue.isEmpty()) {
            //若是緩衝池不爲空,就從緩衝池中取threadWorker
            ThreadWorker threadWorker = expiringWorkerQueue.poll();
            if (threadWorker != null) {
                return threadWorker;
            }
        }

        //若是緩衝池中爲空,就建立一個並返回。
        ThreadWorker w = new ThreadWorker(threadFactory);
        allWorkers.add(w);
        return w;
    }
}
複製代碼

ThreadWorker到底作了什麼呢?追進去父類NewThreadWorker

NewThreadWorker 的構造函數
public class NewThreadWorker extends Scheduler.Worker implements Disposable {
    private final ScheduledExecutorService executor;
		volatile boolean disposed;

		public NewThreadWorker(ThreadFactory threadFactory) {
    		//構造方法中建立一個ScheduledExecutorService對象,能夠經過ScheduledExecutorService來使用線程池
    		executor = SchedulerPoolFactory.create(threadFactory);
		}
    }
複製代碼
SchedulerPoolFactory.create
public final class SchedulerPoolFactory {
		/** * Creates a ScheduledExecutorService with the given factory. * @param factory the thread factory * @return the ScheduledExecutorService */
    public static ScheduledExecutorService create(ThreadFactory factory) {
        // 此處建立了線程!!
        final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, factory);
        if (PURGE_ENABLED && exec instanceof ScheduledThreadPoolExecutor) {
            ScheduledThreadPoolExecutor e = (ScheduledThreadPoolExecutor) exec;
            POOLS.put(e, exec);
        }
        return exec;
    }
}
複製代碼

因此,IoScheduler 使用 CachedWorkerPool 做爲線程池,其內部維護了一個阻塞隊列,用於記錄全部可用線程,當有新的任務需求時,線程池會查詢阻塞隊列中是否有可用線程,沒有的話就新建一個。

咱們想要知道爲何線程突增沒有複用,就要看看全部使用過的那些空閒線程什麼時機會被回收到阻塞隊列中去。

CachedWorkerPool 的 release()
void release(ThreadWorker threadWorker) {
            // Refresh expire time before putting worker back in pool
  					// 刷新線程的到期時間 將執行完畢的 Worker 放入緩存池中
            threadWorker.setExpirationTime(now() + keepAliveTime);
            expiringWorkerQueue.offer(threadWorker);
        }
複製代碼

調用此處代碼只有一處:

@Override
        public void dispose() {
            if (once.compareAndSet(false, true)) {
                tasks.dispose();
                pool.release(threadWorker);
            }
        }
複製代碼

對於這一處的調用,能夠簡單理解爲線程內部維護了一個狀態列表,當線程內的任務完成以後,會調用 dispose 來解除訂閱,釋放線程的佔用。

那何時銷燬呢?能夠看到 CachedWorkerPool 構造函數中建立了清理定時任務:

static final class CachedWorkerPool implements Runnable {
        CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) {
            //...
          	// 建立一個線程,該線程默認會每60s執行一次,來清除已到期的線程
                evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY);
           // 設置定時任務
                task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS);
            //...
        }

        @Override
        public void run() {
            evictExpiredWorkers();
        }
     }
複製代碼
CachedWorkerPool 的 evictExpiredWorkers()
void evictExpiredWorkers() {
            if (!expiringWorkerQueue.isEmpty()) {
                long currentTimestamp = now();

                for (ThreadWorker threadWorker : expiringWorkerQueue) {
                    if (threadWorker.getExpirationTime() <= currentTimestamp) {
                        if (expiringWorkerQueue.remove(threadWorker)) {
                            allWorkers.remove(threadWorker);
                        }
                    } else {
                        // 隊列是根據失效時間排序的,因此一旦當咱們找到未失效的Worker就能夠中止清理了
                        break;
                    }
                }
            }
        }
複製代碼

這個IO調度器不像計算調度器,計算調度器用一個數組來保存一組線程,而後根據索引將任務分配給每一個線程,多餘的任務放在隊列中等待執行,因此每一個線程後面任務的執行須要等待前面的任務執行完畢。

而IO調度器裏的線程池是一個能夠自增、無上限的線程池,且60s 保活。也就是說:若是在 60s 內密集請求 IO 調度,超過了複用閾值,調度器不會約束線程數且會不斷開新線程

這樣子就解釋了疑點 1 爲何進直播間時線程暴漲,是由於沒有任務隊列,直接來一個任務,能複用就複用 Worker,不能就新建。

那疑點 2 呢?爲何停留超過了 60s 突漲的線程沒有被回收?

咱們推測:

  • 清理線程是否在正常工做?

  • 有沒有可能存在訂閱泄露?有的地方 Observable 沒有及時結束,因此一直佔用着線程呢?

齋看源碼沒法模擬真實生產環境,那

如何在沒法改動源碼狀況下作動態觀察?

3種方式:

  1. 動態hook
  2. 靜態插樁
  3. 非阻塞式斷點,打 log

觀察點整理

  1. 任務的入口 —— 到底有多頻繁,誰在提交任務?
  2. 工做的邏輯 —— 這個任務被分配了哪一個線程?
  3. 複用的邏輯 —— 滿的時候觸發 new Thread,此時複用的狀況怎麼樣,爲何會新建這麼多?
  4. 釋放的邏輯 —— 解除訂閱和訂閱數量的比對,是否存在訂閱泄漏?
  5. 過時清除的邏輯 —— 清理線程是否在正常工做?每一個線程都在作什麼,爲何停留在直播間沒有被銷燬掉

好,具體觀察結果的log就不貼了,由於齋看日誌體驗不好,我畫了一張圖總結下整個流程:

看出什麼問題了嗎?

在直播間內一直停留,超過 keepAliveTime,之因此沒有清理線程,是由於線程都沒過時,沒錯,前面的 46 個 IO 線程都沒有過時,IoScheduler 使用 ConcurrentLinkedQueue 維護使用完畢的Worker,按插入順序(也就是釋放順序)排序,因此會優先使用最先過時的 Worker 提供給新任務。

咱們來算一下,算 2s 一個輪詢,直播間只有 1 個輪詢協議(實際上不止), 那 60s 已經足夠讓 30 個 Work 更新一遍過時時間了,n 個輪詢能夠更新 60 / 2 * n 個 worker 的過時時間。

果真源碼面前,了無祕密

結論

  1. 直播間這種業務場景的特色:進房時,短時間內大量任務要並行;存在多輪詢

  2. RxJava 的 IO 調度策略,並不適合用於併發多 IO + 輪詢的狀況,沒有任務排隊隊列、線程可自增、無上限、優先使用快過時的線程;

  3. 另外,業務中存在 Rx 不合理使用(前面咱們攔截了入口,因此能夠直觀看到哪裏在使用 IO 調度),如 timer 、打點、jsBridge 都使用了 IO 調度,嵌套調度(重複 new 了 Worker 任務),沒有跟隨生命週期取消訂閱等等等等。

3、解決

找到問題的根源,問題便已經解決了一半,基本 3 個解決方向:

  1. 優化不合理的調度器建立釋放

  2. 線程收斂,不是阻塞就必定要用 IO 調度

    其實 IO 不必使用多線程,改成 IO 多路複用或協程更合理

  3. 減小併發 IO,分塊加載

4、思考

4.1 如何快速分類並定位線程?如何拿到NativeThread?

用前面的方式去分析,有幾個缺點:

  1. 由於前面咱們拿到堆棧是用了斷點 log 的方式,因此咱們拿不到沒有經過指定方法建立的任務信息;
  2. 咱們無法約束開發小夥伴和三方 SDK 爲每一個線程起自定義名稱,沒法快速分類線程,例如 thread-1,咱們就很難定位到是哪一個類發起的調用;
  3. 咱們只能拿到 Java 層與其對接的 native 層 thread 總數,拿不到沒有 attach 到 java 層的 native thread,也就是直接在 native 層建立的線程,好比 Futter engine 中的native thread。

有什麼騷操做呢?

1. ASM字節碼修改

思路很簡單,你既然要建立線程,就確定是經過如下幾種方式:

  • Thread 及其子類
  • TheadPoolExecutor 及其子類、ExecutorsThreadFactory 實現類
  • AsyncTask
  • Timer 及其子類

滴滴團隊開源庫booster就是這麼個思路 —— 利用ASM對字節碼修改,將全部建立線程的指令在編譯期間替換成自定義的方法調用,爲線程名加上調用者的類名前綴,實現了追蹤線程建立來源。

除了支持線程重命名,還能夠把Executors 的方法調用替換成 ShadowExecutors 中對應的優化方法,達到全局暴力收斂的效果。

備註:若是採用 booster ,儘可能多作一些測試和降級方案。例如:ShadowExecutors.newOptimizedFixedThreadPool方法中使用了 LinkedBlockingQueue 隊列,沒有指定隊列大小,默認爲 Integer.MAX_VALUE,無界的LinkedBlockingQueue 做爲阻塞隊列,當任務耗時較長時可能會致使大量新任務在隊列中堆積, CPU 和內存飆升,最終致使 OOM。

2. NativeHook

那問題來了,ASM字節碼修改,只 hook 到了 Java 層與其對接的 native 層 thread ,怎麼拿到直接在 native 層建立的線程呢?誒,咱們前面不是看了線程建立的 C++ 代碼嗎?基本思路就是找到 pthread_create 相關的函數,攔截它。

第一步:尋找Hook點

這須要對線程的啓動流程有必定的瞭解,能夠參考這篇文章Android線程的建立過程

java_lang_Thread.cc:Thread_nativeCreate

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) {
  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}
複製代碼

thread.cc 中的CreateNativeThread函數

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
    ...
    pthread_create_result = pthread_create(&new_pthread,
                                             &attr,
                                             Thread::CreateCallback,
                                             child_thread);
    ...
}
複製代碼
第二步:查找Hook的So

上面Thread_nativeCreate、CreateNativeThread和pthread_create函數分別編譯在哪一個 library 中呢?

很簡單,咱們看看編譯腳本Android.bp就知道了。

art_cc_library {
   name: "libart",
   defaults: ["libart_defaults"],
}

cc_defaults {
   name: "libart_defaults",
   defaults: ["art_defaults"],
   host_supported: true,
   srcs: [
    thread.cc",
   ]
}
複製代碼

能夠看到是在"libart.so"中。

第三步:查找Hook函數的符號

C++ 的函數名會 Name Mangling,咱們須要看看導出符號。

readelf -a libart.so
複製代碼

pthread_create函數的確是在libc.so中,並且由於c編譯的不須要deMangling

001048a0  0007fc16 R_ARM_JUMP_SLOT   00000000   pthread_create@LIBC
複製代碼
第四步:實現

考慮到性能問題,咱們只 hook 指定的so。

hook_plt_method("libart.so", "pthread_create", (hook_func) &pthread_create_hook);
複製代碼

若是你想監控其餘so庫的 pthread_create,能夠本身加上。Facebook 的 profilo 中有一種作法是把目前已經加載的全部so都統一hook了。

至於 pthread_create 的參數直接查看pthread.h就能夠了。

int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
複製代碼

獲取堆棧就是在 native 反射 Java 的方法

jstring java_stack = static_cast<jstring>(jniEnv->CallStaticObjectMethod(kJavaClass, kMethodGetStack));
複製代碼

Profilo :Facebook 的性能分析工具,黑科技不少

epic:該庫已經支持攔截 Thread 類以及 Thread 類全部子類的 run方法,更進一步,咱們能夠結合 Systrace 等工具,來生成整個過程的執行流程圖。

注:對於 app 上的 hook,不要再像之前那樣去依賴反射、動態代理了,關注下 lancet、epic,真的是隨心所欲。

4.2 排查痛點

很是惋惜的是沒能保留一手現場,只能靠猜,靠復現。

卡頓、崩潰都須要「現場信息」。由於 bug 產生也是依賴不少因素,好比用戶的系統版本、CPU 負載、網絡環境、應用數據、線程數、利用率、崩潰發生時全部的線程棧,而不僅是崩潰的線程棧……

脫離這個現場,咱們本地難以復現,也就很難去解決問題。那咱們應該如何去監控線上,而且保留足夠多的現場信息協助咱們排查解決問題呢?

這裏要麼咱們能夠本身研發一套崩潰收集系統,要麼能夠接入現有的方案

4.3 異步的本質?協程、NIO、fiber、loom 解決了什麼?

回到本質上,思考下,爲何咱們須要多線程?多線程真的有必要嗎?

由於順序代碼結構是阻塞式的,每一行代碼的執行都會使線程阻塞在那裏,也就決定了全部的耗時操做不能在主線程中執行,因此就須要多線程來執行。

因此目的是非阻塞,方式是異步。

但許多異步庫被引入,根本緣由是當前線程實現的不足,而並不是說明異步的代碼更好。咱們不要理所固然以爲,異步就是正常不過的事情。其實是 Java 的設計問題,讓咱們一直默默忍受到如今,異步帶來的一系列問題:回調地獄、不方便調試分析……

長期以來,Java 的線程是與操做系統的線程一一對應的,這種模式直接限制了 Java 平臺併發能力的提高:任務阻塞意味着線程阻塞,線程狀態切換又帶來開銷,阻塞線程對系統資源的浪費…… 從 Quasar 項目、Alibaba JDK 的協程特性,到 Kotlin 協程和 OpenJDK 的 Project Loom, Java 社區已經愈來愈多地認識到:目前 Java 的線程模型愈來愈難以知足整個行業對高併發應用開發的需求。

解決方式不少,其中一個流派 —— 語言層:

其中的表明 —— 協程,雖然在不一樣語言中,協程的實現方法各有不一樣,但本質是一致的,是一種任務封裝的思想:調度任務代替了調度線程,從而減小線程阻塞,用盡可能少的線程執行儘可能多的任務。

線程調度二級模型

好比 Kotlin 協程,由於 Kotlin 的運行依賴於 JVM,所以沒辦法在底層支持協程。同時,Kotlin 是一門編程語言,須要在語言層面支持協程,而不是像框架那樣在語言層面之上支持。所以,Kotlin-JVM 協程最核心的部分是在編譯器中,基於各類 Callback 技術達到看起來像同步代碼的效果,本質上仍是異步,調用各類阻塞 API 仍是無解,好比 synchronized、 Native 方法中線程掛起,該阻塞線程仍是會阻塞。

爲了使 Java 併發能力在更大範圍上獲得提高,從底層進行改進即是必然。這就是 Project Loom 項目發起的緣由,也就是另一個流派 —— JVM 層。

[譯]loom項目提案

表明項目是 Project LoomAJDK(Alibaba JDK),從 Erlang 和 Go 語言中獲得了啓發,從 JVM 層面着手,把以前阻塞線程的,通通改成阻塞「纖程(fiber)」、「輕量級線程」或者「虛擬線程」,這麼作的優勢是更能完全解決問題,不須要靠 async / await 這種語法糖了,在JVM 和類庫層面作支持,能使整個 JVM 生態上的其餘語言都收益

但缺點或者說難點一樣明顯,那就是如何與現有的代碼兼容,這個改造,意味着不少 native 方法也要改,大概要等到 JDK20 才能出預覽版本吧,再看看咱們 Android 如今僅支持到 Java8,emmm,仍是早日使用 Kotlin-JVM 協程吧。

若是後面 JVM 開發團隊完成這個項目,看看 Kotlin coroutine 對此會有什麼反應,須要怎麼調整,在最好的狀況下,Kotlin coroutine 未來只是簡單映射到 「纖程」 上。其實蠻有意思的,社區上關於異步的討論,還有對Java、JVM的設計反思。感興趣的小夥伴能夠去研究研究。

參考:


我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力於追求代碼優雅架構設計T 型成長

歡迎關注 FeelsChaotic 的簡書掘金若是個人文章對你哪怕有一點點幫助,歡迎 ❤️! 你的鼓勵是我寫做的最大動力!

最最重要的,請給出你的建議或意見,有錯誤請多多指正!

相關文章
相關標籤/搜索