Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to analyze the principle of JVM method overloading and method rewriting

2025-01-30 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Internet Technology >

Share

Shulou(Shulou.com)06/01 Report--

This article will explain in detail how to carry out JVM method reloading and method rewriting principle analysis, the content of the article is of high quality, so the editor will share it for you to do a reference. I hope you will have a certain understanding of the relevant knowledge after reading this article.

Preface

JVM executes bytecode instructions based on the stack architecture, that is, all operands must be put into the stack first, and then operate and calculate as needed, and then put the results into the stack. This process is essentially different from the register-based architecture, and based on the register architecture, it may not be completely compatible on different machines, which is one of the reasons why Java will choose the stack-based design.

Thinking

Let's think about how the parameters are passed, how the return values are saved when we call a method, and how do we proceed to the next method call after a method call. It is certain that some information such as the parameters and return values of the method will be stored during the call. where is the information stored?

As we mentioned in JVM series 1, each call to a method produces a stack frame, so we can certainly imagine that the stack frame stores all the data that needs to be used during the call. Now let's take a closer look at the stack frames in the Java virtual machine stack.

Stack frame

When we call a method, a stack frame is generated, and when a method call is completed, its corresponding stack frame is destroyed, whether the completion is normal or sudden (an uncaught exception is thrown).

Each stack frame includes a local variable table (Local Variables), Operand stack (Operand Stack), dynamic link (Dynamic Linking), method return address (Return Address), and additional information.

In a given thread, only one stack frame is active, so the active stack frame is called the current stack frame, and the corresponding method is called the current method, and the class that defines the current method is called the current class. When a method call ends, its corresponding stack frame is also discarded.

Local variable scale (Local Variables)

The local variable table is stored as an array, and the maximum length that needs to be allocated by the current stack frame method is determined at compile time. The local variable table is addressed by index, and the variable is passed from index [0].

In the array of local variables, each location can hold a 32-bit data type: boolean, byte, char, short, int, float, reference, or returnAddress. The 64-bit data types long and double need two locations to store, but because the local variable table is private to the thread, although it is divided into two variable storage, there is still no need to worry about security problems.

For 64-bit data types, if they occupy the index [n] and index [n + 1] positions in the array, then individual access to one of these locations is not allowed. The Java virtual machine specification stipulates that if a 64-bit data is accessed separately, an exception should be thrown during the check phase of the class loading mechanism.

The Java virtual machine uses local variables to pass parameters when the method is called. In a class method (static method) call, all parameters are passed starting with index [0] in the local variable. In the instance method call, index [0] is used to pass the object instance to which the method belongs, and all other parameters are passed from the position of index [1] in the local variable table.

Note: the variables in the local variable table cannot be used directly. If necessary, they must be loaded into the Operand stack as operands through relevant instructions before they can be used.

Operand stack (Operand Stacks)

Operand stack, which can also be called operation stack (Operand Stack) when the context semantics is clear, is a last-in, first-out (Last In First Out,LIFO) stack. Like the local variable table, the maximum depth of the Operand stack is also determined at compile time.

The Operand stack is empty when it is first created (that is, when the method is first executed), and then, during the execution of the method, the constant / value from the local variable table or field is loaded into the Operand stack through virtual machine instructions. then operate on it, and press the operation result into the stack.

Each entry on the Operand stack can hold values of any Java virtual machine type, including values of type long or double.

Note: we must manipulate the values in the Operand stack in a way that suits their type. For example, it is not possible to push the values of two int types into the stack and treat them as long types, nor can you press two float type values into the stack and use the iadd instruction to add them.

Dynamic connection (Dynamic Linking)

Each stack frame contains a reference to the method to which the stack frame belongs in the runtime constant pool, which is held to support dynamic connections during method calls.

There are a large number of symbolic references in the constant pool in the Class file, and the method call instructions in the bytecode take the symbolic references pointing to the method in the constant pool as parameters, and some of these symbolic references will be converted into direct references in the class loading phase or the first time they are used, which is called static parsing. The other part is converted to direct reference only during each run, which is called dynamic connection.

Method returns the address

When a method starts to execute, there are only two ways to exit: one is to encounter the bytecode instruction returned by the method, and the other is to encounter an exception that is not handled in the method body.

Normal exit (Normal Method Invocation Completion)

If the call to the current method completes normally, a value may be returned to the calling method. When the called method executes one of the return instructions, the selection of the return instruction must match the type of the returned value, if any.

