Java - Hello world, 从源码到 JVM

说明

系统: Win10 2004 版本
Java: AdoptOpenJDK 14

源码

Hello world

你知道 Java 代码 System.out::println 方法是怎么打印字符串的吗?
让我们先来看看 Hello world 是怎么从 Java 代码到 JVM 内部是怎么个流程吧.

Main

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

接着我们编译 ps> javac Main.java 这个文件. 执行 ps> java Main, 我们就可以看到

Hello world

System

从源码中我们发现, 我们是调用 System 这个类的类属性 outprintln 方法, 看看这个 out 是什么类型.

java.lang.System

public final class System {
	// 省略...

	// 在源码中, 我们发现 out 是 PrintStream 类型, 修饰符为 public static final, 且被初始化为 null
	// 那就有个大问题, 被 final 修饰且初始化为 null, 那是怎么打印的呢?
	public static final PrintStream out = null;
	
	// 省略...
	
	// 原来 System 有个 native 方法, 可以在虚拟机内部修改 out 指向的对象,
	// 这个方法就是 setOut0, 那这个方法是什么时候被调用的呢?
	private static native void setOut0(PrintStream out);

	/**
     * Initialize the system class.  Called after thread initialization.
     * 初始化 System class 后, 并在线程也完成初始化调用.
     */
    private static void initPhase1() {
		// 省略...
		FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
		// 省略...
		setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
	}
	
	// 省略...

	/**
     * Create PrintStream for stdout/err based on 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);
    }

	// 省略...
}

看完上面 System 源码, 我心中不由产生了疑问: 我是不是可以自己写一个 System.out 实例?
Talk is cheap. Show me the code.

StdOut

import java.io.BufferedOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;

public class StdOut {
    public static void main(String[] args) {
    	// 注意这行代码, 这个是最终需要了解的地方
        FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
        BufferedOutputStream bos = new BufferedOutputStream(fdOut, 128);
        // 打印的都是 ASCII, 忽视编码
        PrintStream out = new PrintStream(bos, true);

        out.println("Hello StdOut");
    }
}

编译 ps> javac StdOut.java 运行 ps> java StdOut
StdOut
结论是可行的, 那么我们就要看看 PrintStream::println 方法到底做了些什么

PrintStream

我们从 PrintStream 的构造方法看起, 看看 PrintStream 内部有什么需要注意的

java.io.PrintStream

public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable {
	// 省略...

	private OutputStreamWriter charOut;
	private BufferedWriter textOut;

	// 我们使用的构造方法
	public PrintStream(OutputStream out, boolean autoFlush) {
        this(autoFlush, requireNonNull(out, "Null output stream"));
    }

	private PrintStream(boolean autoFlush, OutputStream out) {
		// 注意这个构造器, 这里实例化了相关类
		// FileOutputStream out
        super(out);
        this.autoFlush = autoFlush;
        this.charOut = new OutputStreamWriter(this);
        this.textOut = new BufferedWriter(charOut);
    }

	public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

	private void writeln(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                // 从这里可以看到, 在 PrintStream 里最终是调用这个方法
                textOut.write(s);
                textOut.newLine();
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

	// 省略...

	// 这个方法先不要看, 直接到 BufferedWriter,
	// 看完 StreamEncoder 再回来看这个方法
	@Override
    public void write(byte buf[], int off, int len) {
        try {
            synchronized (this) {
                ensureOpen();
                
                // private PrintStream(boolean autoFlush, OutputStream out)
                // FileOutputStream out
                // super(out);
                // 最后是调用 FileOutputStream::write 方法写入数据
                // 然后我们再去看看 FileOutputStream 类是怎么实现的
                out.write(buf, off, len);
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

	// 省略...
}

BufferedWriter

PrintStream 源码可知, 字符串在最后是作为参数传入了 BufferedWriter::write 方法, 而 write 方法是父类 Writer

java.io.Writer

public abstract class Writer implements Appendable, Closeable, Flushable {
	// 省略...
    public void write(String str) throws IOException {
        write(str, 0, str.length());
    }

	// 该方法主要是将字符串转为字符数组
	public void write(String str, int off, int len) throws IOException {
        synchronized (lock) {
            char cbuf[];
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else {    // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }
	
	// 该方法由子类实现, 处理字符数组
	public abstract void write(char cbuf[], int off, int len) throws IOException;
}

java.io.BufferedWriter

public class BufferedWriter extends Writer {
	// OutputStreamWriter
	private Writer out;

	// 还记得 PrintStream 的构造器是怎么实例化的吗?
	// OutputStreamWriter out
	public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
    }

	public BufferedWriter(Writer out, int sz) {
        super(out);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.out = out;
        cb = new char[sz];
        nChars = sz;
        nextChar = 0;
    }

	// 省略...
	
	public void write(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }

            if (len >= nChars) {
                /* If the request length exceeds the size of the output buffer,
                   flush the buffer and then write the data directly.  In this
                   way buffered streams will cascade harmlessly. */
                flushBuffer();
                
