天天看點

高端面試必備:Android中一個線程的記憶體大小為多少?

作者:網際網路雜談A
一般在Android開發中,分析線上問題的時候,最頭大的就是OOM這類問題了。這裡列舉出幾種和Thread線程相關的Crash日志來作為對這個問題的答複。

一般和線程相關的OOM比較常見的有如下兩種:

java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:745)           
java.lang.OutOfMemoryError: Could not allocate JNI Env
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:729)           

從第一個Crash日志中我們就可以得出這麼一個結論:建立線程的大小為1040KB 那到底怎麼驗證到底是不是1040KB呢?還是得要從系統源碼來看。

從Java到Native層

棧頂的Java方法調用的 Thread::start, 該方法内部調用了 native 方法 Thread::nativeCreate

[-> Thread.java]
public synchronized void start() {
    checkNotStarted(); // 保證線程隻有啟動一次
    hasBeenStarted = true;
    // this 表示線程本身
    // stackSize 表示該參數是平台相關的,也可以自己設定
    // daemon 表示線程是否是Daemon線程
    nativeCreate(this, stackSize, daemon);
}
    
[-> java_lang_Thread.cc]
NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V")

[-> java_lang_Object.cc]
#define NATIVE_METHOD(className, functionName, signature) \
    { #functionName, signature, reinterpret_cast<void*>(className ## _ ## functionName) }

[-> java_lang_Thread.cc]
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]
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { 
  Thread* self = static_cast<JNIEnvExt*>(env)->self;
  Runtime* runtime = Runtime::Current();
  ...
  // 建立了 java.lang.Thread 相對應的 native 層C++對象
  Thread* child_thread = new Thread(is_daemon);
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);

  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
                    reinterpret_cast<jlong>(child_thread));

  // java中每一個 java線程 對應一個 JniEnv 結構。這裡的JniEnvExt 就是ART 中的 JniEnv
  std::unique_ptr<JNIEnvExt> child_jni_env_ext(
      JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    // 建立線程
    pthread_create_result = pthread_create(&new_pthread,
                         &attr, Thread::CreateCallback, child_thread);

    if (pthread_create_result == 0) {
      child_jni_env_ext.release();
      return;
    }
  }
  // Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
  ...
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  {
    std::string msg(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)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }
  ...
}    
  
[ -> pthread_create.cpp]
int pthread_create(pthread_t* thread_out, pthread_attr_t const* 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) { 
    return result;
  }
  ... 
  // 2. linux 系統調用 clone,執行真正的建立動作。 
  int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid)); 
  if (rc == -1) { 
    return errno;
  }
  ... 
  return 0;
}
  
static int __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) { 
    return EAGAIN;
  }
  ...
}
  
static void* __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) {
    ... 
    return NULL;
  } 
}             

主體邏輯再簡單不過,即:調用mmap配置設定棧記憶體。這裡mmap flag中指定了 MAP_ANONYMOUS,即匿名記憶體映射(mapping anonymous)。這是在Linux中配置設定大塊記憶體的常用方式。其配置設定的是虛拟記憶體,對應頁的實體記憶體并不會立即配置設定,而是在用到的時候,觸發核心的缺頁中斷,然後中斷處理函數再配置設定實體記憶體。

接下來重點看看這裡:stack_size = FixStackSize(stack_size), 設定線程棧大小。

static size_t FixStackSize(size_t stack_size) {
  // A stack size of zero means "use the default".
  if (stack_size == 0) { //這裡java層傳遞過來的是0
    //GetDefaultStackSize是啟動art時指令行的"-Xss="參數,實際運作環境沒有傳遞,預設是0
    stack_size = Runtime::Current()->GetDefaultStackSize();
  }

  // Dalvik used the bionic pthread default stack size for native threads,
  // so include that here to support apps that expect large native stacks.
  stack_size += 1 * MB;//預設棧大小是 1M

  // Under sanitization, frames of the interpreter may become bigger, both for C code as
  // well as the ShadowFrame. Ensure a larger minimum size. Otherwise initialization
  // of all core classes cannot be done in all test circumstances.
  if (kMemoryToolIsAvailable) {
    stack_size = std::max(2 * MB, stack_size);
  }

  // It's not possible to request a stack smaller than the system-defined PTHREAD_STACK_MIN.
  if (stack_size < PTHREAD_STACK_MIN) {
    stack_size = PTHREAD_STACK_MIN;
  }

  if (Runtime::Current()->ExplicitStackOverflowChecks()) {
    // It's likely that callers are trying to ensure they have at least a certain amount of
    // stack space, so we should add our reserved space on top of what they requested, rather
    // than implicitly take it away from them.
    stack_size += GetStackOverflowReservedBytes(kRuntimeISA);
  } else {
    // If we are going to use implicit stack checks, allocate space for the protected
    // region at the bottom of the stack.
    //stack_size += 8K + 8K;
    stack_size += Thread::kStackOverflowImplicitCheckSize +
        GetStackOverflowReservedBytes(kRuntimeISA);
  }

  // Some systems require the stack size to be a multiple of the system page size, so round up.
  stack_size = RoundUp(stack_size, kPageSize);

  return stack_size;
}           

該函數主要對stack_size進行了設定,預設的棧大小中,包含了預設棧大小為1M,以及棧溢出相應檢查所需空間8k + 8k。

是以,線程棧所需記憶體總大小 = 1M + 8k + 8k,即為1040k。

總結

  1. Android中,線程建立過程包括Java層和native層2個部分。
  2. Java層,Thread對象建立出來後,隻是建立了一個Java對象而已,系統并沒有真正的建立一個線程。
  3. Thread建立時,可以傳遞線程名參數,如果沒有,則預設線程名為:“Thread-” + nextThreadNum()。為特定的線程命名是一個好習慣。
  4. 當Java層的Thead對象調用start()方法時,會調用native方法nativeCreate通過native層進行真正的線程建立過程。
  5. native層會通過pthread_create()函數調用核心建立線程。pthread_create是pthread庫中的函數,通過syscall再調用到clone來建立線程。
  6. native層通過FixStackSize設定線程棧大小,預設情況下,線程棧所需記憶體總大小 = 1M + 8k + 8k,即為1040k。
  7. 如果在建立線程過程中,記憶體不足,會引發OOM。

繼續閱讀