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
, 我们就可以看到
System
从源码中我们发现, 我们是调用 System
这个类的类属性 out
的 println
方法, 看看这个 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
结论是可行的, 那么我们就要看看 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 文件,
这是因为不同系统实现所需要的, 这里我的系统是 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.c
的 writeBytes
实现
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 编写代码执行
到这里, 我们就可以知道 Java 字符串是如何在控制台输出的了.
总结
控制台输出流程图