Moment For Technology

How are exceptions handled? The answer is not in the source code.

Posted on Dec. 2, 2022, 6:28 p.m. by Stephen Godfrey-Curtis
Category: The back-end Tag: The back-end java

Hello, I am crooked.

Take this reader:

Did he read my "God! Abnormal information suddenly disappeared?" The question that arises after this article.

Since I read my article to bring further thinking, incidentally, I happen to know.

Although this kind of article read less people, but I still to fill a pit.

Yeah, that's a really warm stone hammer.

How the exception is thrown.

Let's start with a simple snippet:

The results are very familiar.

There is nothing of value to be explored just by looking at these few lines of code.

We all know it works like this, and there's nothing wrong with it.

That's what I know.

So why?

And so, hidden in the bytecode behind the code.

When compiled with JavAP, the bytecode of the above code looks like this:

Let's focus on the following section, and I'll also comment on the meaning of the bytecode instructions:

public static void main(java.lang.String[]); Code: 0: iconst_1 // Push an int to the top of the stack 1: iconst_0 // push an int to the top of the stack 2: idiv // Divide two ints on the top of the stack and push the result to the top of the stack 3: Istore_1 // store the stack top int in the second local variable 4: return // return void from the current methodCopy the code

Don't ask me how I know the meaning of bytecode, just look at the table, no one can memorize it.

Bytecode doesn't seem to make any sense either.

But remember this, and I'll show you a transformation:

public class MainTest { public static void main(String[] args) { try { int a = 1 / 0; } catch (Exception e) { e.printStackTrace(); }}}Copy the code

Wrap the code with a try-catch to catch the exception.

After compiling with Javap again, the bytecode looks like this:

You can clearly see that the bytecode has changed, or at least it's getting longer.

I'm going to focus on what I've framed.

Let's compare the bytecode in both cases:

It is clear that the bytecode instruction is more than one line after the addition of a try-catch.

What's not in the box are the extra bytecode instructions.

And the extra part, one of which is called the Exception table, is particularly obvious:

The exception table, this is what the JVM uses to handle exceptions.

As for the meaning of each parameter here, we bypassed the "secondary" information on the Internet and went to the official website to find the document:

Docs.oracle.com/javase/spec...

It looks like a lot of English, a lot of pressure, but don't be afraid, I have, I will pick the key to you.

Start_pc and end_pc are a pair of parameters, corresponding to from and to in the Exception table, indicating the coverage of the Exception.

For example, if from is 0 and to is 4, the index of the bytecode covered by the exception is this range:

0: iconst_1 // pushes the int 1 to the top of the stack 1: iconst_0 // pushes the int 0 to the top of the stack 2: idiv // divides the top int and pushes the result to the top of the stack 3: istore_1 // stores the top int to the second local variableCopy the code

There's a detail. I don't know if you noticed.

The range does not contain 4. The range is [start_pc, end_pc].

It's interesting why it doesn't include end_pc.

Let's talk about it.

The fact that end_pc is exclusive is a historical mistake in the design of the Java Virtual Machine: if the Java Virtual Machine code for a method is exactly 65535 bytes long and ends with an instruction that is 1 byte long, then that instruction cannot be protected by an exception handler. A compiler writer can work around this bug by limiting the maximum size of the generated Java Virtual Machine code for any method, instance initialization method, or static initializer (the size of any code array) to 65534 bytes.

Not including end_PC is a historical error in the JVM design process.

Because if a method in the JVM compiles code that is exactly 65535 bytes long and ends with an instruction that is 1 byte long, that instruction is not protected by exception handling.

Compiler authors can resolve this error by limiting the maximum length of code generated by any method, instance initializer, or static initializer.

Above is the explanation of the official website, anyway, is to see the half - understand.

It doesn't matter, just run an example:

When THERE is only one method in my code and the length is 16391 lines, the compiled bytecode is 65532 lines long.

And we know from the previous analysis that a line of a=1/0 will be compiled into four lines of bytecode.

So if I add one more line of code, I'm out of limit, and I compile the code, what's going to happen?

Look at the picture:

Direct compilation fails, telling you the code is too long.

So you now know that the length of a method, in terms of bytecode, is limited. But this limit is quite large, and no normal person can write code of this length.

This doesn't make much sense, but if you ever come across a method at work that's thousands of lines long, even if it doesn't trigger a bytecode length limit, I have a word for you: Run.

The next parameter, handler_PC, corresponds to the target in the Exception table.

It's pretty easy to understand, it's the index of the instruction that started the exception handler.

For example, target 7 corresponds to astore_1:

This tells the JVM to start processing from here if there is an exception.

Finally, see the catch_type parameter, corresponding to the Exception table type.

This is the exception that the program caught.

For example, I modify the program to catch three types of exceptions:

The compiled bytecode exception table can handle these three types:

Why can't I write a String here?

Don't ask. It's grammar to ask.

What is it?

Right here in the exception table:

The compiler checks to see if the class is Throwable or a subclass of Throwable.

Throwable, Exception, Error, and RuntimeException will not be mentioned in detail. We will generate an inheritance diagram to show you:

