8. Secret HotSpot Virtual Machine Objects and Direct Memory

1. Creation of Objects

1. Several ways to create objects

  • new: The most common way, in a single class, is to call getInstance's static class method, XXXFactory's static method.
  • Class's newInstance method: The reflected way is marked obsolete in JDK9 because only empty parameter's construction method can be invoked and the permission must be public.
  • Constructor's newInstance(xxx): Reflectively, you can call an empty, parameterized constructor with any permissions.
  • clone(): The current class needs to implement the Cloneable interface to implement the clone() method without invoking any constructors.
  • Deserialization: Gets the binary stream of an object from a file or network.
  • Third-party library Objenesis

2. Viewing Object Creation from Byte Code

public class ObjectTest {
    public static void main(String[] args) {
        Object o = new Object();
    }
}
After compilation, the corresponding byte code is viewed through javap-c-p ObjectTest.class, -v: All byte code information is output
Look at the byte code corresponding to main(), find the #2 position string constant pool after calling the new directive, find the Object class, first determine if the class is loaded in the method area, and if it is not loaded, use the class loader to load the class. Then call the init() method of the Object class.
Classfile /D:/IdeaProbject/JVM/target/classes/day5/ObjectTest.class
  Last modified 2021-9-11; size 430 bytes
  MD5 checksum 5eb0fa7172fbe9c882683b37b9a32a50
  Compiled from "ObjectTest.java"
public class day5.ObjectTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // java/lang/Object
   #3 = Class              #21            // day5/ObjectTest
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lday5/ObjectTest;
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               args
  #14 = Utf8               [Ljava/lang/String;
  #15 = Utf8               o
  #16 = Utf8               Ljava/lang/Object;
  #17 = Utf8               SourceFile
  #18 = Utf8               ObjectTest.java
  #19 = NameAndType        #4:#5          // "<init>":()V
  #20 = Utf8               java/lang/Object
  #21 = Utf8               day5/ObjectTest
{
  public day5.ObjectTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lday5/ObjectTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1     o   Ljava/lang/Object;
}

3. Determine whether the class corresponding to the object is loaded, linked, initialized

1. When a Java Virtual Opportunity encounters a byte-code new instruction, it first checks whether the parameters of the instruction can locate a symbol reference of a class in the Constant Pool of Matespace, and whether the class represented by the symbol reference has been loaded, parsed, and initialized.
2. If not, use the current class loader in parental delegation mode to locate the corresponding.Class file using ClassLoader+Package Name+Class Name as Key. If no class file is found, throw a ClassNotFoundException exception; if found, load the class and generate the corresponding Class class object.

4. Allocate memory space for new objects

1. The size of memory required for an object can be determined completely after class loading, and the task of allocating space for an object is essentially equivalent to dividing a block of memory of a certain size from the Java heap. If an instance member variable is a reference variable, only the reference variable space can be allocated, that is, 4 bytes in size.
2. Memory in the Java heap is absolutely clean: allocate memory by using Bump The Pointer
  • All used memory is put aside, free memory is put on the other side, with a pointer in the middle as an indicator of the bounding point, and the allocated memory is simply moving that pointer a distance equal to the size of the object toward the free space
3. Memory in the Java heap is not tidy: allocate memory using Free List
  • There is no simple pointer collision when used memory and idle memory are interlaced. The virtual machine must maintain a list of memory blocks available, find a large enough space in the list to allocate to the object instance, and update the records on the list.
4. The choice of allocation depends on whether the Java heap is tidy or not, and whether the Java heap is tidy or not is determined by the ability of the garbage collector to use Compact.
  • When using a collector with a compression collation process such as Serial, ParNew etc., the allocation algorithm used by the system is pointer collision, which is simple and efficient;
  • When using CMS, a Sweep-based collector, in theory only a more complex free list can be used to allocate memory.
5. Difference between tag clearance algorithm and tag compression algorithm
  • Label Cleanup Algorithm: There will be a lot of memory fragmentation after GC.
  • Label compression algorithm: After GC, memory fragmentation is defragmented.

