天天看點

深入OpenJDK源碼--你真的了解System.out.println嗎?

一、前戲

  可能不少小夥伴習慣在代碼中使用sout列印一些資訊,就像這樣:

System.out.println("hello world!")

      

  做為一位資深幹碼人,本着弘揚黨求真務實的精神,必須得來看看這個sout有何玄機~~

  首先看調用就知道,out是System類的一個公共靜态成員變量,進入System.java中:

public final static PrintStream out = null;

      

  嗯,不止是public,還是final的。不管,來找找out是在哪裡指派的。。。。。。日嘛找半天沒找到?那就試試直接在類中搜尋:out = ,結果如下:

深入OpenJDK源碼--你真的了解System.out.println嗎?

  完犢子,整個System類一共将近1300行的代碼,隻找到一個和out指派相關的,還是啥子局部變量fdOut,看起來和out屬性沒有什麼關聯啊~ 往上滑滑看看這個是什麼方法:

private static void initializeSystemClass() {
 ......
 FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
 FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
 setIn0(new BufferedInputStream(fdIn));
 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
 setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
 ......
}

      

  原來如此,看到initializeSystemClass方法,似乎一切都明了了~~

二、JVM源碼分析

  initializeSystemClass不是給我們調用的,這個方法會在vm線程初始化後被虛拟機調用。其定義在thread.cpp中:

static void call_initializeSystemClass(TRAPS) {
  Klass* k =  SystemDictionary::resolve_or_fail(vmSymbols::java_lang_System(), true, CHECK);
  instanceKlassHandle klass (THREAD, k);

  JavaValue result(T_VOID);
  JavaCalls::call_static(&result, klass, vmSymbols::initializeSystemClass_name(),
                                         vmSymbols::void_method_signature(), CHECK);
}

      

  首先擷取Klass(類元資訊在虛拟機中的結構表示,就是一個c++中的類),在vmSymbols.hpp中找找java_lang_System對應的值:

 template(java_lang_System,                          "java/lang/System")       

      

  可以看到代表的就是java.lang.System類,同時,initializeSystemClass_name也定義在vmSymbols.hpp中:

template(initializeSystemClass_name,                "initializeSystemClass")   

      

  這裡使用JavaCalls::call_static就是調用java.lang.System的靜态方法initializeSystemClass。還要再啰嗦一句,call_initializeSystemClass是在何處調用的?我們回到thread.cpp中,進入Threads::create_vm方法,在其中找到了call_initializeSystemClass方法的調用:

jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
 ......
  call_initializeSystemClass(CHECK_0);
 ......
}

      

  然後看看Threads::create_vm是在何處調用的,我們進入jni.cpp,找到JNI_CreateJavaVM方法:

_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
 ......
 result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
 if (result == JNI_OK) {
  ......
 } else {
  ...... 
 }
 ......
}

      

  關于JNI_CreateJavaVM,在深入openjdk源碼全面了解Java類加載器這篇文章梳理JVM啟動過程的時候提過,這裡就不贅述了。

   現在已經知道了,JVM啟動後會在初始化JVM的時候調用CreateJavaVM,進而調用initializeSystemClass方法。但是out屬性是如何設定的呢?我們回到java.lang.System#initializeSystemClass方法:

private static void initializeSystemClass() {
 ......
 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
 ......
}

private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
       if (enc != null) {
            try {
                return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
            } catch (UnsupportedEncodingException uee) {}
        }
        return new PrintStream(new BufferedOutputStream(fos, 128), true);
    }

      

  這個方法中唯一和out相關的就是這個setOut0方法調用了,我們來看看這個方法:

 private static native void setOut0(PrintStream out);

      

  嗯,這個是一個native方法,沒辦法,又隻有回到JVM源碼了。怎麼找呢?首選組裝一下本地方法名,根據規則setOut0對應的本地方法應該叫:Java_java_lang_System_setOut0,我們到源碼中找找,然後在System.c中找到了該方法(關于JNI,可以看看Java深入JVM源碼核心探秘Unsafe(含JNI完整使用流程)):

JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

      

  首先擷取了java.io.PrintStread類型的out靜态成員,嗯,這個就是我們java.lang.System類的靜态成員out:

public final static PrintStream out;

      

 然後将stream參數指派給它,這個stream就是initializeSystemClass方法中通過newPrintStream方法建立的PrintStream 對象。到現在我們已經明白了,out原來是這樣指派的,真麻煩~

  趁熱打鐵,弄明白了out,接下來看看println。既然out是PrintStream對象,那麼到PrintStream中看看println方法:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

      

  Java代碼看起來是要親切多了,但是這個synchronized是個什麼鬼???

三、坑?

  前面我們發現println方法竟然有個synchronized關鍵字,經常在項目中使用sout的小夥伴會不會感覺腦袋嗡嗡的?為什麼要嗡嗡?不要忘記我們前面通過JVM源碼跟蹤的System.out的指派過程,這個out可是單例!你個加了synchronized(this)的虛方法,還是單例的,如果在系統中進行并發使用,後果不用我多說吧?

  那可能有人就要說了,那我不用println(“hello world!”)了嘛,我換成print總行了吧?

System.out.print("hello world!");
System.out.print("\n");

      

  因為print方法好像沒有加鎖啊:

public void print(String s) {
        if (s == null) {
            s = "null";
        }
        write(s);
    }

      

  那我們看看write方法,不好意思:

private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
  ......
    }