When the method exits normally, the current stack frame restores the state of the calling program by properly setting the caller's pc program counter and skipping the current calling instruction, including its local variable table and Operand stack. Then continue to execute the subsequent process at the stack frame of the calling method, and press the return value into the Operand stack if there is a return value.

Abnormal termination (Abrupt Method Invocation Completion)

If the execution of the Java virtual machine instruction in the method causes the Java virtual machine to throw an exception and the exception is not handled in the method, the method call ends abruptly because the method ends abruptly because the method caused by the exception will never have a return value to its caller.

Other additional information

This part depends on how the virtual machine manufacturer implements it, and the virtual machine specification does not describe this part.

Method invocation process demonstration

The above concept sounds a little abstract, so let's demonstrate the execution flow of the method through a simple example.

Package com.zwx.jvm;public class JVMDemo {public static void main (String [] args) {int sum = add (1,2); print (sum);} public static int add (int a, int b) {a = 3; int result = a + b; return result;} public static void print (int num) {System.out.println (num);}}

To understand the execution flow of the Java virtual machine, we must compile the class, get the bytecode file, and execute the following command

Javap-c xxx\ xxx\ JVMDemo.class > 1.txt

Output the bytecode instruction generated by JVMDemo.class to the 1.txt file, then open it, and see the following bytecode instruction:

