前言
我们在调试中,经常用到System.out.println()来打印日志,那它的内部是如何工作的?
System的静态属性out又是如何被初始化的?它的静态块中并没有给这些属性做初始化。
public final class System {
...
public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;
...
static {
registerNatives();
}
}
PS: 当时写这篇笔记,是为了看它 和 VM中的 全局变量tty (defaultStream 对象) 有什么关系,最后发现,他们之间,并没有什么关系。
1. System.out 何时被初始化?
java.lang.System 属于系统核心类,它的加载和初始化都是由VM来接管的,见4. VM是如何初始化的-create_vm()-步骤36 初始化核心类
更多的细节我们暂时不关注,现在知道是怎么一回事就行了。
// 调用System类的 initializeSystemClass()
call_initializeSystemClass(CHECK_0);
initializeSystemClass()
/**
* Initialize the system class. Called after thread initialization.
*/
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")));
...
}
// java/io/FileDescriptor.java
// 0、1、2 分别对应标准输入、标准输出、标准错误输出
public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);
private /* */ FileDescriptor(int fd) {
this.fd = fd; // int字段
}
2. print() 调用分析
java I/O 在设计上使用的是“装饰者”模式,调过来调过去,被被它绕晕了,忽略细节,抓住重点。
最终的输出还要落到 FileOutputStream fdOut上, 如果对过程不感兴趣,可以直接跳到2.3.1 PrintStream. write()上。
// java/io/PrintStream.java
private PrintStream(boolean autoFlush, OutputStream out, Charset charset) {
super(out); // 调用父类构造函数,将 FileOutputStream out 赋值给父类字段
this.autoFlush = autoFlush; // 初始化自动刷新标志
this.charOut = new OutputStreamWriter(this, charset); // 创建字符输出流
this.textOut = new BufferedWriter(charOut); // 创建缓冲字符输出流, 缓冲区是一个长度为8192的char数组。
}
public void print(String s) {
if (s == null) {
s = "null"; // 如果字符串为 null,设置为 "null"
}
write(s); // 调用 write 方法写入字符串
}
private void write(String s) {
try {
// 这里是为什么在生产中不建议使用System.out.print()的原因,
// System.out是静态属性,对应到这里的PrintStream 对象;
// 若某段高并发的代码使用了System.out.print(), 这里的锁会有激烈竞争,有性能消耗。
synchronized (this) { // 同步以确保线程安全
ensureOpen(); // 确保流是打开状态
textOut.write(s); // 将字符串写入缓冲字符输出流
textOut.flushBuffer(); // 刷新缓冲区
charOut.flushBuffer(); // 刷新字符输出流
// 如果启用了自动刷新且字符串中包含换行符,刷新输出流
if (autoFlush && (s.indexOf('\n') >= 0)) {
out.flush();
}
}
} catch (InterruptedIOException x) {
Thread.currentThread().interrupt(); // 恢复中断状态
} catch (IOException x) {
trouble = true; // 标记发生了 IO 错误
}
}
2.1. textOut.write(s);
// 父类 java/io/Writer.java
public void write(String str) throws IOException {
write(str, 0, str.length());
}
// java/io/BufferedWriter.java
public void write(String str, int off, int len) throws IOException {
se.write(str, off, len);
}
public void write(String s, int off, int len) throws IOException {
synchronized (lock) { // 使用锁对象以确保线程安全
ensureOpen(); // 确保流是打开状态
int b = off; // 当前索引,初始化为 off
int t = off + len; // 结束索引,等于 off + len
while (b < t) { // 只要当前索引小于结束索引
// 计算当前要写入的字符数,不能超过缓冲区剩余空间
int d = min(nChars - nextChar, t - b);
// 将字符串 s 中的字符复制到字符缓冲区 cb
s.getChars(b, b + d, cb, nextChar);
b += d; // 更新当前索引
nextChar += d; // 更新下一个字符索引
// 如果缓冲区已满,刷新缓冲区
if (nextChar >= nChars) {
flushBuffer();
}
}
}
}
2.2. textOut.flushBuffer()
// 将缓冲区中的字符数据写入输出流。
void flushBuffer() throws IOException {
synchronized (lock) { // 使用锁对象以确保线程安全
ensureOpen(); // 确保流是打开状态
// 如果没有字符需要写出,直接返回
if (nextChar == 0) {
return;
}
// 将字符缓冲区 cb 中的数据写入输出流 out
// 这里的out 就是构造时传入的 OutputStreamWriter charOut对象。
out.write(cb, 0, nextChar); // char cb[]
// 重置下一个字符索引
nextChar = 0;
}
}
2.2.1 charOut.write(cb, 0, nextChar);
/java/io/OutputStreamWriter.java
public void write(char cbuf[], int off, int len) throws IOException {
// 调用 StreamEncoder 的 write 方法,处理字符的实际写入。
se.write(cbuf, off, len);
}
/sun/nio/cs/StreamEncoder.java
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; // 如果长度为0,直接返回
}
// 调用实际写入实现方法
implWrite(cbuf, off, len);
}
}
void implWrite(char cbuf[], int off, int len) throws IOException {
// 将字符数组 cbuf 从 off 位置开始的 len 长度包装成 CharBuffer
CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
// 如果有剩余字符,先刷新这些字符
if (haveLeftoverChar) {
flushLeftoverChar(cb, false);
}
// 循环处理 CharBuffer 中的字符
while (cb.hasRemaining()) {
// 使用编码器将字符编码为字节,并存入字节缓冲区 bb
CoderResult cr = encoder.encode(cb, bb, false);
// 如果编码结果为下溢,表示没有更多可编码字符
if (cr.isUnderflow()) {
assert (cb.remaining() <= 1) : cb.remaining(); // 确保剩余字符数不超过1
if (cb.remaining() == 1) {
// 如果剩余字符为1,标记为有剩余字符,并保存
haveLeftoverChar = true;
leftoverChar = cb.get();
}
break; // 结束循环
}
// 如果编码结果为溢出,表示字节缓冲区已满
if (cr.isOverflow()) {
assert bb.position() > 0; // 确保字节缓冲区已有数据
writeBytes(); // 写入字节
continue; // 继续编码下一个字符
}
// 抛出编码异常
cr.throwException();
}
}
2.3 charOut.flushBuffer();
void implFlushBuffer() throws IOException {
if (bb.position() > 0) { // 检查缓冲区是否有数据
writeBytes(); // 如果有数据,调用写入字节的方法
}
}
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 {
// 这里的out 就是构造时传入的PrintStream对象
// 兜兜转转,终于快绕回来了。
out.write(bb.array(), bb.arrayOffset() + pos, rem); // 否则写入到输出流
}
}
bb.clear(); // 清空缓冲区
}
2.3.1 PrintStream. write()
// java.io/PrintStream.java
public void write(byte buf[], int off, int len) {
try {
synchronized (this) { // 使用锁对象以确保线程安全
ensureOpen(); // 确保流是打开状态
// 这里的out则是构造时传入的FileOutputStream对象;
// 终于是转出来了...
out.write(buf, off, len); // 将字节数组中的数据写入输出流
if (autoFlush) { // 如果自动刷新已启用
out.flush(); // 刷新输出流
}
}
} catch (InterruptedIOException x) {
Thread.currentThread().interrupt(); // 处理中断异常,恢复中断状态
} catch (IOException x) {
trouble = true; // 处理 IO 异常,设置 trouble 标志
}
}
// java/io/FileOutputStream.java
public void write(byte b[], int off, int len) throws IOException {
writeBytes(b, off, len, append);
}
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;
现在碰到了native方法,搜索: Java_java_io_FileOutputStream_writeBytes 可以很快找到对应的实现。
// solaris/native/java/io/FileOutputStream_md.c
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);
}
/*
env: JNI 环境指针。
this: 当前对象的引用。
bytes: 要写入的字节数组。
off: 字节数组的偏移量。
len: 要写入的字节长度。
append: 是否以追加模式写入。
fid: 字段 ID,用于获取文件描述符
*/
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; // 文件描述符
// 检查字节数组是否为 null
if (IS_NULL(bytes)) {
JNU_ThrowNullPointerException(env, NULL); // 抛出空指针异常
return;
}
// 检查偏移量和长度是否越界
if (outOfBounds(env, off, len, bytes)) {
JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL); // 抛出索引越界异常
return;
}
// 如果长度为 0,直接返回
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 = GET_FD(this, fid); // 获取文件描述符
if (fd == -1) { // 如果文件描述符无效
JNU_ThrowIOException(env, "Stream Closed"); // 抛出流已关闭异常
break;
}
// 根据 append 标志决定是追加还是写入
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; // 更新剩余长度
}
}
// 释放动态分配的缓冲区
if (buf != stackBuf) {
free(buf);
}
}
IO_Write(fd, buf + off, len);
将数据写入指定的文件描述符 fd.
最终是走到了write系统调用这里,再往下就是linux源码了,不在本章的探索范围了。
/solaris/native/java/io/io_util_md.c
/*
fd: 文件描述符,用于指定目标文件或设备。
buf: 指向要写入的数据缓冲区的指针。
len: 要写入的数据长度。
*/
ssize_t
handleWrite(FD fd, const void *buf, jint len)
{
ssize_t result;
// 使用 RESTARTABLE 宏进行可重启的写入操作,确保在系统调用被信号中断的情况下能够重新尝试写入。
// 这通常是在 POSIX 系统编程中使用的一种设计模式。
RESTARTABLE(write(fd, buf, len), result);
return result;
}
总结
java I/O 模块的实现,采用的“装饰者“模式,这属于设计模式中的”结构型模式”中的一个;
“装饰者模式”通过 动态地给一个对象添加额外的职责,同时不改变其结构,灵活的替代继承方式来扩展功能。
总结经验: 探究代码时要注意抓住重点,忽略细节。