How System.out.println() really works

How System.out.println() really works

A few days ago I came across an interesting article, Where the printf() Rubber Meets the Road, describing how the printf function ‘works’ on the low level.

Commonly asked by Java beginners is the question, “How does System.out.println() work?”; the above blog post inspired me to do some research into this question. In this blog post I’ll attempt to provide an explanation of what System.out.println() does behind the scenes.

Most of the relevant code can be found on OpenJDK, which is the primary implementation of Java itself. System.out.println() may be implemented completely differently on other Java platforms.

I will warn you now that this article is very long and not for the easily bored.

First steps

Our first step in figuring out how System.out.println works is by first understanding what System.out is and how it came to be.

Let’s take a look through the OpenJDK’s Mercurial online repository. Digging around a bit, we find System.java. In this file System.out is declared:

public final static PrintStream out = nullPrintStream();

But when we find the code for nullPrintStream():

private static PrintStream nullPrintStream() throws NullPointerException {
     if (currentTimeMillis() > 0 ) {
         return null ;
     }
     throw new NullPointerException();
}

So nullPrintStream() simply returns null or throws an exception. This can’t be it. What’s going on here?

The answer can be found in the function initializeSystemClass(), also in System.java:

FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
setOut0( new PrintStream( new BufferedOutputStream(fdOut, 128 ), true ));

There’s a lot of stuff going on in this code. I’m going to refer back to this two lines of code later, but setOut0() is what actually initializes System.out.

The function setOut0() is a native function. We can find its implementation in 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);
}

This is pretty standard JNI code that sets System.out to the argument passed to it.

At first all this deal with setting System.out to nullPrintStream() and later setting it with JNI seems entirely unnecessary. But this is actually justified.

In Java, static fields are initialized first, and everything else comes after. So even before the JVM and the System class is fully initialized, the JVM tries to initialize System.out.

Unfortunately at this point the rest of the JVM isn’t properly initialized so it’s impossible to reasonably set System.out at this point. The best that could be done would be to set it to null.

The System class, along with System.out is properly initialized in initializeSystemClass() which is called by the JVM after static and thread initialization.

There is a problem, however. System.out is final, meaning we cannot simply set it to something else in initializeSystemClass(). There’s a way around that, however. Using native code, it is possible to modify a final variable.

Wait, what’s a FileDescriptor?

Notice this line of code:

FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);

A FileOutputStream object is created from something referred to as FileDescriptor.out.

The FileDescriptor class, though part of java.io, is rather elusive. It can’t be found in the java.io directory in OpenJDK.

This is because FileDescriptor is much lower level than most of the Java standard library. While most .java files are platform independent, there are actually different implementations of FileDescriptor for different platforms.

We’ll be using the Linux/Solaris version of FileDescriptor.java.

A FileDescriptor object is very simple. Essentially all it really holds is an integer. It holds some other data too, which aren’t really important. The constructor of FileDescriptor takes an integer and creates a FileDescriptor containing that integer.

The only use of a FileDescriptor object is to initialize a FileOutputStream object.

Let’s see how FileDescriptor.out is defined:

public static final FileDescriptor out = new FileDescriptor( 1 );

FileDescriptor.out is defined as 1, in as 0, and err as 2. The basis of these definitions are from a very low level somewhere in Unix.

We now know how System.out is initialized. For now, we’re going to leave behind the FileDescriptor; we only need to know what it does.

A tour through java.io

Now we redirect our attentions to the println() function of PrintStream.

PrintStream is a comparably higher level class, capable of writing many different kinds of data, flushing and handling errors for you without much effort.

Let’s see how println() is defined in PrintStream.java:

public void println(String x) {
     synchronized ( this ) {
         print(x);
         newLine();
     }
}

Following the call stack to print():

public void print(String s) {
     if (s == null ) {
         s = "null" ;
     }
     write(s);
}

Going deeper, and looking at 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();
         }
     }
     catch (InterruptedIOException x) {
         Thread.currentThread().interrupt();
     }
     catch (IOException x) {
         trouble = true ;
     }
}

