preface

When I looked at the STRUCTURE of JVM memory, I saw in understanding the JVM that “each thread has a program counter that keeps track of where the bytecode is currently executed.” But remember that JVM threads delegate to OS implementations, or that Java threads map to OS threads, what exactly is the bytecode instruction location recorded by the PC?

OS threads that are serious C threads, C threads where the PC stores binary instructions (ignoring incomplete instructions caused by out-of-order execution), how do they correspond one to one? Is there a local machine code for each bytecode? But Java also has this thing called the interpretor, which translates one by one. 👴 true thought for a long time, later on Google turned over a lot of time, reluctantly find some explanations.

Before we get into this, let’s implement a thread of our own to verify.

requirements

  • 1 one DEBUG JDK, one Google.
  • 2 discount ️ C++ code can be read.

process

First, let’s try to define our own Java thread. Since my nickname is CodeWithBuff, the prefix is CWB:

/ * * *@authorCodeWithBuff *@device iMacPro
 * @time 2021/6/29 1:27 下午
 */
public class CWBThread {

    private String msg;

    public CWBThread(String msg) {
        this.msg = msg;
    }

    public void run(a) {
        System.out.println(msg);
    }

    public void start(a) {
        start0();
    }

    private native void start0(a);
}
Copy the code

We modeled the entire start0() method on the JVM, which is native, so we need to implement it in C++. Using it is simple:

new CWBThread("aaa").start();
Copy the code

Can.

Since we want to emulate the JVM, let’s leave the creation and destruction of threads to the OS. Instead of learning from the JVM to add dynamically linked libraries to the JDK, I’m going to do it the JNI way.

Enter:

javac /... /CWBThread.java -h /... / []Copy the code

To generate the header file for the local method we need to implement, and unsurprisingly you get.h files like this:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_codewithbuff_javathread_CWBThread */