So, here's the message:

  • From: the starting point at which exceptions may occur Instruction index subscript (included)
  • To: End point instruction index subscript where an exception may occur (excluding)
  • Target: In the range from and to, the instruction index subscript to begin handling the exception after the exception occurs
  • Type: indicates the exception class information that can be handled by the current range

Now that you know the exception list, you can answer the question: How do exceptions get thrown?

The JVM threw it for us through the exception table.

What's in the exception table?

As I said before, I won't repeat it.

How to use an exception table?

A brief description:

1. If an exception occurs, the JVM will look for the exception table in the current method to see if the exception was caught. 2. If an exception is found in the exception table, the target index is called and the execution continues.

Ok, so here we go again. What if the exception doesn't match?

I found the answer here in the official website document:

Docs.oracle.com/javase/spec...

The sample code looks like this:

Then there's this description:

If a value does not match any of the catch clauses, the Java VIRTUAL machine will rethrow the value without calling any of the catch clauses.

What do you mean?

Basically, I can't handle it anyway, so I'm going to throw the exception to the caller.

This is common programming knowledge, and of course everyone knows it.

But when something common sense is presented to you in this normative way, it's kind of amazing.

When someone asks you why the call process is this way, you say it's the rule.

When someone asks you where the rules are, you can throw the official website document in their face and point: Here it is.

Although, it doesn't seem to work.

Slightly special case

Finally:

public class MainTest { public static void main(String[] args) { try { int a = 1 / 0; } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("final"); }}}Copy the code

After javAP compilation, three records appear in the exception table section:

The first is that we actively catch exceptions.

The number two is any. What are these?

Here's the answer:

Docs.oracle.com/javase/spec...

Mainly look at where I draw the line:

A try statement with a finally clause is compiled to have a special exception handler that can handle (any) any exception thrown in the try statement.

So, to translate the above table of exceptions:

  • If an Exception of type Exception occurs between instructions 0 through 4, the instruction with index 15 is called to begin handling the Exception.
  • If any exception occurs between instructions 0 and 4, the instruction with index 31 is called (where the finally block begins)
  • If between instructions 15 and 20 (that is, the part of the catch), the instruction with index 31 is called regardless of any exception.

Then, let's look at this part:

What do you think? You see? Is god magic?

The output statement appears only once in the finally block in the source code and three times in the bytecode.

The code in the finally block is copied twice, one after the try and one after the catch statements. When used in conjunction with the exception table, the finally statement must be executed.

You'll never be afraid of your interviewer asking you why you finally did it.

No interviewer would ask such a boring question, though.

When asked, give him a bytecode analysis.

Of course, there's not much point in talking about System.exit if you have to give me a leg up.

Finally, about finally, let's talk about this scenario again:

public class MainTest { public static void main(String[] args) { try { int a = 1 / 0; } finally { System.out.println("final"); }}}Copy the code

In this case, nothing is said. The try throws an exception that triggers the output statement finally, which is then thrown out and printed to the console:

What if I add a return to finally?

As you can see, there are no exceptions thrown in the result:

Why is that?

The answer lies in bytecode:

In fact, it's pretty clear.

The finally on the right has a return in it, but no athrow, so the exception is not thrown at all.

This is one of the reasons why it is recommended not to use a return ina finally statement.

Cold knowledge

Just to give you a little tidbit about exceptions.

The same screenshot as above. Do you think it's a little weird?

In the dead of night, have you ever thought about this question:

There is no place in the program to print the log, so who prints the console's days and where?

Who did it?

This is an easy question to answer, and you can guess that the JVM did it for us.

Where is it?

The answer to this question is hidden here in the source code, and I'll give you a break point to run it, although I recommend you to run it as well:

java.lang.ThreadGroup#uncaughtException

This is where you can put a breakpoint and follow the call stack to find it:

java.lang.Thread#dispatchUncaughtException

Look at the comment on the method:

This method is intended to be called only by the JVM.

This method can only be called by the JVM.

Since the source has said so, we can go to find the corresponding source.

Hg.openjdk.java.net/jdk7u/jdk7u...

The openJdk thread. CPP source code does find where this method is called:

And there's an interesting use for this.

Look at the following program and output:

We can customize the current thread's UncaughtExceptionHandler to do some padding inside.

Is there a hint of global exception handling?

All right, one last question:

If I asked you that, the answer must be no.

Think about it. Use your little brain and think about it. When does the code inside a try throw an exception that can't be caught by a catch?

Here, look at the picture:

Didn't you think?

If we do that, we can't catch the exception outside.

Do you really want to hit me.

Don't panic. These dolls are so boring up there.

Look at this code:

public class MainTest { public static void main(String[] args) { try { ExecutorService threadPool = Executors.newFixedThreadPool(1); threadPool.submit(()-{ int a=1/0; }); } catch (Exception e) { e.printStackTrace(); }}}Copy the code

You just take it and execute it, and the console doesn't get any output.

Check out the GIF:

Isn't that amazing?

Don't panic. There's more.

Alter threadPool. Execute from threadPool. Submit to threadPool. Execute

But if you look closely, you'll see that the exception message is printed, but not because of the catch block.

Why exactly?

See this article, which I covered in detail earlier: "One last interview question about Throwing exceptions in Multiple Threads!"

One last word

All right, let's give it a like here. Thank you for your reading, I insist on the original, very welcome and thank you for your attention.

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.