What happens in the virtual machine when you create a HashMap object and use its put and get methods, and when the HashMap is destroyed.

What happens in the virtual machine when you create a HashMap object and use its put and get methods, and when the HashMap is destroyed.

Suppose your Java source code is like this:

import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        // Create a HashMap object
        HashMap<String, Integer> map = new HashMap<>();
        // Use the put method to add key-value pairs
        map. put("apple", 1);
        map. put("banana", 2);
        map. put("orange", 3);
        // Use the get method to get the value based on the key
        System.out.println(map.get("apple"));
        System.out.println(map.get("banana"));
        System.out.println(map.get("orange"));
        // Destroy the HashMap object
        map = null;
    }
}

So, when you execute this code, the following things will happen in the JVM:

First, the JVM will start and initialize, loading some necessary classes and resources, such as the java.lang.Object class, java.lang.String class, java.lang.System class, etc. These classes will be loaded into the method area by the system class loader and generate corresponding class objects. At the same time, the JVM will create a program counter and a local method stack for each thread, and create a stack for the main thread (main thread).
Then, the JVM executes the main method, which is the entry point of the program. In order to execute the main method, the JVM creates a stack frame for the main thread and pushes it onto the stack. The stack frame contains information such as the local variable table of the main method, the operand stack, the dynamic link, and the method return address. Since the main method is a static method, it does not need to pass in an object instance as a parameter, so there is only one parameter args in the local variable table, which is a string array (String[]) used to receive command line parameters. . The operand stack is a last-in first-out (LIFO) data structure used to store intermediate results during operations. The dynamic link is a pointer to a symbolic reference to the method in the runtime constant pool. The runtime constant pool is a part of the method area, which stores constant information such as classes, fields, methods, and strings. A method return address is an address that points to the bytecode instruction that called the method.
Next, the JVM will execute the main method one by one according to the bytecode instructions indicated by the program counter. First, the JVM encounters a new instruction, which is used to create a new object instance. In order to create a new object instance, the JVM needs to do the following things:
First, the JVM needs to check whether the parameters following the instruction can locate a symbolic reference to a class in the runtime constant pool, and check whether the class represented by the symbolic reference has been loaded, verified, and prepared. If not, then the JVM needs to perform the class loading process first.
Second, the JVM needs to allocate memory space for the new object instance. An object instance allocates memory space in the heap, and includes two parts: object header and instance data. The object header contains the runtime data of the object itself, such as hash code, GC generation age, lock status flag, thread holding lock, Biased thread ID (biased thread ID), biased timestamp (biased timestamp) and other information. Instance data contains the truly effective information of the object, that is, various types of field contents, including fields inherited from the parent class and fields defined by itself.
Finally, the JVM needs to perform necessary initialization operations on the object, that is, execute the class constructor or object initializer. Here, since we are creating a HashMap object, the JVM needs to execute the constructor of the HashMap class. The HashMap class has multiple constructors, but we did not pass in any parameters, so the JVM will execute the parameterless constructor, which is the HashMap() method. This method will initialize some properties of the HashMap object, such as initial capacity, load factor, threshold, hash table, etc. Among them, the hash table is an array, which is used to store key-value pairs. Since we haven’t added any key-value pairs yet, the hash table is an empty array.
Then, the JVM will push the reference of the newly created object instance onto the operand stack. Next, the JVM will encounter a dup instruction, which is used to copy the value at the top of the operand stack and push it into the operand stack. In this way, there are two identical object references in the operand stack. Next, the JVM will encounter an invokespecial instruction, which is used to call special methods of the object, such as instance initialization method, private method, and superclass method. Here, the JVM will call the instance initialization method of the Object class, which is the Object() method. This method is an empty method that does nothing but ensures that each object has an instance initialization method. After calling the Object() method, the JVM will pop the object reference on the top of the operand stack and store it in the first position in the local variable table, which is where the variable map is located. In this way, we have completed the process of creating a HashMap object and assigning it to the variable map.
Next, the JVM will encounter an invokevirtual instruction, which is used to call the object’s virtual method. A virtual method refers to a method that cannot be determined at compile time which class is specifically called, but is determined at runtime based on the actual type of the object. Here, the JVM will call the put method of the HashMap object, which is the put(Object key, Object value) method. This method is used to add a key-value pair to the HashMap. In order to call this method, the JVM needs to first retrieve the object reference corresponding to the variable map from the local variable table and push it into the operand stack. Then, the JVM needs to take out the string constant “apple” and the integer constant 1 from the constant pool, and push them into the operand stack in sequence. In this way, there are three parameters in the operand stack: object reference, key and value. Then, the JVM will execute the bytecode instructions of the put method. The main logic of the put method is as follows:
First, the put method checks whether the key is null. If it is null, it maps the key to the first position of the hash table, which is a[0]. This is because null has no hash code and cannot be hashed.
Secondly, the put method calculates the hash code of the key and calculates the index based on the hash code and the length of the hash table. index = hash code % hash table length. Here, since the hash code of the string “apple” is 96354 (can be obtained through “apple”.hashCode()), and the length of the hash table is 16 (default value), the index = 96354 % 16 = 2.
Then, the put method checks whether a node already exists at the index position in the hash table. If not, create a new node and store the key-value pair in the node. Then insert the node into the hash table at the index position. If so, traverse the linked list or red-black tree at that position to find whether the same key exists. If there is, the original value is replaced and the old value is returned. If not, insert the new node into the linked list or red-black tree.
Finally, the put method checks whether the current number of elements exceeds the threshold. Threshold = hash table length * load factor. The loading factor is a floating point number between 0 and 1, which represents the filling degree of the hash table, that is, the ratio of the number of elements to the length of the hash table. Generally speaking, the larger the load factor, the fuller the hash table and the higher the probability of hash collision, which affects performance; the smaller the load factor, the emptier the hash table is, which wastes memory space. By default, the load factor is 0.75, which is a compromise. If the current number of elements exceeds the threshold, the put method will trigger the hash table expansion (resize) operation. The expansion operation doubles the length of the hash table, recalculates the index of all elements, and reallocates them to the new hash table. This reduces hash collisions and improves performance. However, the capacity expansion operation is also a time-consuming operation, so you should try to avoid triggering the capacity expansion operation frequently. If we know the number of elements to be stored in advance, we can specify an appropriate initial capacity (initial capacity) when creating a HashMap object, which can reduce the number of expansion operations.

