深入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 = ,结果如下:
完犊子,整个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();
}
}
......
}
四、总结
不知道有没有小伙伴经常在项目中使用System.out.println来输出"日志"?千万不要乱用哟,不然说不定哪天就被out了~~~
————————————————
版权声明:本文为CSDN博主「黄智霖-blog」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/huangzhilin2015/article/details/115311271
粉丝福利:Java从入门到入土学习路线图
👇👇👇
👆长按上方微信二维码 2 秒
感谢点赞支持下哈