These two concepts may be confusing to some, as they are both part of the JVM specification, but they are not the same thing! They describe and solve different problems. In short,

  • The Java memory model describes the behavior allowed by multiple threads
  • The JVM memory structure describes the memory space designed for running threads

What is the JVM? It shields underlying architectural differences, is the basis for Java to cross platform, and is part of what every Java programmer must understand.

JVM architecture

The Java Virtual Machine(JVM) is an abstract computer based on a stack architecture with its own instruction set and memory management. It loads the class file, analyzes, interprets, and executes the bytecode. The basic structure is as follows:

As shown in the figure above, the JVM is divided into three main subsystems: the classloader, the runtime data area, and the execution engine.

Class loader subsystem

Its main function is to handle the dynamic loading of classes, as well as linking, and initialization on the first reference to a class.

Loading-loading, as the name implies, is used to load classes. There are three types of loaders that load from different paths according to the parent delegate model:

  • Bootstrap ClassLoader – Loads the rt.jar core class library, which is the highest priority loader
  • Extension ClassLoader – Responsible for loading classes in the JRE \lib\ext folder
  • Application ClassLoader – Is responsible for loading the class libraries specified by CLASSPATH

Linking dynamically to resources required by the runtime is divided into three steps:

  • Verify-verify: Verifies that the generated bytecode is correct
  • Prepare-prepare: Allocates memory and assigns default values for all static variables
  • Resolve-resolve: Replaces all symbolic references to memory in the class file constant pool with direct references to the method area

Initialization – Class Initialization, the final phase of class loading, where static variables are assigned and static blocks are executed. (Note the distinction between object initialization)

Runtime data area

It specifies the storage location of data such as variables and parameters of the program code at runtime. It mainly contains the following parts:

  • PC register (program counter) : Holds the address of the bytecode instruction being executed
  • The stack: Creates a call called when the method is calledThe stack frameData structure for storing local variables and partial procedure results,The stack frameIt consists of the following parts:
    • Local variable table: Stores parameters passed when a method is called, starting at 0 stores this, method parameters, and local variables
    • Operand stack: Performs intermediate operations, stores constant or variable values copied from local variable tables or object instance fields, and the results of operations, in addition to preparing the parameters of the called method and receiving the return results of method calls
    • Dynamic linking: A reference to a run-time constant pool that converts symbolic references in a class file (describing a method calling another method or accessing a member variable) into direct references
    • Method return address: method exits normally or throws an exception and returns the location where the method was called
  • Heap: Stores class instance objects and array objects, the main area for garbage collection
  • Methods area: Also known as a meta-space, or non-heap, uses local memory to store class meta-data metadata (runtime constant pool, field and method data, constructors and method bytecode, etc.). Move interned String and class static variables to the Java heap
  • Runtime constant pool: Stores numeric literals, string literals, and references to any method or field in a class or interface, basically referring to a method or field, and the JVM searches the runtime constant pool for its specific memory address
  • Native method stack: Similar to the JVM stack, but serving Native methods

Execution engine

The runtime data area stores the bytecode to be executed, which the execution engine reads and executes one by one.

Interpreter – an Interpreter that interprets bytecode quickly but executes slowly, with the disadvantage that methods that are called multiple times need to be reinterpreted each time.

JIT compiler-JIT Compiler solves the shortcomings of the interpreter and still uses the interpreter to convert bytecode. However, when code is repeatedly executed, the JIT Compiler will be used to compile the entire bytecode into local code and use local code for repeated calls, thus improving the performance of the system. It consists of the following parts:

  • Intermediate code Generator – Generates intermediate code
  • Code optimizer – responsible for optimizing the intermediate code generated above
  • Object Code generator – Responsible for generating machine code or native code
  • Profiler – a special component that looks for hot spots and determines if the method has been called more than once

Garbage Collector- A Garbage Collector that collects and deletes unreferenced objects.

In addition, it includes the Native Method Libraries required by the execution engine * and the Java Native Interface (JNI) to interact with it *.

Now let’s look at how the Java memory model differs from the JVM memory structure.

JVM memory structure

The common JVM memory structure refers to the area of data submitted to the runtime, where the heap and method areas are shared by the thread, and the program counter, stack, and runtime constant pool are exclusively owned by the thread.