#ifndef _Included_com_codewithbuff_javathread_CWBThread
#define _Included_com_codewithbuff_javathread_CWBThread
#ifdef __cplusplus
extern "C" {
#endif
/* * Class: com_codewithbuff_javathread_CWBThread * Method: start0 * Signature: ()V */
JNIEXPORT void JNICALL Java_com_codewithbuff_javathread_CWBThread_start0
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
Copy the code

Then is the way to implement it, I am writing in CLion, before this need to link jni.h and jni_md.h two files, otherwise will fail. So my CMakeLists file is as follows:

Cmake_minimum_required (VERSION 3.19) project(JavaThreadLearn) set(CMAKE_CXX_STANDARD 20) # add_executable(JavaThreadLearn src/main/cpp/com_codewithbuff_javathread_CWBThread.h src/main/cpp/cwb_thread.cpp) # Remember here is modified into your JDK directory include_directories (/ Library/Java/JavaVirtualMachines/JDK - 15.0.2. JDK/Contents/Home/include) Include_directories (/ Library/Java/JavaVirtualMachines/JDK - 15.0.2. JDK/Contents/Home/include/Darwin)Copy the code

After that, we can write the corresponding CPP file as follows:

//
// Created by joker on 2021/6/29.
//
#include <iostream>
#include <pthread.h>
#include "com_codewithbuff_javathread_CWBThread.h"
using namespace std;

class CWBThreadWrapper {
private:
    JavaVM* javaVm;
    jobject cwbThreadObject;
    JNIEnv* attachToJVM(a);
public:
    CWBThreadWrapper(JNIEnv *env, jobject obj);
    void callRunMethod(a);
    ~CWBThreadWrapper(a); };JNIEnv *CWBThreadWrapper::attachToJVM(a) {
    JNIEnv *jniEnv;
    if (javaVm->AttachCurrentThread((void **)&jniEnv, nullptr) != 0) {
        cout << "Attach failed.\n";
    }
    return jniEnv;
}

CWBThreadWrapper::CWBThreadWrapper(JNIEnv *env, jobject obj) {
    env->GetJavaVM(& (this->javaVm));
    this->cwbThreadObject = env->NewGlobalRef(obj);
}

CWBThreadWrapper::~CWBThreadWrapper() {
    javaVm->DetachCurrentThread(a); }void CWBThreadWrapper::callRunMethod(a) {
    JNIEnv *env = attachToJVM(a); jclass clazz = env->GetObjectClass(this->cwbThreadObject);
    jmethodID methodId = env->GetMethodID(clazz, "run"."()V");
    if(methodId ! =nullptr) {
        env->CallVoidMethod(this->cwbThreadObject, methodId);
    } else {
        cout << "Can't find run() method.\n"; }}void *thread_entry_pointer(void *args) {
    cout << "Start set thread entry pointer.\n";
    CWBThreadWrapper *cwbThreadWrapper = (CWBThreadWrapper *) args;
    cwbThreadWrapper->callRunMethod(a);delete cwbThreadWrapper;
    return nullptr;
}

JNIEXPORT void JNICALL Java_com_codewithbuff_javathread_CWBThread_start0(JNIEnv *jniEnv, jobject cswThreadObject) {
    CWBThreadWrapper *cwbThreadWrapper = new CWBThreadWrapper(jniEnv, cswThreadObject);
    pthread_attr_t pthreadAttr;
    pthread_attr_init(&pthreadAttr);
    pthread_attr_setdetachstate(&pthreadAttr, PTHREAD_CREATE_DETACHED);
    pthread_t pthread;
    if (pthread_create(&pthread, &pthreadAttr, thread_entry_pointer, cwbThreadWrapper)) {
        cout << "Create error.\n";
    } else {
        cout << "Start a linux thread.\n"; }}Copy the code

This code is easy to read, just a simple JNI native method call. Since we leave thread creation to the PThread, we don’t have to worry about inserting safe points, safe zones, interpreter portals, etc., which are reserved for JVMS, so there’s a lot of code and a lot of functionality.

Now that everything is ready, let’s compile this CPP file into a platform-specific dynamic link file, with the suffix jnilib since I’m macOS, and type:

The location of the g + + -i [your JDK] / JDK - 15.0.2. JDK/Contents/Home/include -i your JDK [position] / JDK - 15.0.2. JDK/Contents/Home/include/Darwin -dynamiclib [location of your CPP file]/cwb_thread. CPP -o libcwbthread. jnilibCopy the code

Finally, a jnilib file will be generated in your C++ project folder. We then load the file into the JVM using the system.load () method, and the JVM can call C++ methods for our native start0() method.

As follows:

public class Main {

    public static void main(String[] args) {
        System.load("[your C++ project location]/ libcwbthread.jnilib");
        new CWBThread("aaa").start();
        new CWBThread("bbb").start(); }}Copy the code

We can see output like this:

We do this by creating a Thread in C++, passing in the CWBThread object, and then calling its run() method through the object.

conclusion

Is this the end of it? What about the first question we asked?

Before answering this question, let’s take a look at the JVM architecture:

Class loaders are not mentioned, but the runtime data area mainly includes the following:

Focus on the execution engine section later.

The execution engine is responsible for executing class files and, in addition, handling JNI local calls in Java code and returning the results to Java programs.

The execution engine here consists of three: an interpreter, a just-in-time compiler, and a garbage collector.

  • 1️ interpreter is responsible for interpreting the class file line by line, instead of translating into machine code, it reads each line of bytecode and executes it internally, so the PC of Java thread records which line the current interpreter interprets (first question — what is the PC recording).

  • 2️ discount is responsible for generating local code on hotspot code, the specific process includes:

    1. Hotspot detection technology finds hotspot code
    2. Bytecode => Intermediate code
    3. Intermediate code optimization
    4. Intermediate code => Local machine code

    Here to be sure, only responsible for generating the JIT, is not responsible for enforcing, generated after the machine code calls or interpreter to complete, so the actual implementation process in a process of the interpreter did what the code is compiled, if be compiled, native is executed directly compiled code, otherwise your line interpretation is carried out.

  • 3️ waste recycling device is not the focus of our section, we will not say.

The JVM does nothing more than call OS Thread Create to create threads. The underlying implementation of OS is also called PThreads. Second, we implement run() in the thread by passing in our JavaThread object at pThread creation and then calling its run() method. And then when it’s done, the thread, because it’s C, is destroyed by the OS, and we end up doing nothing.

Here we ignore operations such as safety points that the JVM inserts when creating OS threads for JavaThread, and stick to the simplest features.

Later I found the answer on StackOverflow and Zhihu.

Execution per thread is divided into two types: interpreter direct execution + local method execution. If the method is executed locally, the interpreter does nothing; If the interpreter executes, the interpreter executes the current bytecode and, if necessary, the JIT translates the native machine code, but the interpreter still executes the native code.

Java bytecode must be executed by the execution engine, so even the run() method in pThread contains bytecode that must be executed by the execution engine.

The execution engine +run() runs together in the pThread, the two run together, rather than run() running separately in the operating system thread, or multiple runs () sharing an execution engine. The execution engine is just a program that inserts some code before run(), reads the bytecode, and runs in the pThread (that is, runs bytecode itself).

Those answers emphasize that Java can only be executed with bytecode + execution engines, and that the JIT just compiles hot code in bytecode faster than the ground code, but it does not mean that Java bytecode can be run directly on the machine. Since Java bytecode can only be run through the execution engine, and the bytecode is stored inside run(), run() run by pThread must require the execution engine to intervene.

Incidentally, for method calls, only stack frames + bytecode at the execution engine entry are replaced in the same thread. Different threads have their own execution engines, independent of each other; It is important to note here that the execution engine is a program, not an instance object, which is located at the beginning of the thread, takes a method’s stack frame + bytecode as a parameter, and then executes the bytecode (either pure interpreter execution or native code executed by the interpreter after JIT translation).

One more thing to say about PC is that if you’re executing Java code (bytecode), the PC is the bytecode location, even if it’s executing in a PThread, because we just figured out that even in a PThread, you’re still running bytecode, so the PC exists, It actually records the bytecode line numbers; But if the local method is running, then the PC is undefined, because the local method’s PC represents the location of the binary instruction.

reference

For OpenJDK, is there an execution engine thread for every Java thread? – ETIN’s answer – Zhihu

How Java thread maps to OS thread?

Wouldn’t each thread require its own copy of the JVM?

JVM Tutorial – Java Virtual Machine Architecture Explained for Beginners

What exactly is the JIT compiler inside a JVM?

Interpreter