Internally, the PrintStream object (System.out) contains three different objects to do its work:

  • The OutputStreamWriter object (charOut), writing character arrays into a stream
  • The BufferedWriter object (textOut), writing not only character arrays but also strings and text
  • A BufferedOutputStream object (out), passed all the way down the call stack and used much lower then at the PrintStream level

We can see that PrintStream.write() calls BufferedWriter.write() and flushes both buffers. I’m not sure why it’s necessary to flush the charOut buffer, so I’m going to ignore that.

Delving deeper, let’s find the implementation of write() inBufferedWriter.java.. wait it’s not here. The function write(String) is actually defined in the abstract class Writer.java:

public void write(String str) throws IOException {
     write(str, 0 , str.length());
}

Moving back to BufferedWriter:

public void write(String s, int off, int len) throws IOException {
     synchronized (lock) {
         ensureOpen();
 
         int b = off, t = off + len;
         while (b < t) {
             int d = min(nChars - nextChar, t - b);
             s.getChars(b, b + d, cb, nextChar);
             b += d;
             nextChar += d;
             if (nextChar >= nChars)
                 flushBuffer();
         }
     }
}

As its name suggests, BufferedWriter is buffered. Data is stored in a data buffer until it’s written all at once, or flushed. Buffered IO is much faster than simply writing to the hardware one byte at a time.

The function BufferedWriter.write() doesn’t actually write anything. It only stores something in an internal buffer. The flushing is not done here, but back at PrintStream.write().

Let’s go to flushBuffer(), in the same file:

void flushBuffer() throws IOException {
     synchronized (lock) {
         ensureOpen();
         if (nextChar == 0 )
             return ;
         out.write(cb, 0 , nextChar);
         nextChar = 0 ;
     }
}

We find yet another write() call, on a Writer object (out). The out object here is the charOut object of PrintStream, and has the type OutputStreamWriter. This object is also the same object as charOut in PrintStream.

Let’s look at OutputStreamWriter.write() in OutputStreamWriter.java:

public void write( char cbuf[], int off, int len) throws IOException {
     se.write(cbuf, off, len);
}

This now transfers the job to another object, se. This object is of type sun.nio.cs.StreamEncoder. We’re going to leave the java.io directory for a while.

Let’s see the implementation of StreamEncoder.write() inStreamEncoder.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 ;
         }
         implWrite(cbuf, off, len);
     }
}

Moving on to StreamEncoder.implWrite():

void implWrite( char cbuf[], int off, int len)
     throws IOException
{
     CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
 
     if (haveLeftoverChar)
         flushLeftoverChar(cb, false );
 
     while (cb.hasRemaining()) {
         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 ;
             writeBytes();
             continue ;
         }
         cr.throwException();
     }
}

Again this calls another function, writeBytes(). Here’s the implementation:

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.write(bb.array(), bb.arrayOffset() + pos, rem);
         }
     }
     bb.clear();
}

We’re done with StreamEncoder. This class essentially processes or encodes character streams, but ultimately delegates the task of writing the bytes back to BufferedOutputStream.

Let’s take a look at the code for write() in BufferedOutputStream.java:

public synchronized void write( byte b[], int off, int len) throws IOException {
     if (len >= buf.length) {
         /* If the request length exceeds the size of the output buffer,
            flush the output buffer and then write the data directly.
            In this way buffered streams will cascade harmlessly. */
         flushBuffer();
         out.write(b, off, len);
         return ;
     }
     if (len > buf.length - count) {
         flushBuffer();
     }
     System.arraycopy(b, off, buf, count, len);
     count += len;
}

And BufferedOutputStream passes the baton again, this time to FileOutputStream. Remember when we instantiated fdOut as a FileOutputStream? Well, this is it, passed down through dozens of system calls.

Believe it or not, FileOutputStream is the final layer before JNI. We see the function write() in FileOutputStream.java:

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