It describes where the bytecode and code data is stored at run time.

The memory model

Java aside, what is the memory model? Definition in Wikipedia:

In computing, a memory model describes the interactions of threads through memory and their shared use of the data.

That is, in computing, the memory model describes how multiple threads correctly interact and use shared data across memory. In other words, the memory model constrains what the processor can read or write to memory.

There is usually one or more layers of cache between CPU and memory, which might be fine for a single processor, but in a multi-processor system, there might be cache consistency issues, which is what happens when two processors (threads) read the same memory location at the same time? When do you see the same value?

The cache consistency problem, in concurrent programming, is also known as the visibility problem. The memory model defines, at the processor level, the necessary and sufficient conditions for visibility of the results of memory writes by processors to each other:

  • The strong memory model generally refers to sequential consistency, where all memory operations have a full order relationship, and each operation is atomic and immediately visible to all processors
  • A weak-memory model that does not limit the order in which processors operate on memory, but uses special instructions to flush or invalidate local caches so that writes from other processors are seen or made visible to other processors. These special instructions are called memory barriers

Most processors don’t limit the order of memory operations, and multithreading can have confusing and counterintuitive results when it comes to execution. This is because the CPU, in order to take full advantage of the bus bandwidth of different types of memory (registers, cache, main memory), will reorder memory operations to execute them out of order. This action is called memory sort or instruction reorder.

Reordering, also known as compiler optimization and processor optimization, can occur either at compile time or at CPU runtime. In order to keep multiple threads in order, memory barriers are used to prevent reordering.

Therefore, the memory model describes the use of memory barriers (flushing cache or disabling instruction reordering) to solve visibility and ordering problems in multithreaded programming at the hardware level.

Java memory model

The Java Memory model (hereinafter referred to as JMM) defines its own multithreaded semantics based on the underlying processor memory model. It explicitly specifies a set of collation rules to ensure visibility between threads.

This set of rules is called happens-before, and the JMM states that the happens-before relationship between A and B must be satisfied in order to guarantee that action B will see the result of action A (whether or not they are on the same thread) :

  • Single-thread rule: Every action in a thread happens-before every subsequent action in that thread
  • The monitor locking rule: happens-before Specifies the subsequent locking action for this listener
  • Rule for volatile variables: Writes to a volatile field happens-before Each subsequent read to that field
  • Thread start rule: Execution of the thread start() method happens-before any action within a starting thread
  • Thread join rule: All actions within a thread happens-before any other thread returns from the thread join() successfully
  • Transitivity: If A happens-before B, and B happens-before C, then A happens-before C

How do you understand happens-before? What if the thread (regardless of whether it is the same or not) is literally unlocked before the lock, as in the second rule? This is clearly not true. Happens-before also ensures visibility, such as the action of unlocking and locking. Thread 1 releases the lock to exit the block, thread 2 locks the block, and thread 2 sees the result of thread 1’s changes to the shared object.

Java provides several language constructs, including volatile, final, and synchronized, designed to help programmers describe the concurrency requirements of a program to the compiler:

  • Volatile – Ensures visibility and order
  • Synchronized – ensures visibility and order; The atomicity of a set of actions is guaranteed by Monitor
  • Final – Ensures visibility by disallowing reordering in constructor initialization and assignment of final fields (visibility is not guaranteed if this reference escapes)

When the compiler encounters these keywords, it inserts corresponding memory barriers to ensure semantic correctness.

Synchronized does not forbid reordering of code in a synchronized block, because it locks that only one thread accesses a synchronized block (or critical region) at a time. That is, synchronized block code only needs to satisfy as-if-serial semantics — as long as the execution result of a single thread does not change. It can be reordered.

So, the Java memory model describes the visibility of shared memory modifications made by multiple threads to each other, and also ensures that properly synchronized Java code runs correctly on processors with different architectures.

summary

The relationship can be summarized as follows: Implementing a JVM that satisfies the components of the memory structure description, designing how to execute multiple threads, satisfies the multithreaded semantics of the Java memory model convention.

Search the public account “Epiphany source code” for more source code analysis and build wheels.