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 {
		......
	}
	......
}

现在已经知道了,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中找到了该方法

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了~~~

到此这篇关于OpenJDK源码解析之System.out.println详解的文章就介绍到这了,更多相关OpenJDK源码解析内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • yum安装openJDK1.8后无法卸载的快速解决方法

    一.问题描述 用yum安装openJDK1.8,安装后使用yum remove卸载,提示如下 Remove 1 Package(s) Installed size: 490 k Is this ok [y/N]: y Downloading Packages: Running rpm_check_debug Running Transaction Test Transaction Test Succeeded Running Transaction Erasing : 1:java-1.8.0-

  • OpenJDK源码调试图文教程

    前言      随着Java生态愈发庞大,各种各样的新技术层出不穷,这也给大家的学习带来了很多困惑,这么多技术我该学什么,盲目的在各种新技术间穿梭,并不能取得很好的效果.      作为Java核心技术的JDK相信很多同学都看过源码,了解过Java的内存模型,但是很多时候debug到最后都是 native,这是让人很沮丧的事情,于是乎了解JDK底层的实现变得极为重要.     编译OpenJDK源码的文章很多,但是很少有从头到尾搭建环境的文章,于是我这里就写了这篇文章,这里涉及的主要步骤: 虚拟

  • docker安装openjdk并运行jar包的操作方法

    下载镜像 docker pull openjdk 创建数据卷 创建一个java_app的数据卷 docker volume create java_app 将jar包上传到/var/lib/docker/volumes/java_app/_data/下,然后启动镜像 启动镜像 docker run --name=javaApp --restart=always --network=host \ -v java_app:/usr/src/myapp openjdk java -jar /usr/s

  • 如何卸载linux自带openjdk并安装sun jdk

    参见:https://www.jb51.net/article/112612.htm 检查一下系统中的jdk版本 [hadoop@master ~]$ java -version openjdk version "1.8.0_222-ea" OpenJDK Runtime Environment (build 1.8.0_222-ea-b03) OpenJDK 64-Bit Server VM (build 25.222-b03, mixed mode) 检测jdk安装包 [hadoo

  • 基于编译虚拟机jvm—openjdk的编译详解

    java只所以被推广,实际上很大原因是因为本身是跨平台的,很大作用是因为虚拟机的关系. 一般情况下开发人员不需要关注虚拟机内部实现就可以日常开发了,但是有时候涉及到性能的时候就需要了解虚拟机的实现机制了. 那么今天写的内容更多的是关于编译一套自己的虚拟机,为日后了解虚拟机底层原理铺铺路. 编译虚拟机可能会遇到很多坑,也很花费时间.也因大家的环境的差异,可能遇到的问题都不一致. 我只能说把自己遇到的问题都列出来,权当抛砖引玉了. 1首先我们应该下载openjdk的源码,这个openjdk实际上是有

  • 在CentOS系统上安装Java的openjdk的方法

    CentOS 6.X 和 5.X 自带有OpenJDK runtime environment  (openjdk).它是一个在linux上实现开源的java 平台.CentOS  yum 命令 安装 Java SDK openjdk centos linux JAVA(openjdk)软件包名 1.java-1.7.0-openjdk - OpenJDK Runtime Environment 2.java-1.7.0-openjdk-devel - OpenJDK Development E

  • Ubuntu如何轻松编译openJDK详解

    前言 花了三天在windows上搞openJDK,对bash本来就不熟,加上各种莫名依赖和脚本里的bug,身心俱疲.最后make all的时候产生一个莫名其妙的错误说什么有warning且-Werror置为了true,死活没google到-Werror是什么鬼,只好作罢了. 今天本菜鸟突然想起来,朕还有Ubuntu虚拟机,权且一试,没想到轻松成功. 平台:Ubuntu Version 5.1.30 (3.2.0-98-generic-pae #138-Ubuntu SMP Mon Jan 11

  • openjdk与Oraclejdk的区别

    在2006年11月13日的JavaOne大会上,Sun公司(当时还没被收购)宣布计划要把Java开源,在随后的一年多时间内,它陆续地将JDK的各个部分在GPL v2(GNU General Public License v2)协议下公开了源码,并建立了OpenJDK组织对这些源码进行独立管理.除了极少量的产权代码(Encumbered Code,这部分代码所有权不属于Sun公司,Sun本身也无权进行开源处理)外,OpenJDK几乎拥有了当时SunJDK 的全部代码. OpenJDK的质量主管曾经

  • Docker 解决openjdk容器里无法使用JDK的jmap等命令问题

    零.问题描述 项目:Java Spring Boot 项目 Docker 环境:Docker Toolbox.不是 Windows10,用不了 Docker for Windows 只能用这个. 使用 Docker Compose 编排服务,并启动 Mysql 和 Spring Boot 项目. docker-compose.yml 文件内容: version: '2' services: mysql: build: ./docker/builds/mysql ports: - "3386:33

  • 麒麟V10更换OpenJDK为Oracle JDK的方法

    1 简介 JDK(Java Development Kit)是 Java 平台编程中使用的软件开发环境.它包含一个完整的 Java 运行时环境,即所谓的私有运行时.该名称来自于它包含的工具多于独立的 JRE 以及开发 Java 应用程序所需的其他组件.常见的有 OpenJDK 和 Oracle JDK OracleJDK 根据 Oracle 二进制代码许可协议获得许可,而 OpenJDK 具有 GNU 通用公共许可证(GNU GPL)版本2. 使用 Oracle 平台时会产生一些许可影响.如 O

随机推荐