                // 看这里, 字符数组最终消费
                // OutputStreamWriter
                out.write(cbuf, off, len);
                
                return;
            }

            int b = off, t = off + len;
            while (b < t) {
                int d = min(nChars - nextChar, t - b);
                System.arraycopy(cbuf, b, cb, nextChar, d);
                b += d;
                nextChar += d;
                if (nextChar >= nChars)
                    flushBuffer();
            }
        }
    }
    
	// 省略...
}

OutputStreamWriter

BufferedWriter 源码中, 我们发现字符数组是交给了 OutputStreamWriter::write 方法, 所以接下来我们得看看 OutputStreamWriter 内部是怎么处理字符数组的.

java.io.OutputStreamWriter

public class OutputStreamWriter extends Writer {
	private final StreamEncoder se;

	// 省略...

	// PrintStream out
	public OutputStreamWriter(OutputStream out) {
        super(out);
        // 处理字符编码
        se = StreamEncoder.forOutputStreamWriter(out, this,
                Charset.defaultCharset());
    }

	public void write(char cbuf[], int off, int len) throws IOException {
		// 这里字符数组又交给了 StreamEncoder 
        se.write(cbuf, off, len);
    }

	// 省略...
}

StreamEncoder

StreamEncoder 是和编码相关的, 我们继续看处理方法

sun.nio.cs.StreamEncoder

public class StreamEncoder extends Writer {
	// 省略...

	// 保存字符数组编码后的字节数组
	private ByteBuffer bb;
	
	// 省略...

	// PrintStream out
	// 由 out 消费 bb
	private final OutputStream out;

	public static StreamEncoder forOutputStreamWriter(OutputStream out,
                                                      Object lock,
                                                      CharsetEncoder enc) {
        // PrintStream out
        return new StreamEncoder(out, lock, enc);
    }

	public void write(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
            implWrite(cbuf, off, len);
        }
    }

	void implWrite(char cbuf[], int off, int len) throws IOException {
        CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
        implWrite(cb);
    }

	void implWrite(CharBuffer cb) throws IOException {
        if (haveLeftoverChar) {
            flushLeftoverChar(cb, false);
        }

        while (cb.hasRemaining()) {
        	// 1. 注意这里, 这里将字符数组根据之前所选择的字符集编码 encode 为字节数组 bb
            CoderResult cr = encoder.encode(cb, bb, false);
            if (cr.isUnderflow()) {
                assert (cb.remaining() <= 1) : cb.remaining();
                if (cb.remaining() == 1) {
                    haveLeftoverChar = true;
                    leftoverChar = cb.get();
                }
                break;
            }
            if (cr.isOverflow()) {
                assert bb.position() > 0;
                // 2. 消费字节数组 bb
                writeBytes();
                continue;
            }
            cr.throwException();
        }
    }

	private void writeBytes() throws IOException {
        bb.flip();
        int lim = bb.limit();
        int pos = bb.position();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);

        if (rem > 0) {
        	if (ch != null) {
	            if (ch.write(bb) != rem)
    	            assert false : rem;
        	} else {
        		// 3. 最后看这里, 由 PrintStream out 的 write 方法输出
        		// 此时我们可以回去 PrintStream 标题里的代码看最后一个方法 write 
            	out.write(bb.array(), bb.arrayOffset() + pos, rem);
	        }
        }
        bb.clear();
    }

	// 省略...
}

FileOutputStream

PrintStream::write 的代码里我们可以知道, 最后字符串是转化为字节数组, 然后由 FileOutputStream::write 处理, 那我们看看 FileOutputStream 类又是怎么处理的吧.

java.io.FileOutputStream

public class FilterOutputStream extends OutputStream {
	/**
     * Access to FileDescriptor internals.
     */
    private static final JavaIOFileDescriptorAccess fdAccess =
        SharedSecrets.getJavaIOFileDescriptorAccess();

	/**
     * The system dependent file descriptor.
     * FileDescriptor.out = new FileDescriptor(1);
     */
    private final FileDescriptor fd;

	public FileOutputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkWrite(fdObj);
        }
        this.fd = fdObj;
        this.path = null;

        fd.attach(this);
    }

	// 省略...

	public void write(byte b[], int off, int len) throws IOException {
        writeBytes(b, off, len, fdAccess.getAppend(fd));
    }

	private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;

	// 省略...
}