5. Handling concurrency issues

1. Object creation is a very frequent behavior in the virtual machine. Even if only one pointer is modified to point to a location, it is not thread-safe in concurrent situations. There may be situations where memory is being allocated to Object A, the pointer has not yet been modified, and Object B uses the original pointer to allocate memory at the same time.
2. Two solutions
  • Synchronize actions that allocate memory space - essentially ensuring the atomicity of the update operation by using CAS with failed retries.
  • The action of allocating memory is divided into different spaces by threads, and a small piece of memory is pre-allocated to each thread in the Eden area, called the Thread Local Allocation Buffer (TLAB).Which thread allocates memory is allocated in which thread's local buffer, and synchronous locking is required only when the local buffer is exhausted and a new one is allocated. TLAB can be used or not by using the -XX:+/-UseTLAB parameter.

6. Initialize allocated memory space

After memory allocation is complete, the virtual machine must allocate memory space to (but not object headers)All are initialized to zero values, which can be done conveniently before TLAB is assigned if TLAB is used. This ensures that instance fields of objects can be used directly in Java code without initial values, enabling programs to access the zero values corresponding to the data types of these fields.

7. Set the object header of the object

Store data such as the class to which the object belongs (i.e. metadata information of the class), HashCode of the object, GC generation age of the object, lock information of the object, etc. in the object header of the object. How this process is set depends on the JVM implementation.

8. Initialize by executing init method

After all the above steps have been completed, a new object has been created from a virtual machine perspective. But from a Java program perspective, object creation is just beginning - the constructor <init>() in the Class fileThe method has not yet been implemented, all fields are default zero values, and other resources and state information required by the object have not been constructed for the intended purpose.(Depending on whether the new directive follows the invokespecial directive in the byte stream, the Java compiler generates both byte code directives where it encounters the new keyword, but not necessarily if it is generated directly by other means), and the new directive is followed by <init>().Method, initialize the object as the programmer wishes, so that such a truly available object can be completely constructed.
/**
 * Test object creation process:
 * 1,Load class meta information; 2, allocate memory for objects; 3, handle concurrency issues; 4, default initialization of attributes (zero-value initialization)
 * 5,Set object header information; 6. Explicit initialization of attributes, initialization in code blocks, initialization in constructors
 * Assign operations to object attributes:
 * 1,Default initialization of properties
 * 2,Explicit Initialization / Initialization in Code Blocks (side-by-side relationship, who sees the order in which the code was written first)
 * 3,constructor initialization
 */
public class CreateObject {
    int id = 1001;
    String name;
    Users users;

    {
        name = "anonymous";
    }
    public CreateObject() {
        users = new Users();
    }
}
1. Attribute default initialization: id=1001
2. Explicit Initialization/Code Block Initialization: neme="Anonymous User"
3. Construction initialization: users=new Users()

2. Memory layout of objects

1. Overview

In the HotSpot virtual machine, the storage layout of objects in heap memory can be divided into three parts: Header, Instance Data, and Padding.

2. Object Header

The object header section of the HotSpot virtual machine object includes two types of information: runtime metadata (Mark Word) and type pointers.
Runtime metadata (Mark Word):
  • Used to store its own runtime data, such as HashCode, GC generation age, lock status flags, locks held by threads, biased thread ID, biased timestamp, etc. The length of this data is 32 bits and 64 bits in 32-bit and 64-bit virtual machines (with uncompressed pointers turned on), respectively, called Mark Word.
  • Objects need to store a lot of runtime data, which actually exceeds the maximum recorded by 32-bit and 64-bit Bitmap structures, but the information in the object's head is an additional storage cost that is independent of the data defined by the object itself, considering the space efficiency of the virtual machine,Word is designed to have a dynamically defined data structure that stores as much data as possible in a very small amount of space and reuses its own storage space based on the state of the object.
For example, in a 32-bit HotSpot virtual machine, such as when an object is not locked by a synchronous lock, 25 bits of Mark Word's 32-bit storage space are used to store object hash codes, 4 bits are used to store object generation age, 2 bits are used to store lock flag bits, 1 bit is fixed to zero, and in other states (lightweight lock, heavyweight lock, GC flag, skewed)The following object is stored as shown in the following figure


