https://cloud.tencent.com/developer/article/1071770
有兩種棧:
出現(xiàn)次數(shù)最多的一種,稱之為堆棧A。
java.lang.OutOfMemoryError:pthread_create(1040KB stack)failed:Outofmemory? ? java.lang.Thread.nativeCreate(Native Method)java.lang.Thread.start(Thread.java:745)...
另一種,出現(xiàn)次數(shù)較少,稱之為堆棧B。
java.lang.OutOfMemoryError:Could not allocate JNI Envjava.lang.Thread.nativeCreate(Native Method)java.lang.Thread.start(Thread.java:729)...
針對(duì)上面兩種crash,分析一下Android/Linux中線程的創(chuàng)建過程,以及該OOM出現(xiàn)的原因。
1. 從java到native
我們看到最靠近棧頂?shù)膉ava方法調(diào)用的Thread::start, 該方法內(nèi)部調(diào)用了 native 方法Thread::nativeCreate。如下:
publicsynchronizedvoidstart(){...nativeCreate(this,stackSize,daemon);...}
這里我們主要關(guān)注傳入的兩個(gè)參數(shù)
1.this: 即Thread對(duì)象自身
2.stackSize: 這個(gè)比較關(guān)鍵,指定了新創(chuàng)建的線程的棧大小,單位是字節(jié)(Byte)
Thread 類其中一個(gè)構(gòu)造函數(shù),接受stackSize參數(shù)
設(shè)置為0表示忽略之
文檔提到:提高stackSize會(huì)減少StackOverFlow的發(fā)生,而降低stackSize會(huì)減少OutOfMemory的發(fā)生
另外:該參數(shù)是平臺(tái)相關(guān)的,在一些平臺(tái)上可能會(huì)直接被無視(有點(diǎn)類似Syste::gc的描述,然而目前來看gc在絕大多數(shù)平臺(tái)都生效)
3.daemon: 表明新創(chuàng)建的線程是否是Daemon線程
2. 從native到ART
native層的代碼分析的是Android 8.0的ART虛擬機(jī)源碼,相關(guān)文件會(huì)給出全路徑。
首先我們看一下 Thread::nativeCreate 的native實(shí)現(xiàn)。在art/runtime/native/java_lang_thread.cc 中。其主要邏輯會(huì)調(diào)用到 art/runtime/thread.cc 的 art::Thread::CreateNativeThread 函數(shù)來。
其主要邏輯如下:
voidThread::CreateNativeThread(JNIEnv*env,jobject java_peer,size_t stack_size,bool is_daemon){// 代碼1Thread*child_thread=newThread(is_daemon);// 代碼2std::unique_ptr<JNIEnvExt>child_jni_env_ext(JNIEnvExt::Create(child_thread,Runtime::Current()->GetJavaVM(),&error_msg));if(child_jni_env_ext.get()!=nullptr){// 代碼片段3if(success)return;}// Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.// 代碼片段4}
代碼1
創(chuàng)建了java.lang.Thread相對(duì)應(yīng)的 native 層C++對(duì)象。
代碼2
有JNI基礎(chǔ)的同學(xué)知道,java中每一個(gè) java線程 對(duì)應(yīng)一個(gè) JniEnv 結(jié)構(gòu)。這里的JniEnvExt 就是ART 中的 JniEnv。這里源碼中有一段注釋
Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and do not have a good way to report this on the child’s side.
代碼片段4
3是創(chuàng)建線程的主要邏輯,4是執(zhí)行創(chuàng)建流程失敗的收尾邏輯。我們先跳過3,看一下4的邏輯。
std::stringmsg(child_jni_env_ext.get()==nullptr?StringPrintf("Could not allocate JNI Env: %s",error_msg.c_str()):StringPrintf("pthread_create (%s stack) failed: %s",PrettySize(stack_size).c_str(),strerror(pthread_create_result)));ScopedObjectAccesssoa(env);soa.Self()->ThrowOutOfMemoryError(msg.c_str());
可以看到最后一句就是拋出我們熟悉的OOM異常的地方了。而且msg剛好和我們遇到的兩種堆棧吻合。
child_jni_env_ext.get() == nullptr 對(duì)應(yīng)的是堆棧B
pthread_create 調(diào)用失敗對(duì)應(yīng)的是堆棧A
所以這里我們可以得出堆棧B發(fā)生的原因:JNIEnvExt::Create調(diào)用失敗。
跟進(jìn)去看一下為什么JNIEnvExt::Create會(huì)return nullptr:
JNIEnvExt*JNIEnvExt::Create(Thread*self_in,JavaVMExt*vm_in,std::string*error_msg){std::unique_ptr<JNIEnvExt>ret(newJNIEnvExt(self_in,vm_in,error_msg));if(CheckLocalsValid(ret.get())){returnret.release();}returnnullptr;}
直接原因是CheckLocalsValidreturnfalse,再進(jìn)一步是JniEnvExt::table_mem_map_是nullptr。
調(diào)用鏈?zhǔn)荍niEnvExt::Create() -> JNIEnvExt::JNIEnvExt()(構(gòu)造函數(shù)) ->IndirectReferenceTable::IndirectReferenceTable()
我們一步到位,直接看一下IndirectReferenceTable::IndirectReferenceTable()的實(shí)現(xiàn)
constsize_t table_bytes=max_count*sizeof(IrtEntry);table_mem_map_.reset(MemMap::MapAnonymous(...,table_bytes,...));
這里的max_count是常量art::kLocalsInitial == 512。而筆者自己計(jì)算了一下sizeof(IrtEntry) == 8。所以table_bytes = 512 * 8 = 4096 = 4k,剛好是一個(gè)內(nèi)存頁的大小。
因此是調(diào)用MemMap::MapAnonymous()失敗了。
核心代碼摘要如下, art/runtime/mem_map.cc:
// 1. 創(chuàng)建 ashmemfd.reset(ashmem_create_region(debug_friendly_name.c_str(),page_aligned_byte_count));// 2. 調(diào)用mmap映射到用戶態(tài)內(nèi)存地址空間void* actual = MapInternal(..., fd.get(), ...);
需要注意的是如果步驟1失敗的話,fd.get()返回-1,步驟2仍然會(huì)正常執(zhí)行,只不過其行為有所不同。
如果步驟1成功的話,兩個(gè)步驟則是:
1.通過Andorid的匿名共享內(nèi)存(Anonymous Shared Memory)分配 4KB(一個(gè)page)內(nèi)核態(tài)內(nèi)存
2.再通過 Linux 的 mmap 調(diào)用映射到用戶態(tài)虛擬內(nèi)存地址空間。
如果步驟1失敗的話,步驟2則是:
通過 Linux 的 mmap 調(diào)用創(chuàng)建一段虛擬內(nèi)存。
注意是分配虛擬內(nèi)存失敗了,區(qū)分一下虛擬內(nèi)存和物理內(nèi)存的概念。
考察失敗的場(chǎng)景:
步驟1 失敗的情況一般是內(nèi)核分配內(nèi)存失敗,這種情況下,整個(gè)設(shè)備/OS的內(nèi)存應(yīng)該都處于非常緊張的狀態(tài)。
步驟2 失敗的情況一般是 進(jìn)程虛擬內(nèi)存地址空間耗盡。
另外,8.0的代碼中可以看到,在mmap失敗之后,會(huì)整理一串錯(cuò)誤信息出來,而外網(wǎng)的crash中沒看到相關(guān)信息,猜測(cè)是新版本加入的。錯(cuò)誤信息如下:”Failed anonymous mmap(%p, %zd, 0x%x, 0x%x, %d, 0): %s. See process maps in the log.”
顯然,此處是因?yàn)椴襟E2 失敗。
PS: 關(guān)于Android 的ashmem可以閱讀
Android系統(tǒng)匿名共享內(nèi)存Ashmem(Anonymous Shared Memory)驅(qū)動(dòng)程序源代碼分析(http://blog.csdn.net/luoshengyang/article/details/6664554)
技術(shù)內(nèi)幕:Android對(duì)Linux內(nèi)核的增強(qiáng) Ashmem(http://www.jmpcrash.com/?p=315)
Android Kernel Features(https://elinux.org/Android_Kernel_Features#ashmem)
至此,代碼片段4就分析完了,其只主要功能就是創(chuàng)建子線程相關(guān)的數(shù)據(jù)結(jié)構(gòu)。同事也分析出了Crash堆棧B的出現(xiàn)原因,而Crash堆棧A出現(xiàn)的原因則隱藏在代碼片段3中。
3. 從 ART 到 pthread
代碼片段3:
if(child_jni_env_ext.get()!=nullptr){pthread_t new_pthread;pthread_attr_t attr;...CHECK_PTHREAD_CALL(pthread_attr_setstacksize,(&attr,stack_size),stack_size);pthread_create_result=pthread_create(&new_pthread,&attr,Thread::CreateCallback,child_thread);if(pthread_create_result==0){...return;}}
可以看到,主要邏輯就是調(diào)用了pthread_create,該函數(shù)有幾個(gè)參數(shù):
new_pthread: 新創(chuàng)建的線程的句柄。attr: 指定了新線程的一些屬性,其中包括棧大小。Thread::CreateCallback: 新創(chuàng)建的線程的routine函數(shù),即,線程的入口函數(shù)。child_thread: callbac的唯一參數(shù),此處是 native 層的 Thread 類。
廢話不多少,我們進(jìn)去pthread_create看一下代碼邏輯。
PS:Android的C語言標(biāo)準(zhǔn)庫實(shí)現(xiàn)是區(qū)別于普通GNU/Linux發(fā)行版的glic的,因?yàn)楹笳呤荓GPL協(xié)議的,Android重寫了一個(gè)實(shí)現(xiàn),用的是BSD協(xié)議。該lib叫做Bionichttps://www.wikiwand.com/en/Bionic_(software) (意為仿生)。
bionic/lib/bionic/pthread_create.cpp:
intpthread_create(pthread_t*thread_out,pthread_attr_tconst*attr,void*(*start_routine)(void*),void*arg){...// 1. 分配棧。pthread_internal_t*thread=NULL;void*child_stack=NULL;int result=__allocate_thread(&thread_attr,&thread,&child_stack);if(result!=0){returnresult;}...// 2. linux 系統(tǒng)調(diào)用 clone,執(zhí)行真正的創(chuàng)建動(dòng)作。int rc=clone(__pthread_start,child_stack,flags,thread,&(thread->tid),tls,&(thread->tid));if(rc==-1){returnerrno;}...return0;}
步驟2先按下不表,我們看看步驟1的邏輯:
staticint__allocate_thread(...){mmap_size=BIONIC_ALIGN(attr->stack_size+sizeof(pthread_internal_t),PAGE_SIZE);attr->stack_base=__create_thread_mapped_space(mmap_size,attr->guard_size);if(attr->stack_base==NULL){returnEAGAIN;}...}
再看一下__create_thread_mapped_space干了什么:
staticvoid*__create_thread_mapped_space(size_t mmap_size,size_t stack_guard_size){// Create a new private anonymous map.int prot=PROT_READ|PROT_WRITE;int flags=MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE;void*space=mmap(NULL,mmap_size,prot,flags,-1,0);if(space==MAP_FAILED){...returnNULL;}// 代碼片段1returnspace;}
主體邏輯再簡單不過,即:調(diào)用mmap分配棧內(nèi)存。這里mmap flag中指定了MAP_ANONYMOUS,即匿名內(nèi)存映射(mapping anonymous)(https://www.wikiwand.com/en/Mmap#/File-backed_and_anonymous)。這是在Linux中分配大塊內(nèi)存的常用方式。其分配的是虛擬內(nèi)存,對(duì)應(yīng)頁的物理內(nèi)存并不會(huì)立即分配,而是在用到的時(shí)候,觸發(fā)內(nèi)核的缺頁中斷,然后中斷處理函數(shù)再分配物理內(nèi)存。
我們看一下創(chuàng)建的內(nèi)存大小是怎么計(jì)算的。在pthread的實(shí)現(xiàn)中,mmap分配的內(nèi)存賦值給了stack_base,stack_base不光是線程執(zhí)行的棧,其中還存儲(chǔ)了線程的其他信息(如線程名,ThreadLocal變量等),這些信息定義在pthread_internal_t結(jié)構(gòu)體中。因此實(shí)際分配的內(nèi)存大小是stack_size + sizeof(pthread_internal_t),然后再向上取整,按照內(nèi)存頁大小對(duì)齊。
還記得crash堆棧A的異常描述嗎
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
結(jié)論
沒錯(cuò)!就是因?yàn)檫@里的mmap失敗了。又是虛擬內(nèi)存分配失敗。
默認(rèn) StackSize 是多少
另外一個(gè)需要考慮的事,如果沒有指定stackSize,默認(rèn)的是多少呢?
Java層的Thread類默認(rèn)stackSize是0,傳給native層也是0,于是在native層有這樣一段代碼。
staticsize_tFixStackSize(size_t stack_size){if(stack_size==0){// GetDefaultStackSize 是啟動(dòng)art時(shí)命令行的 "-Xss=" 參數(shù)// Android 中沒有該參數(shù),因此為0.stack_size=Runtime::Current()->GetDefaultStackSize();}// bionic pthread 默認(rèn)棧大小是 1Mstack_size+=1*MB;...if(Runtime::Current()->ExplicitStackOverflowChecks()){// 8Kstack_size+=GetStackOverflowReservedBytes(kRuntimeISA);}else{// 8K + 8Kstack_size+=Thread::kStackOverflowImplicitCheckSize+GetStackOverflowReservedBytes(kRuntimeISA);}...returnstack_size;}
因此 默認(rèn)的stackSize = 1M + 8K + 8K = 1040K,和crash堆棧完全一致。
Native 層的Stack Overflow檢測(cè)
另外上面的代碼片段1其實(shí)也挺有意思的,它優(yōu)雅的判斷了StackOverflow的場(chǎng)景,避免棧內(nèi)存溢出污染其他內(nèi)存區(qū)域。
PS 代碼片段1:
// Stack is at the lower end of mapped space, stack guard region is at the lower end of stack.// Set the stack guard region to PROT_NONE, so we can detect thread stack overflow.if(mprotect(space,stack_guard_size,PROT_NONE)==-1){...munmap(space,mmap_size);returnNULL;}prctl(PR_SET_VMA,PR_SET_VMA_ANON_NAME,space,stack_guard_size,"thread stack guard page");
棧的增長方向是從高地址到低地址,因此把棧最低地址的stack_guard_size字節(jié)的內(nèi)存設(shè)置成不可訪問。當(dāng)訪問到的時(shí)候就會(huì)觸發(fā)系統(tǒng)的異常處理~這段內(nèi)存有個(gè)名字叫做Red Zone。
4. 從 pthread 到 Linux 內(nèi)核調(diào)用
這里主要涉及到 linux 的clone系統(tǒng)調(diào)用(SystemCall)(http://man7.org/linux/man-pages/man2/clone.2.html)。man page說:
clone() creates a new process, in a manner similar to fork(2).
嗯,“clone創(chuàng)建新進(jìn)程”?等等,不是線程嗎?哈,這里有一個(gè)很有趣的地方,Unix里面其實(shí)只有進(jìn)程,而線程是 POSIX標(biāo)準(zhǔn)定義的。因此這里的clone是實(shí)現(xiàn)線程的一種手段。
簡單來說:
fork:創(chuàng)建新的進(jìn)程,并把父進(jìn)程的內(nèi)存全部copy到子進(jìn)程,兩者的內(nèi)存不共享。(后來優(yōu)化出了CopyOnWrite機(jī)制,幾乎完全優(yōu)化掉了Copy內(nèi)存的開銷)。
clone:創(chuàng)建新的進(jìn)程,并且父進(jìn)程和子進(jìn)程共享內(nèi)存。
因此當(dāng)兩個(gè)進(jìn)程的內(nèi)存共享之后,完全就符合“線程”的定義了。
5. 結(jié)論OOM分析
OK,終于分析完了,看了好多代碼。最終得出一個(gè)結(jié)論,不管是堆棧A,還是堆棧B:
創(chuàng)建線程過程中發(fā)生OOM是因?yàn)檫M(jìn)程內(nèi)的虛擬內(nèi)存地址空間耗盡了。
所以,什么情況下虛擬內(nèi)存地址空間才會(huì)耗盡呢?我們先研究一下linux的虛擬內(nèi)存怎么布局的??梢詤⒖催@里, 筆者借用另一個(gè)PPT 21頁(https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf)
可以看到32位系統(tǒng)中,用戶空間的內(nèi)存是3G大,簡單起見,我們粗略估計(jì)一下,假設(shè)
1.可見虛擬內(nèi)存是3G大(實(shí)際值更?。?/p>
2.創(chuàng)建一個(gè)進(jìn)程需要1M虛擬內(nèi)存(實(shí)際值更大)
因此再假設(shè)有一個(gè)進(jìn)程,除了創(chuàng)建線程什么都不干,那他最多能創(chuàng)建多少個(gè)線程?
3G/1M=約3000個(gè)
沒錯(cuò),在完全理想的情況下最多是3000個(gè)線程。綜合其他因素,實(shí)際值會(huì)明顯小于3000。雖然3000的上限看上去很大,而如果有代碼邏輯問題,創(chuàng)建很多線程,其實(shí)很容易爆掉。
外網(wǎng)上報(bào)的crash則屬于這種情況,某種corner-case下會(huì)導(dǎo)致線程的無節(jié)制創(chuàng)建。
6. PS: FileDescriptor超出上限?!
受到《不可思議的OOM》(https://mp.weixin.qq.com/s/AjtzDxwJzyqC95FXgDPS1g) 啟發(fā),在此特別感謝作者。
請(qǐng)讀者先行閱讀上文。
怎么判斷虛擬內(nèi)存用完還是FileDescriptor耗盡呢?
對(duì)于堆棧A
我們看到拋出OOM的地方已經(jīng)保留了錯(cuò)誤碼信息
pthread_create_result=pthread_create(...);...StringPrintf("pthread_create (%s stack) failed: %s",PrettySize(stack_size).c_str(),strerror(pthread_create_result)));
代碼中pthread_create_result是linux標(biāo)準(zhǔn)錯(cuò)誤碼定義,定義在 bionic/lib/private/bionic_errdefs/bionic_errdefs.h 頭文件中,
__BIONIC_ERRDEF(EBADF,9,"Bad file descriptor")__BIONIC_ERRDEF(ECHILD,10,"No child processes")__BIONIC_ERRDEF(EAGAIN,11,"Try again")__BIONIC_ERRDEF(ENOMEM,12,"Out of memory")// <-----...__BIONIC_ERRDEF(EMFILE,24,"Too many open files")// <-----
因此我們可以通過OOM異常的message字段,對(duì)應(yīng)看到錯(cuò)誤碼。在企鵝FM的異常場(chǎng)景中,屬于12,即Out of memory。
同時(shí),在上文提到的linux clone系統(tǒng)調(diào)用中,有一處log。
int rc=clone(__pthread_start,child_stack,flags,thread,&(thread->tid),tls,&(thread->tid));if(rc==-1){...__libc_format_log(ANDROID_LOG_WARN,"libc","pthread_create failed: clone failed: %s",strerror(errno));}
因此,在系統(tǒng)log中也能看到蛛絲馬跡,例如:
:pthread_create failed:clone failed:Outofmemory11-0612:27:00.2563077531188W art:Throwing OutOfMemoryError"pthread_create (1040KB stack) failed: Out of memory"
對(duì)于堆棧B
在上文提到的代碼片段中:
fd.Reset(ashmem_create_region(debug_friendly_name.c_str(),page_aligned_byte_count),/* check_usage */false);if(fd.Fd()==-1){*error_msg=StringPrintf("ashmem_create_region failed for '%s': %s",name,strerror(errno));
可以看到會(huì)打印出來錯(cuò)誤信息,然而Android 8.0 似乎改了代碼 https://android.googlesource.com/platform/art/+/a5c61bf479453e7e195888afb4e62a9872d6be7c%5E%21/runtime/mem_map.cc
對(duì)應(yīng)日志中可以看到 errno
11-0606:25:54.19337258575E art:ashmem_create_region failedfor'indirect ref table':Too many open files11-0606:25:54.19337258575W art:Throwing OutOfMemoryError"Could not allocate JNI Env"
企鵝FM中的堆棧B場(chǎng)景屬于 FileDescriptor 耗盡