And writeBytes():

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

We’ve reached the end of the Java part. But we’re not quite finished.

A Review of the java.io call stack

This is a ‘contains’ chart:

Also here’s the entire call stack:

Stepping into the JNI

After FileOutputStream, the writing of bytes to the console is handled natively. Much of this native code is platform dependent: there are different versions of the code for Windows and Linux. We’re going to deal with the Linux versions first.

The native implementation of writeBytes() is defined inFileOutputStream_md.c.

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
     jobject this , jbyteArray bytes, jint off, jint len) {
     writeBytes(env, this , bytes, off, len, fos_fd);
}

The field fos_fd is the integer stored in the FileDescriptor object that we’ve visited so long ago. So for the out stream, fos_fd should be 1.

We’re just calling a method, writeBytes, with the additional argument of fos_id. The implementation of writeBytes() is defined in io_util.c:

void
writeBytes(JNIEnv *env, jobject this , jbyteArray bytes,
            jint off, jint len, jfieldID fid)
{
     jint n;
     char stackBuf[BUF_SIZE];
     char *buf = NULL;
     FD fd;
 
     if (IS_NULL(bytes)) {
         JNU_ThrowNullPointerException(env, NULL);
         return ;
     }
 
     if (outOfBounds(env, off, len, bytes)) {
         JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException" , NULL);
         return ;
     }
 
     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 ;
             }
             n = IO_Write(fd, buf+off, len);
             if (n == JVM_IO_ERR) {
                 JNU_ThrowIOExceptionWithLastError(env, "Write error" );
                 break ;
             } else if (n == JVM_IO_INTR) {
                 JNU_ThrowByName(env, "java/io/InterruptedIOException" , NULL);
                 break ;
             }
             off += n;
             len -= n;
         }
     }
     if (buf != stackBuf) {
         free (buf);
     }
}

The writing here is done by a method called IO_Write. At this point, what happens next becomes platform dependent, as IO_Write is defined differently for Windows and Linux.

The Linux Way

The linux way of handling IO uses the HPI (Hardware Platform Interface). Thus, the method is defined as JVM_Write in io_util_md.h:

#define IO_Write JVM_Write

The code form JVM_Write is defined in the JVM itself. The code is not Java, nor C, but it’s C++. The method can be found in jvm.cpp:

JVM_LEAF(jint, JVM_Write(jint fd, char *buf, jint nbytes))
   JVMWrapper2( "JVM_Write (0x%x)" , fd);
 
   //%note jvm_r6
   return (jint)hpi::write(fd, buf, nbytes);
JVM_END

The writing is now done by various HPI methods. Although you could go further, I’m going to stop here, since we’re now so far from where we started.

The Way of Windows

In Windows, the method IO_Write is routed away from the HPI layer. Instead, it’s redefined as handleWrite in io_util_md.h.

The implementation for handleWrite() is defined in io_util_md.c:

JNIEXPORT
size_t
handleWrite(jlong fd, const void *buf, jint len)
{
     BOOL result = 0;
     DWORD written = 0;
     HANDLE h = ( HANDLE )fd;
     if (h != INVALID_HANDLE_VALUE) {
         result = WriteFile(h,           /* File handle to write */
                       buf,              /* pointers to the buffers */
                       len,              /* number of bytes to write */
                       &written,         /* receives number of bytes written */
                       NULL);            /* no overlapped struct */
     }
     if ((h == INVALID_HANDLE_VALUE) || (result == 0)) {
         return -1;
     }
     return written;
}

The WriteFile function is in the Windows API. The Windows API is not open source, so we would have to stop here.

Conclusion

We’ve taken a tour through the entire call stack of System.out.println(), from the instantiation of System.out to the path through java.io, all the way down to JNI and HPI level IO handling.

We haven’t even reached the end. No doubt there’s dozens more levels underneath the HPI layer and the WriteFile API call.

Perhaps the better answer to the question, “how does System.out.println() work” would be “it’s magic”.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值