The 64-bit Mark Word store is as follows:


Type pointer:
  • A pointer to the type metadata of an object through which the Java virtual machine determines which instance of the class the object is. Not all virtual machine implementations must retain the type pointer on the object data, in other words, finding metadata information for an object does not have to go through the object itself.
  • In addition, if the object is a Java array, there must be a piece of data in the object header that records the length of the array, because the virtual machine can determine the size of the Java object from the metadata information of the normal Java object, but if the length of the array is uncertain, the size of the array cannot be inferred from the information in the metadata.

3. Instance Data

It is valid information that the object actually stores, that is, the various types of field content that we define in the program code, whether inherited from the parent class or defined in the subclass, must be recorded.
The storage order of this section is affected by the virtual machine allocation policy parameters (-XX:FieldsAllocationStyle parameter) and the order in which the fields are defined in the Java source code.
The default allocation order for HotSpot virtual machines is longs/doubles, ints, shorts/chars, bytes/booleans, oops (Ordinary Object Pointers, OOPs)As you can see from the default allocation policy above, fields of the same width are always allocated to be stored together, and if this precondition is met, variables defined in the parent class will appear before the subclass. If the value of the + XX:CompactFields parameter of the HotSpot virtual machine is true (the default is true)Narrow variables in that subclass also allow insertion into gaps in parent variables to save a little space.

4. Object Filling

This does not necessarily exist, nor does it have any special meaning, it just acts as a placeholder. Since the automatic memory management system of the HotSpot virtual machine requires that the object start address be an integer multiple of 8 bytes, in other words, any object must be an integer multiple of 8 bytes in size. The object head part has been carefully designed to be exactly a multiple of 8 bytes(twice or twice), so if the object instance data portion is not aligned, it needs to be filled by alignment fills.

5. Illustrate memory layout

public class CreateObject {
    int id = 1001;
    String name;
    Users users;

    {
        name = "anonymous";
    }
    public CreateObject() {
        users = new Users();
    }
}

public class Users {
}

public class CreateObjTest {
    public static void main(String[] args) {
        CreateObject create = new CreateObject();
    }
}
Memory Layout Diagram

3. Access Location of Objects

1. Overview

Objects are naturally created for subsequent use, and Java programs manipulate specific objects on the stack using reference data. Since reference types are in the Java Virtual Machine SpecificationIt only specifies that it is a reference to an object, and it does not define how the reference should be located and accessed to the specific location of objects in the heap. Therefore, the way of accessing objects is also determined by the virtual machine implementation. The main ways of accessing objects are using handles and direct pointers.

2. Handle access

Handle access means that in the local variable table of the stack, a reference to a recorded object may be divided into a block of memory in the Java heap as the handle pool. The reference stores the handle address of the object, and the handle contains the specific address information of the object instance data and the type data.
Advantage:
  • References store stable handle addresses, which only change the instance data pointer in the handle when the object is moved (which is a common behavior when garbage is collected), while reference s themselves do not need to be modified.
Disadvantages:
  • Requires separate space storage handles, which is a bit of a waste of space.
  • Accessing an object requires finding the handle by reference and then the object entity by variables in the handle, which is inefficient.

3. Direct pointer access (HotSpot uses)

Direct pointers are object addresses stored directly in reference s in the local variable table, pointing directly to instances in the heap, type pointers in object instances, and object type data in the method area.
Advantage:
  • Faster access saves time spent on pointer positioning, which is also a significant execution cost because object access is so frequent in Java.

4. Direct Memory

1. Overview

It is not part of the virtual machine runtime data area or the memory area defined in the Java Virtual Machine Specification.
Direct memory is the range of memory outside the Java heap that is directly requested from the system.
From NIO, Native memory is manipulated by DirectByteBuffer in the presence heap
In general, direct memory access is faster than the Java heap. That is, high read and write performance.
  • Therefore, for performance reasons, direct memory may be considered in situations where read and write are frequent.
  • Java's NIO library allows Java programs to use direct memory for data buffers