到这里 Java 所有和控制台输出的代码就结束了, 接下来我们看看 JVM 里是怎么做的, 也就是看看 FileOutputStream::writeBytes 这个 native 方法是怎么实现的.

JVM

如何查看 JVM 内部的实现代码呢?
我们可以去 GitHub 下载 OpenJDK 的源码, 然后搜索 FileOutputStream, 当然, 也可以在 GitHub 上搜索, 但国内环境非常慢, 所以推荐下载下来本地搜索会快些.

FileOutputStream_md.c

检索完后我们会发现有搜出来的有两个和 FileOutputStream 相关的 C 文件,
FileOutputStream_md.c

这是因为不同系统实现所需要的, 这里我的系统是 Win10, 所以选择

src/java.base/windows/native/libjava/FileOutputStream_md.c

#include "jni.h"
#include "io_util.h"

/*******************************************************************/
/*  BEGIN JNI ********* BEGIN JNI *********** BEGIN JNI ************/
/*******************************************************************/

jfieldID fos_fd; /* id for jobject 'fd' in java.io.FileOutputStream */

/**************************************************************
 * static methods to store field ID's in initializers
 */

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_initIDs(JNIEnv *env, jclass fosClass) {
    fos_fd = (*env)->GetFieldID(env, fosClass, "fd", "Ljava/io/FileDescriptor;");
}

// 看这个方法
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
                                         jobject this, 
                                         jbyteArray bytes, 
                                         jint off, 
                                         jint len, 
                                         jboolean append) {
    writeBytes(env, this, bytes, off, len, append, fos_fd);
}

io_util.c

查看 FileOutputStream_md.cwriteBytes 实现

src/java.base/share/native/libjava/io_util.c

void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
           jint off, jint len, jboolean append, jfieldID fid)
{
    jint n;
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;

	// 省略...

    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return;
        }
    } else {
        buf = stackBuf;
    }

    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        off = 0;
        while (len > 0) {
        	// 获取标准输出文件描述符
            fd = getFD(env, this, fid);
            if (fd == -1) {
                JNU_ThrowIOException(env, "Stream Closed");
                break;
            }
            if (append == JNI_TRUE) {
                n = IO_Append(fd, buf+off, len);
            } else {
            	// 看这里, 输出缓冲数组
                n = IO_Write(fd, buf+off, len);
            }
            if (n == -1) {
                JNU_ThrowIOExceptionWithLastError(env, "Write error");
                break;
            }
            off += n;
            len -= n;
        }
    }
    
    // 省略...
}

io_util_md

这里, 我们发现 IO_Write 是一个宏

src/java.base/windows/native/libjava/io_util_md.h

#define IO_Write handleWrite

jint handleWrite(FD fd, const void *buf, jint len);

实现

src/java.base/windows/native/libjava/io_util_md.c


#include "io_util_md.h"

jint handleWrite(FD fd, const void *buf, jint len) {
    return writeInternal(fd, buf, len, JNI_FALSE);
}

static jint writeInternal(FD fd, const void *buf, jint len, jboolean append)
{
    BOOL result = 0;
    DWORD written = 0;
    HANDLE h = (HANDLE)fd;
    if (h != INVALID_HANDLE_VALUE) {
        OVERLAPPED ov;
        LPOVERLAPPED lpOv;
        if (append == JNI_TRUE) {
            ov.Offset = (DWORD)0xFFFFFFFF;
            ov.OffsetHigh = (DWORD)0xFFFFFFFF;
            ov.hEvent = NULL;
            lpOv = &ov;
        } else {
            lpOv = NULL;
        }

		// 重点看这个函数 WriteFile
        result = WriteFile(h,                /* File handle to write */
                           buf,              /* pointers to the buffers */
                           len,              /* number of bytes to write */
                           &written,         /* receives number of bytes written */
                           lpOv);            /* overlapped struct */
    }
    if ((h == INVALID_HANDLE_VALUE) || (result == 0)) {
        return -1;
    }
    return (jint)written;
}

最后, 调用系统函数

WINBASEAPI
BOOL
WINAPI
WriteFile(
    _In_ HANDLE hFile,
    _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToWrite,
    _Out_opt_ LPDWORD lpNumberOfBytesWritten,
    _Inout_opt_ LPOVERLAPPED lpOverlapped
);

我们使用 Visual Studio 编写代码执行
WriteFile
到这里, 我们就可以知道 Java 字符串是如何在控制台输出的了.

总结

控制台输出流程图
String 转换流程图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值