The get method is a method used to obtain the corresponding value based on the key. Its main logic is as follows:

First, the get method checks whether the key is null. If it is null, it directly returns the value at the first position in the hash table, which is a[0]. This is because null has no hash code and cannot be hashed.
Secondly, the get method calculates the hash code of the key and calculates the index based on the hash code and the length of the hash table. index = hash code % hash table length.
Then, the get method checks whether a node exists at the index position in the hash table. If not, returns null. If so, traverse the linked list or red-black tree at that position to find whether the same key exists. If there is, the corresponding value is returned. If not, returns null.

Next, the JVM will encounter two other invokevirtual instructions, which are used to call the put method of the HashMap object, adding the string “banana” and the integer 2, and the string “orange” and the integer 3 as key-value pairs to the HashMap. . The process of these two calls is similar to the previous one, except that the calculated index is different. The hash code of the string “banana” is 940837101, index = 940837101 % 16 = 13. The hash code of the string “orange” is -1008851410, index = -1008851410 % 16 = 6. Since no nodes exist at these two index locations, the JVM will directly create new nodes and insert them into the hash table. In this way, we have completed the process of adding three key-value pairs to the HashMap.
Then, the JVM will encounter three getstatic instructions, which are used to obtain the value of the static field out of the System class and push it into the operand stack. The System class is a class that provides system-related functions. It has a static field out, which is an object of PrintStream type and is used to output information to the standard output stream. In order to obtain the value of the static field out of the System class, the JVM needs to first load the System class and execute its static initialization method (static initialization method), which is the () method. This method will initialize some static properties and operations of the System class, such as setting the security manager (security manager), setting the native method library (native method library), setting the standard input/output/error stream (standard input/output/error stream), etc. After executing the () method, the JVM will push the value of the static field out of the System class into the operand stack.
Next, the JVM will encounter three invokevirtual instructions, which are used to call the get method of the HashMap object, obtain the corresponding values based on the strings “apple”, “banana” and “orange” as keys, and push them into the operand stack . The main logic of the get method is as follows:
First, the get method checks whether the key is null. If it is null, it directly returns the value at the first position in the hash table, which is a[0].
Secondly, the get method calculates the hash code of the key and calculates the index based on the hash code and the length of the hash table. index = hash code % hash table length.
Then, the get method checks whether a node exists at the index position in the hash table. If not, return null. If so, traverse the linked list or red-black tree at that position to find whether the same key exists. If so, return the corresponding value. If not, return null.
Then, the JVM will encounter three invokevirtual instructions, which are used to call the println method of the PrintStream object, pass in the integers 1, 2, and 3 in the operand stack as parameters, and output them to the standard output stream. In this way, we have completed the process of getting the value based on the key and printing it out.
Finally, the JVM will encounter an aconst_null instruction, which is used to push a null value onto the operand stack. Next, the JVM will encounter an astore_1 instruction, which is used to pop the value at the top of the operand stack and store it in the second position in the local variable table, which is where the variable map is located. In this way, we have completed the process of assigning the variable map to null. Since the HashMap object originally pointed to by the variable map no longer has any reference to it, it becomes a garbage object, waiting for the garbage collector to recycle it. When the garbage collector collects it, it will release the memory space it occupies and execute its finalization method, which is the finalize() method. This method is a method of the Object class, which can be overridden by subclasses to perform some cleaning operations before the object is destroyed. Here, since the HashMap class does not override the finalize() method, it does not do anything but simply returns.