/**
 * Direct Memory Occupancy and Release
 */
public class BufferTest {
    private static final int BUFFER = 1024 * 1024 * 1024;//1GB

    public static void main(String[] args){
        //Allocate local memory space directly
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("Direct memory allocation complete, request indication!");

        Scanner scanner = new Scanner(System.in);
        scanner.next();

        System.out.println("Direct memory begins to release!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}

2. Direct Cache

Originally based on BIO architecture, when reading and writing local files, you need to interact with the disk and switch from user state to kernel state. This requires two copies of memory to store duplicate data, which is inefficient.

3. Direct Buffer

When using NIO, the operating system directly delimits a direct cache that can be accessed directly by Java code, with only one copy. NIO is suitable for reading and writing large files. Direct manipulation of physical disks.

4. Copy large files using NIO and BIO, respectively

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Copy large files using NIO and BIO, respectively
 */
public class CopyFileWithNio {
    private static final String TO = "D:\\Project\\lifecycle-lib.zip";
    private static final int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        long sum = 0;
        String src = "D:\\Project\\lifecycle-lib.zip";
        for (int i = 0; i < 3; i++) {
            String dest = "D:\\lifecycle-lib_" + i + ".zip";
            //Copy files using traditional methods
            // sum += io(src,dest);//1673
            //Copy files using NIO
            sum += directBuffer(src, dest);//957
        }
        System.out.println("The total time spent is:" + sum);
    }

    /**
     * Using NIO
     * @param src
     * @param dest
     * @return
     */
    private static long directBuffer(String src, String dest) {
        long start = System.currentTimeMillis();
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            inChannel = new FileInputStream(src).getChannel();
            outChannel = new FileOutputStream(dest).getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
            while (inChannel.read(byteBuffer) != -1) {
                //Modify to read data mode
                byteBuffer.flip();
                outChannel.write(byteBuffer);
                //empty
                byteBuffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inChannel != null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel != null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        long end = System.currentTimeMillis();
        return end - start;
    }

    /**
     * Use traditional methods
     * @param src
     * @param dest
     * @return
     */
    private static long io(String src, String dest) {
        long start = System.currentTimeMillis();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dest);
            byte[] buffer = new byte[_100Mb];
            while (true) {
                int len = fis.read(buffer);
                if (len == -1) {
                    break;
                }
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        long end = System.currentTimeMillis();
        return end - start;
    }
}

5. Direct Memory and OOM

Direct memory may also cause OutofMemoryError exceptions
Since direct memory is outside the Java heap, its size is not directly limited to the maximum heap size specified by -Xmx, but system memory is limited, and the sum of Java heap and direct memory is still limited to the maximum memory given by the operating system.
Disadvantages:
  • Higher cost of allocation and recycling
  • Not managed by JVM memory recycling
Direct memory size can be set through -XX:MaxDirectMemorySize, and if not specified, defaults to the maximum heap-Xmx parameter value
/**
 * Test OOM for local memory
 * java.lang.OutOfMemoryError: Direct buffer memory
 */
public class DirectOOM {
    private static final int BUFFER = 1024 * 1024 * 20;//20MB
    public static void main(String[] args) {
        ArrayList<ByteBuffer> list = new ArrayList<>();
        int count = 0;
        try {
            while(true){
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                list.add(byteBuffer);
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            System.out.println(count);
        }
    }
}

6. Apply for local memory directly through the Unsafe class

Allocate local memory using the Unsafe class in the new DirectByteBuffer() method ByteBuffer.allocateDirect()
/**
 * -Xmx20m -XX:MaxDirectMemorySize=10m
 * Set maximum heap memory 20M, direct memory 10M
 */
public class MaxDirectMemorySizeTest {
    private static final long _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while(true){
            unsafe.allocateMemory(_1MB);
        }
    }
}

Added by e39m5 on Sun, 12 Sep 2021 19:47:10 +0300