System.out 的工作机制

前言

我们在调试中,经常用到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 模块的实现,采用的“装饰者“模式,这属于设计模式中的”结构型模式”中的一个;

“装饰者模式”通过 动态地给一个对象添加额外的职责,同时不改变其结构,灵活的替代继承方式来扩展功能

总结经验: 探究代码时要注意抓住重点,忽略细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FTC9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值