Compiled from "JVMDemo.java" public class com.zwx.jvm.JVMDemo {public com.zwx.jvm.JVMDemo (); Code: 0: aload_0 1: invokespecial # 1 / / Method java/lang/Object. "": () V 4: return public static void main (java.lang.String []) Code: 0: iconst_1 1: iconst_2 2: invokestatic # 2 / / Method add: (II) I 5: istore_1 6: iload_1 7: invokestatic # 3 / / Method print: (I) V 10: return public static int add (int, int) Code: 0: iconst_3 1: istore_0 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn public static void print (int); Code: 0: getstatic # 4 / / Field java/lang/System.out:Ljava/io/PrintStream 3: iload_0 4: invokevirtual # 5 / / Method java/io/PrintStream.println: (I) V 7: return}

If you are in contact for the first time, you may not understand the instructions, but the general class structure is still very clear. Let's first outline the bytecode instructions used:

Iconst_i

It means to press the integer number I into the Operand stack. Note that the return of I is only-1 to 5. If it is not in this range, other instructions will be used, such as the bipush instruction will be used when the value range of int is [- 128127].

Invokestatic

Indicates that a static method is called

Istore_n

This means that an integer number is stored in the index n position of the local variable table, because the local variable table stores variables in the form of an array

Iload_n

Indicates that the variable of the local variable position n is pushed into the Operand stack

Ireturn

Returns the result of the current method to the previous stack frame

Invokevirtual

Call virtual method

Now that we know the general meaning of bytecode instructions, let's demonstrate the main execution processes:

1. After compiling the code, you will roughly get the following Java virtual machine stack. Note that the Operand stack is empty at this time (the value of the pc register is not considered here. In fact, the pc register will change all the time in the process of calling instructions.)

2. Execute the iconst_1 and iconst_2 instructions, that is, press the integers 1 and 2 into the Operand stack from the local variables:

4. Call the iconst_3 instruction in the frame of the add stack to press integer 3 into the Operand stack from the local variable.

6. Call iload_0 and iload_1 to press the variables of index [0] and index [1] in the local variable table into the Operand stack

8. Execute the istore_2 instruction, eject the current stack top element into the location of the local variable table index [2], and call iload_2 again to press the data of the index [2] position into the Operand stack from the local variable table.

9. Finally, execute the ireturn command to return the result 5 to the main stack frame, where the stack frame add is destroyed, and then go back to the main stack frame to continue the subsequent execution.

The call to the method is roughly the process of constantly getting into and out of the stack. The above process omits a lot of details and only pays attention to the general flow. The actual call is much more complex than in the diagram.

Method call analysis

We know that Java is an object-oriented language that supports polymorphism, and the forms of polymorphism are method overloading and method rewriting, so how does the Java virtual machine confirm which method we should call?

Method call instruction

First, let's take a look at the bytecode call instruction for the method. In Java, four bytecode instructions are provided to invoke the method (before jdk1.7):

1. Invokestatic: call static methods

2. Invokespecial: call instance constructor method, private method, parent method

3. Invokevirtual: call all virtual methods

4. Invokeinterface: call the interface method (the runtime determines an object that implements the interface)

Note: starting with JDK1.7, Java has added a new instruction invokedynamic, which is introduced to implement a dynamically typed language, which we will not discuss here.

Method analysis

In the parsing phase of the class loading mechanism, the main thing to do is to convert symbolic references to direct references, but there is a premise for method calls, that is, you can only determine which method to call before the method is actually run, and this method is immutable at run time. Only methods that meet this premise are directly replaced with direct references during the parsing phase, otherwise they can only be determined at run time.

Non-virtual method

In the Java language, the method that satisfies the premise that the compiler knows and the runtime is immutable is called a non-virtual method. In the parsing phase of the class loading mechanism, non-virtual methods can directly convert symbolic references into direct references. There are 4 kinds of non-virtual methods:

1. Static method

2. Private methods

3. Instance constructor method

4. Parent class method (called through super.xxx, because Java is single inheritance and has only one parent class, you can determine the uniqueness of the method)

Non-final methods other than non-virtual methods are called virtual methods, and virtual methods require runtime to determine which method is actually called. It is clearly pointed out in the Java language specification that the final method is a non-virtual method, but final belongs to a special existence, because the final method is different from the bytecode instructions called by other non-virtual methods.

Knowing the type of virtual method, combined with the call instruction of the above method, we can know that virtual method is called through bytecode instruction invokestatic and invokespecial, and final method is an exception. Final method is called through bytecode instruction invokevirtual, but because the characteristic of final method is that it can not be rewritten and overridden, it must be unique. Although the calling instruction is different, it still belongs to the category of non-virtual method.

Method overload

Let's take a look at an example of method overloading:

Package com.zwx.jvm.overload;public class OverloadDemo {static class Human {} static class Man extends Human {} static class WoMan extends Human {} public void hello (Human human) {System.out.println ("Hi,Human");} public void hello (Man man) {System.out.println ("Hi,Man") } public void hello (WoMan woMan) {System.out.println ("Hi,Women");} public static void main (String [] args) {OverloadDemo overloadDemo = new OverloadDemo (); Human man = new Man (); Human woman = new WoMan (); overloadDemo.hello (man); overloadDemo.hello (woman);}}

The output is as follows:

Hi,HumanHi,Human

Here, why did the Java virtual machine choose a method with an argument of Human to make the call?

Before we explain this problem, let's introduce a concept: quantity.

Patriarchal quantity

The recipient (caller) of the method and the method parameters are collectively referred to as arguments. The final decision method allocation is based on the number of quantities to choose, so according to how many quantities to choose the method can be divided into:

Single dispatch: select the method according to 1 quantity

Multi-dispatch: select the method according to more than one quantity

Knowing that the method assignment is based on the quantity, it is easy to understand if we go back to the above example.

OverloadDemo.hello (man)

In this code, overloadDemo represents the receiver, man represents the parameter, and the receiver is the only one that is certain, that is, the overloadDemo instance, so the only parameter that determines which method to call is the parameter (including parameter type and number and order). Let's look at the parameter types again:

Human man = new Man ()

In this sentence, Human calls it the static type of variable, while Man calls it the actual type of variable, while the Java virtual machine confirms the overloaded method based on the static type of the parameter, so in the end, no matter which new object on your right is, the method whose parameter type is Human is called.

Static dispatch

All dispatching actions that depend on the static type of the variable to locate the dispatch performed by the method is called static dispatch. The most typical application of static dispatch is method overloading.

Method overloading can determine the uniqueness of the method at compile time, but even so, in some cases, this overloaded version is not unique or even a bit vague. The reason for this is that literals do not need to be defined, so literals do not have today's types. For example, we directly call a method: xxx.xxx ('1'), which is fuzzy and does not correspond to static types. Let's look at another example:

Package com.zwx.jvm.overload;import java.io.Serializable;public class OverloadDemo2 {public static void hello (Object a) {System.out.println ("Hello,Object");} public static void hello (double a) {System.out.println ("Hello,double");} public static void hello (Double a) {System.out.println ("Hello,Double") } public static void hello (float a) {System.out.println ("Hello,float");} public static void hello (long a) {System.out.println ("Hello,long");} public static void hello (int a) {System.out.println ("Hello,int");} public static void hello (Character a) {System.out.println ("Hello,Character") } public static void hello (char a) {System.out.println ("Hello,char");} public static void hello (char... a) {System.out.println ("Hello,chars");} public static void hello (Serializable a) {System.out.println ("Hello,Serializable");} public static void main (String [] args) {OverloadDemo2.hello ('1');}}

The output here is

Hello,char

Then if you comment out the method, you will output:

Hello,int

Then comment out the int method, and the method call output will be in the following order:

Char- > int- > long- > float- > double- > Character- > Serializable- > Object- > chars

As you can see, the priority of multiple parameters is the lowest, and Serializable is output because the wrapper class Character implements the Serializable interface. Note that the wrapper class Double of double in the example will not be executed.

Method rewriting

Let's modify the first example above:

Package com.zwx.jvm.override;public class OverrideDemo {static class Human {public void hello (Human human) {System.out.println ("Hi,Human");}} static class Man extends Human {@ Override public void hello (Human human) {System.out.println ("Hi,Man") }} static class WoMan extends Human {@ Override public void hello (Human human) {System.out.println ("Hi,Women");}} public static void main (String [] args) {Human man = new Man (); Human woman = new WoMan (); man.hello (man); man.hello (woman); woman.hello (woman) Woman.hello (man);}} copy the code

The output is as follows:

Hi,ManHi,ManHi,WomenHi,Women copy code

Here the static type is Human, but two results are output, so the method is definitely not dispatched according to the static type, but the result should be judged by the actual type of the caller.

Execute the javap command to convert the class to bytecode:

Compiled from "OverrideDemo.java" public class com.zwx.jvm.override.OverrideDemo {public com.zwx.jvm.override.OverrideDemo (); Code: 0: aload_0 1: invokespecial # 1 / / Method java/lang/Object. "": () V 4: return public static void main (java.lang.String []) Code: 0: new # 2 / / class com/zwx/jvm/override/OverrideDemo$Man 3: dup 4: invokespecial # 3 / / Method com/zwx/jvm/override/OverrideDemo$Man. "": () V 7: astore_1 8: new # 4 / / class com/zwx/jvm/override / OverrideDemo$WoMan 11: dup 12: invokespecial # 5 / / Method com/zwx/jvm/override/OverrideDemo$WoMan. "": () V 15: astore_2 16: aload_1 17: aload_1 18: invokevirtual # 6 / / Method com/zwx/jvm/override/OverrideDemo$Human.hello: (Lcom/zwx/jvm/override/OverrideDemo$Human ) V 21: aload_1 22: aload_2 23: invokevirtual # 6 / / Method com/zwx/jvm/override/OverrideDemo$Human.hello: (Lcom/zwx/jvm/override/OverrideDemo$Human;) V 26: aload_2 27: aload_2 28: invokevirtual # 6 / / Method com/zwx/jvm/override/OverrideDemo$Human.hello: (Lcom/zwx/jvm/override/OverrideDemo$Human ) V 31: aload_2 32: aload_1 33: invokevirtual # 6 / / Method com/zwx/jvm/override/OverrideDemo$Human.hello: (Lcom/zwx/jvm/override/OverrideDemo$Human;) V 36: return}

We can find that the method calls here are called using the instruction invokevirtual, because according to the above classification, hello methods are virtual methods.

The main method gives a general explanation.

In the main method, lines 7 (Code serial number) and 15 store the Man object instance and the Women object instance in the index [1] and index [2] positions of the local variable, respectively, and then call the invokevirtual instruction to invoke the method, which includes two lines 16, 17, 21, 22, 26, 27, and 31, respectively.

.

So the most important thing above is how the invokevirtual instruction works. Invokevirtual is mainly selected according to the following steps:

1. Find the method receiver (caller) in the current Operand stack and write it down, such as Caller

2. Then find the method in the type Caller. If you find a method with the same method signature, stop the search and begin to verify the method. The verification fails through the direct call, and the IllegalAccessError exception is thrown directly.

3. If a method with the same method signature is not found in Caller, go up to the parent class and so on until it is found. If no matching method has been found at the top, an AbstractMethodError exception is thrown

Dynamic dispatch

In the above method rewriting example, the dispatch process in which the execution version of the method can be determined based on the actual type at run time is called dynamic dispatch.

Single dispatch and multiple dispatch

In the first example of the method overload above, it is a static dispatch process, during which the Java virtual machine selects the target method for two things:

1. Static type

2. Method parameters

That is, two quantities are used for allocation, so it is a static multi-dispatch process.

In the example of method rewriting above, because the method signature is fixed, that is, the parameters are fixed, then there is only one argument-static type, which can finally determine the method call, so it belongs to dynamic single dispatch.

So it can be concluded that for Java: Java is a static multi-dispatch, dynamic single dispatch language.

On how to carry out JVM method reloading and method rewriting principle analysis is shared here, I hope the above content can be of some help to you, can learn more knowledge. If you think the article is good, you can share it for more people to see.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Internet Technology

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report