YON CISE

JVM 源码阅读指北

你有没有好奇过字节码究竟是怎么在 JVM 里执行的?你有没有因为网上查的关于 JVM 的资料相互之间说法矛盾而不知道该相信谁?你有没有曾经想要看 JVM 源码但不知道从何下手?

这篇文章会带你从头开始编译 JVM,告诉你如何在 IDE 里进行 Debug,并会简单过下 JVM 中 字节码执行、垃圾回收 以及 即时编译 的相关流程。

不用担心自己不熟悉 C++ 和 汇编 而看不懂 JVM 的源码。只要你熟悉 Java,有一点点 C 的基础,那么 “看懂” C++ 代码就没有太大问题,遇到一些不熟悉的语法直接问 ChatGPT 就行了。

环境准备

为了屏蔽底层操作系统的相关差异,我们会使用 Docker 来进行编译,然后在容器里运行程序,最后用 IDE + gdbserver 的方式进行远程调试。

我的环境情况如下:

  1. macOS Sonoma 14.4.1
    1. CPU M1 Pro
    2. 内存 32GB
  2. CLion 2024.2.2
  3. OpenJDK 11.0.0.2
    1. 下载地址 https://jdk.java.net/java-se-ri/11-MR3
  4. Docker 镜像
FROM ubuntu:18.04

RUN apt update && \
    apt install -y vim gdb \
    build-essential file unzip zip \
    # JDK 编译需要 Boot JDK,编译 11 需要 10 以上的版本
    openjdk-11-jdk \
    libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev \
    libcups2-dev libfontconfig1-dev libasound2-dev

CMD bash

编译

下面开始进行 JDK 的编译:

  1. 解压下载下来的 openjdk,假设解压到 ~/Downloads/openjdk
  2. 构建 Docker 镜像 docker build -t jdkbuilder .
  3. 运行镜像 docker run -v ~/Downloads/openjdk:/openjdk -it -p 1234:1234 jdkbuilder
    1. 1234 端口是为了后面进行远程 debug
  4. 在 Docker 里进行构建
cd /openjdk
bash configure --with-jvm-variants=server \
     --disable-warnings-as-errors \
     --with-debug-level=slowdebug \
     --with-native-debug-symbols=internal
# 编译整个 JDK
make images

在给 Docker 分配 10 核 10G 的情况下,完整编译整个耗时大概是 7 分钟。

现在我们就可以运行我们自己编译的 JDK 了

/openjdk/build/linux-aarch64-normal-server-slowdebug/jdk/bin/java -version

输出结果如下:

openjdk version "11.0.0.2-internal" 2024-07-02
OpenJDK Runtime Environment (slowdebug build 11.0.0.2-internal+0-adhoc..openjdk)
OpenJDK 64-Bit Server VM (slowdebug build 11.0.0.2-internal+0-adhoc..openjdk, mixed mode)

调试

我们先通过 gdb 找到程序的入口:

$ gdb java
(gdb) b main
Breakpoint 1 at 0xe98: file /openjdk/src/java.base/share/native/launcher/main.c, line 98.
(gdb) 

成功找到入口文件:/openjdk/src/java.base/share/native/launcher/main.c,在 CLion 里找到 main 函数下个断点。

为了后面调试的方便(主要就是忽略一些操作系统的中断),我们在本机先配置下 gdb,新建 ~/.gdbinit文件,内容如下:

handle SIGILL pass noprint nostop
handle SIGSEGV pass noprint nostop

接着我们在容器里运行 gdbserver:

cd /openjdk/build/linux-aarch64-normal-server-slowdebug/jdk/bin/
gdbserver :1234 ./java -version

下面在 CLion 进行连接,具体设置可以参考 The Remote Debug configuration,关键要设置的参数是:

  1. Debugger 里选择 GDB
  2. ‘target remote’ args 填 127.0.0.1:1234
  3. Path mappings 里 Remote 填容器里的 openjdk 地址 /openjdk,Local 填你本机的 openjdk 的绝对地址

运行后,不出意外程序就会成功停在下断点的地方了。

引子

上面我们一直以 java -version来做例子,那我们就先看下它背后究竟是怎么运行的。简单看下代码,不用太关心细节,前面主要就是些参数的处理,main 函数里的核心是:

// main.c
return JLI_Launch(margc, margv,
               jargc, (const char**) jargv,
               0, NULL,
               VERSION_STRING,
               DOT_VERSION,
               (const_progname != NULL) ? const_progname : *margv,
               (const_launcher != NULL) ? const_launcher : *margv,
               jargc > 0,
               const_cpwildcard, const_javaw, 0);

在 IDE 里跟进去,来到 src/java.base/share/native/libjli/java.c,同样的,忽略细节,我们大概看下代码,根据命名可以看到里面做了些 JVM 加载的事情,最后来到:

// java.c
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);

这里说下,因为我们并没有去配置 CLion 的环境,所以这时候你如果想要在 IDE 里跟进去,应该只会跳到头文件里,而看不到方法的定义,这时候我们就只能用搜索大法了。以 JVMInit为关键字,我们可以看到在一些名字形如 java_md_*.c 的文件里定义了这个函数,有多个定义是因为里面有平台相关的实现(md 是 machine-dependent 的意思) OpenJDK 需要针对不同的操作系统进行实现,我们以 linux 平台的实现 java_md_solinux.c来看下

// java_md_solinux.c
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
        int argc, char **argv,
        int mode, char *what, int ret)
{
    ShowSplashScreen();
    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

最终来到 ContinueInNewThread0函数:

// java_md_solinux.c
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);

里面的实现你可以看到就是调用 linux 的方法启动了一个操作系统线程,然后去运行了 JavaMain这个方法。对于 Java 程序员来说可能会觉得这里有点奇怪,为什么能传一个函数名?这是因为 C/C++ 里函数就是一个指针,指向一段内存里的代码区域,启动一个线程无非就是去运行那段内存里的代码。我们回到 JavaMain这个函数,可以看到:

// java.c
if (printVersion || showVersion) {
    PrintJavaVersion(env, showVersion);
    CHECK_EXCEPTION_LEAVE(0);
    if (printVersion) {
        LEAVE();
    }
}

进入 PrintJavaVersion函数:

// java.c
/*
 * Prints the version information from the java.version and other properties.
 */
static void
PrintJavaVersion(JNIEnv *env, jboolean extraLF)
{
    jclass ver;
    jmethodID print;

    NULL_CHECK(ver = FindBootStrapClass(env, "java/lang/VersionProps"));
    NULL_CHECK(print = (*env)->GetStaticMethodID(env,
                                                 ver,
                                                 (extraLF == JNI_TRUE) ? "println" : "print",
                                                 "(Z)V"
                                                 )
              );

    (*env)->CallStaticVoidMethod(env, ver, print, printTo);
}

根据函数名大概猜下,java -version最终实际运行的是 java.lang.VersionProps这个类的 println方法。

到这里你可能会有很多问题,比如 JNIEnv *env是怎么来的?CallStaticVoidMethod内部又是怎么运行的?我们先来看看 JNIEnv *env是怎么来的,也就是 JVM 是怎么初始化的。

JVM 初始化

回到 JavaMain函数,我们可以看到这段代码:

// java.c
/* Initialize the virtual machine */
start = CounterGet();
if (!InitializeJVM(&vm, &env, &ifn)) {
    JLI_ReportErrorMessage(JVM_ERROR1);
    exit(1);
}

进入 InitializeJVM

// java.c
/*
 * Initializes the Java Virtual Machine. Also frees options array when
 * finished.
 */
static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
    JavaVMInitArgs args;
    jint r;

    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;

    if (JLI_IsTraceLauncher()) {
        int i = 0;
        printf("JavaVM args:\n    ");
        printf("version 0x%08lx, ", (long)args.version);
        printf("ignoreUnrecognized is %s, ",
               args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
        printf("nOptions is %ld\n", (long)args.nOptions);
        for (i = 0; i < numOptions; i++)
            printf("    option[%2d] = '%s'\n",
                   i, args.options[i].optionString);
    }
    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}

关键是 ifn->CreateJavaVM(pvm, (void **)penv, &args);这段代码,但是 CreateJavaVMInvocationFunctions 这个结构体的一个属性,所以要找到它对应的函数定义需要我们找到 ifn是在哪里初始化的。

回到 java.cJLI_Launch函数,我们可以看到这段代码:

// java.c
if (!LoadJavaVM(jvmpath, &ifn)) {
    return(6);
}

LoadJavaVM又是一个平台相关的函数,我们可以在 java_md_solinux.c里找到定义,里面有一段这个代码:

// java.c
ifn->CreateJavaVM = (CreateJavaVM_t)
    dlsym(libjvm, "JNI_CreateJavaVM");
if (ifn->CreateJavaVM == NULL) {
    JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
    return JNI_FALSE;
}

这里用 linux 的动态链接的方式调用了 JNI_CreateJavaVM这个方法,通过搜索大法我们找到它的定义在 src/hotspot/share/prims/jni.cpp(看下路径,这里我们已经进入 HotSpot 的代码了),里面代码很简单,核心代码是:

// jni.cpp
result = JNI_CreateJavaVM_inner(vm, penv, args);

JNI_CreateJavaVM_inner里面就是真正的 JVM 初始化代码里,还记得我们最初的目的吗?JNIEnv *env是怎么来的。我们可以找到这么一段代码:

// jni.cpp
result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
if (result == JNI_OK) {
  JavaThread *thread = JavaThread::current();
  assert(!thread->has_pending_exception(), "should have returned not OK");
  /* thread is thread_in_vm here */
  *vm = (JavaVM *)(&main_vm);
  *(JNIEnv**)penv = thread->jni_environment();

至此,JVM 的初始化就完成了。

字节码执行

前面 java -version最后运行到了 (*env)->CallStaticVoidMethod(env, ver, print, printTo);,执行一个静态方法就会涉及到字节码的执行了,我们来看下CallStaticVoidMethod的实现。

先看下 thread->jni_environment();的实现:

// jni.h
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
// thread.hpp
JNIEnv        _jni_environment;
JNIEnv* jni_environment()                      { return &_jni_environment; }

因为 HotSpot 是 C++ 实现的,所以我们看下 JNIEnv_ 的定义,里面可以看到:

// jni.h
/*
 * We use inlined functions for C++ so that programmers can write:
 *
 *    env->FindClass("java/lang/String")
 *
 * in C++ rather than:
 *
 *    (*env)->FindClass(env, "java/lang/String")
 *
 * in C.
 */
struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus

    // ...
    void CallStaticVoidMethod(jclass cls, jmethodID methodID, ...) {
        va_list args;
        va_start(args,methodID);
        functions->CallStaticVoidMethodV(this,cls,methodID,args);
        va_end(args);
    }
    // ...
#endif /* __cplusplus */
}

这里你可能会发现方法的入参和前面我们看到的调用的地方不一样,第一个入参不是 env,看下源码里的注释,我们会知道是因为我们之前的调用是在 C 里面发起的,C 的环境下 JNIEnv 的定义是 JNINativeInterface_,里面关于 CallStaticVoidMethod的定义是:

// jni.h
struct JNINativeInterface_ {
    //...
    void (JNICALL *CallStaticVoidMethod)
      (JNIEnv *env, jclass cls, jmethodID methodID, ...);
    //...
}

继续看 functions->CallStaticVoidMethodV(this,cls,methodID,args);,找到 JNINativeInterface_定义,在 jni.cpp文件里, 最终 CallStaticVoidMethodV的实现对应的是jni_CallStaticVoidMethodV这个方法 :

// jni.cpp
JNI_ENTRY(void, jni_CallStaticVoidMethodV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args))
  JNIWrapper("CallStaticVoidMethodV");
  HOTSPOT_JNI_CALLSTATICVOIDMETHODV_ENTRY(env, cls, (uintptr_t) methodID);
  DT_VOID_RETURN_MARK(CallStaticVoidMethodV);

  JavaValue jvalue(T_VOID);
  JNI_ArgumentPusherVaArg ap(methodID, args);
  jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
JNI_END

jni_invoke_static里调用 JavaCalls::call(result, method, &java_args, CHECK);,对应方法定义在 javaCalls.cpp里:

// javaCalls.cpp
void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
  // Check if we need to wrap a potential OS exception handler around thread
  // This is used for e.g. Win32 structured exception handlers
  assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
  // Need to wrap each and every time, since there might be native code down the
  // stack that has installed its own exception handlers
  os::os_exception_wrapper(call_helper, result, method, args, THREAD);
}

os::os_exception_wrapper又是个平台相关的函数,linux 下就是直接调用传进来的javaCalls.cpp里的 call_helper函数,里面真正的发起调用的代码是:

// javaCalls.cpp
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
  { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner

    StubRoutines::call_stub()(
      (address)&link,
      // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
      result_val_address,          // see NOTE above (compiler problem)
      result_type,
      method(),
      entry_point,
      args->parameters(),
      args->size_of_parameters(),
      CHECK
    );

    result = link.result();  // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
    // Preserve oop return value across possible gc points
    if (oop_result_flag) {
      thread->set_vm_result((oop) result->get_jobject());
    }
  }
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)

StubRoutines::call_stub返回了一个 cpu 架构相关的函数,主要是做一些寄存器、栈的准备(这块没太深入研究),是以类似写汇编指令的形式在运行时动态往内存里写机器指令实现的(x86 下是在 stubGenerator_x86_64.cppgenerate_call_stub里),不过简单看下代码,可以发现最终是调用的 entry_point指向的代码:

// stubGenerator_x86_64.cpp
address start = __ pc();
// ...
// C++ 里栈上分配对象
const Address entry_point   (rbp, entry_point_off    * wordSize);
// ...
// call Java function
__ BIND(parameters_done);
__ movptr(rbx, method);             // get Method*
__ movptr(c_rarg1, entry_point);    // get entry_point
__ mov(r13, rsp);                   // set sender sp
BLOCK_COMMENT("call Java function");
__ call(c_rarg1);
// ...
return start;

entry_point指向的是 address entry_point = method->from_interpreted_entry();method怎么来的,我们先忽略,就看看 from_interpreted_entry()是什么,很简单,就是返回的method_from_interpreted_entry属性,通过搜索大法,我们发现它是这么被设置的:

// method.hpp
void set_interpreter_entry(address entry) {
  assert(!is_shared(), "shared method's interpreter entry should not be changed at run time");
  if (_i2i_entry != entry) {
    _i2i_entry = entry;
  }
  if (_from_interpreted_entry != entry) {
    _from_interpreted_entry = entry;
  }
}

// method.cpp

// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void Method::link_method(const methodHandle& h_method, TRAPS) {
  // ...
  if (!is_shared()) {
    assert(adapter() == NULL, "init'd to NULL");
    address entry = Interpreter::entry_for_method(h_method);
    assert(entry != NULL, "interpreter entry must be non-null");
    // Sets both _i2i_entry and _from_interpreted_entry
    set_interpreter_entry(entry);
  }
  // ...
}

// abstractInterpreter.hpp
static address    entry_for_kind(MethodKind k)                { assert(0 <= k && k < number_of_method_entries, "illegal kind"); return _entry_table[k]; }
static address    entry_for_method(const methodHandle& m)     { return entry_for_kind(method_kind(m)); }

可以看到就是基于方法类型从_entry_table来获取一个地址,_entry_table怎么初始化的呢?依旧是搜索,我们可以看到是在templateInterpreterGenerator.cpp里的 TemplateInterpreterGenerator::generate_all生成的:

// templateInterpreterGenerator.cpp
#define method_entry(kind)                                              \
  { CodeletMark cm(_masm, "method entry point (kind = " #kind ")"); \
    Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \
    Interpreter::update_cds_entry_table(Interpreter::kind); \
  }

看名字可以知道,这就是我们知道的模版解析器的代码了,模版解释器是把每个字节码对应到一段机器指令的,所以generate_method_entry里调用的generate_normal_entry方法又是一个和 cpu 架构相关的实现了,比如 x86 对应的是 templateInterpreterGenerator_x86.cpp。这里面也是往内存里动态写指令实现的,要看懂需要一些汇编的知识。不过我们不用太关心细节,可以结合一些网上关于模版解释器的介绍,我们可以猜想里面关于 dispatch 的代码是我们要关注的:

// templateInterpreterGenerator_x86.cpp
address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
    // ...
    __ dispatch_next(vtos);
    // ...
}

// interp_masm_x86.cpp
void InterpreterMacroAssembler::dispatch_next(TosState state, int step, bool generate_poll) {
  // load next bytecode (load before advancing _bcp_register to prevent AGI)
  load_unsigned_byte(rbx, Address(_bcp_register, step));
  // advance _bcp_register
  increment(_bcp_register, step);
  dispatch_base(state, Interpreter::dispatch_table(state), true, generate_poll);
}

注意到 Interpreter::dispatch_table(state),相关的实现如下:

// templateInterpreter.hpp
static address*   dispatch_table(TosState state)              { return _active_table.table_for(state); }

// templateInterpreter.cpp
void TemplateInterpreter::initialize() {
  // ...
  // initialize dispatch table
  _active_table = _normal_table;
}

// templateInterpreterGenerator.cpp
void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
  // ...
  // code for short & wide version of bytecode
  if (Bytecodes::is_defined(code)) {
    Template* t = TemplateTable::template_for(code);
    assert(t->is_valid(), "just checking");
    set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
  }
  if (Bytecodes::wide_is_defined(code)) {
    Template* t = TemplateTable::template_for_wide(code);
    assert(t->is_valid(), "just checking");
    set_wide_entry_point(t, wep);
  }
  // set entry points
  EntryPoint entry(bep, zep, cep, sep, aep, iep, lep, fep, dep, vep);
  Interpreter::_normal_table.set_entry(code, entry);
  Interpreter::_wentry_point[code] = wep;
}

// templateTable.hpp
// Templates
static Template        _template_table     [Bytecodes::number_of_codes];
static Template* template_for     (Bytecodes::Code code)  { Bytecodes::check     (code); return &_template_table     [code]; }
static Template* template_for_wide(Bytecodes::Code code)  { Bytecodes::wide_check(code); return &_template_table_wide[code]; }

其中 _template_tableTemplateTable这个类的静态属性,它的类型是 Template 的数组,在 C++ 里,数组类型初始化的时候就会把对应的对象的内存都分配了,Java 程序员熟悉的数组对应到 C++ 里其实是 对象指针的数组。_template_table里对象的数据初始化代码是在 templateTable.cpp里的 TemplateTable::initialize()函数里:

// templateTable.cpp
void TemplateTable::initialize() {
  if (_is_initialized) return;
  // ...
  // Java spec bytecodes                ubcp|disp|clvm|iswd  in    out   generator             argument
  def(Bytecodes::_invokevirtual       , ubcp|disp|clvm|____, vtos, vtos, invokevirtual       , f2_byte      );
  def(Bytecodes::_invokespecial       , ubcp|disp|clvm|____, vtos, vtos, invokespecial       , f1_byte      );
  def(Bytecodes::_invokestatic        , ubcp|disp|clvm|____, vtos, vtos, invokestatic        , f1_byte      );
  def(Bytecodes::_invokeinterface     , ubcp|disp|clvm|____, vtos, vtos, invokeinterface     , f1_byte      );
  def(Bytecodes::_invokedynamic       , ubcp|disp|clvm|____, vtos, vtos, invokedynamic       , f1_byte      );
  def(Bytecodes::_new                 , ubcp|____|clvm|____, vtos, atos, _new                ,  _           );
  // ...
}

每个字节码对应的具体实现只要跳转到对应的 generator 就可以看到了。

简单总结下 Java 字节码的执行流程,类加载后,每个类的方法会基于方法类型关联到一个动态初始化的代码,里面会基于字节码去 TemplateTable 查找对应的指令执行。TemplateTable 是在 JVM 初始化时构建的。

垃圾回收

垃圾回收有很多情况会被触发和设置的垃圾回收器也有关,我们这里就考虑使用 G1 回收器的情况下 new 对象时因为空间不足触发垃圾回收的这种情况。我们不会涉及到 GC 的具体逻辑,只是梳理下 GC 被触发的流程。

基于上面介绍的字节码执行的知识,我们现在要看 new 对应的字节码的实现,相关代码如下:

// templateTable_x86.cpp
void TemplateTable::_new() {
  //...
  Register rarg1 = LP64_ONLY(c_rarg1) NOT_LP64(rax);
  Register rarg2 = LP64_ONLY(c_rarg2) NOT_LP64(rdx);

  __ get_constant_pool(rarg1);
  __ get_unsigned_2_byte_index_at_bcp(rarg2, 1);
  call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), rarg1, rarg2);
   __ verify_oop(rax);

  // continue
  __ bind(done);
}

// interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
  //...
  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END

// instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
  //...
  i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

// collectedHeap.cpp
oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
  ObjAllocator allocator(klass, size, THREAD);
  return allocator.allocate();
}

// memAllocator.cpp
oop MemAllocator::allocate() const {
  oop obj = NULL;
  {
    Allocation allocation(*this, &obj);
    HeapWord* mem = mem_allocate(allocation);
    if (mem != NULL) {
      obj = initialize(mem);
    }
  }
  return obj;
}

HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
  if (UseTLAB) {
    HeapWord* result = allocate_inside_tlab(allocation);
    if (result != NULL) {
      return result;
    }
  }

  return allocate_outside_tlab(allocation);
}

HeapWord* MemAllocator::allocate_outside_tlab(Allocation& allocation) const {
  allocation._allocated_outside_tlab = true;
  HeapWord* mem = _heap->mem_allocate(_word_size, &allocation._overhead_limit_exceeded);
  if (mem == NULL) {
    return mem;
  }

  NOT_PRODUCT(_heap->check_for_non_bad_heap_word_value(mem, _word_size));
  size_t size_in_bytes = _word_size * HeapWordSize;
  _thread->incr_allocated_bytes(size_in_bytes);

  return mem;
}

// g1CollectedHeap.cpp
HeapWord*
G1CollectedHeap::mem_allocate(size_t word_size,
                              bool*  gc_overhead_limit_was_exceeded) {
  assert_heap_not_locked_and_not_at_safepoint();

  if (is_humongous(word_size)) {
    return attempt_allocation_humongous(word_size);
  }
  size_t dummy = 0;
  return attempt_allocation(word_size, word_size, &dummy);
}

inline HeapWord* G1CollectedHeap::attempt_allocation(size_t min_word_size,
                                                     size_t desired_word_size,
                                                     size_t* actual_word_size) {
  //...
  HeapWord* result = _allocator->attempt_allocation(min_word_size, desired_word_size, actual_word_size);

  if (result == NULL) {
    *actual_word_size = desired_word_size;
    result = attempt_allocation_slow(desired_word_size);
  }
  //...
}

整个代码不复杂,就是跳来跳去的,我上面是按照调用顺序整理的,最终走到 attempt_allocation_slow方法,里面就有触发 GC 相关的代码了:

// g1CollectedHeap.cpp
HeapWord* G1CollectedHeap::attempt_allocation_slow(size_t word_size) {
    //...
    if (should_try_gc) {
      bool succeeded;
      result = do_collection_pause(word_size, gc_count_before, &succeeded,
                                   GCCause::_g1_inc_collection_pause);
      //...
    }
    //...
}

HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
                                               uint gc_count_before,
                                               bool* succeeded,
                                               GCCause::Cause gc_cause) {
  assert_heap_not_locked_and_not_at_safepoint();
  VM_G1CollectForAllocation op(word_size,
                               gc_count_before,
                               gc_cause,
                               false, /* should_initiate_conc_mark */
                               g1_policy()->max_pause_time_ms());
  VMThread::execute(&op);

  HeapWord* result = op.result();
  bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
  assert(result == NULL || ret_succeeded,
         "the result should be NULL if the VM did not succeed");
  *succeeded = ret_succeeded;

  assert_heap_not_locked();
  return result;
}

可以发现 GC 是通过在 VMThread 里执行对应的 VM_Operation 实现的,具体逻辑只要去看 VM_G1CollectForAllocation的实现就可以了。

即时编译

同样的,我们也不会涉及具体编译的逻辑,只是梳理下即时编译触发的流程。

我们知道,JVM 是基于方法被调用次数以及循环执行的次数来决策是否进行即时编译的。所以我们可以去看invokevirtual字节码的实现,相关代码如下:

// templateTable_x86.cpp
void TemplateTable::invokevirtual(int byte_no) {
  transition(vtos, vtos);
  assert(byte_no == f2_byte, "use this argument");
  prepare_invoke(byte_no,
                 rbx,    // method or vtable index
                 noreg,  // unused itable index
                 rcx, rdx); // recv, flags

  // rbx: index
  // rcx: receiver
  // rdx: flags

  invokevirtual_helper(rbx, rcx, rdx);
}
void TemplateTable::invokevirtual_helper(Register index,
                                         Register recv,
                                         Register flags) {
  //...
  __ jump_from_interpreted(method, rdx);
}

// interp_masm_x86.cpp
void InterpreterMacroAssembler::jump_from_interpreted(Register method, Register temp) {
  //...
  jmp(Address(method, Method::from_interpreted_offset()));
}

// method.hpp
static ByteSize from_interpreted_offset()      { return byte_offset_of(Method, _from_interpreted_entry ); }

可以看到,最终又调用到了我们前面介绍的 TemplateInterpreterGenerator::generate_normal_entry生成的代码里,可以看到和方法计数相关的代码:

// templateInterpreterGenerator_x86.cpp
address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
  //...
  // increment invocation count & check for overflow
  Label invocation_counter_overflow;
  Label profile_method;
  Label profile_method_continue;
  if (inc_counter) {
    generate_counter_incr(&invocation_counter_overflow,
                          &profile_method,
                          &profile_method_continue);
    if (ProfileInterpreter) {
      __ bind(profile_method_continue);
    }
  }
  //...
  // invocation counter overflow
  if (inc_counter) {
    if (ProfileInterpreter) {
      // We have decided to profile this method in the interpreter
      __ bind(profile_method);
      __ call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::profile_method));
      __ set_method_data_pointer_for_bcp();
      __ get_method(rbx);
      __ jmp(profile_method_continue);
    }
    // Handle overflow of counter and compile method
    __ bind(invocation_counter_overflow);
    generate_counter_overflow(continue_after_compile);
  }
  return entry_point;
}

void TemplateInterpreterGenerator::generate_counter_overflow(Label& do_continue) {
  //...
  __ call_VM(noreg,
             CAST_FROM_FN_PTR(address,
                              InterpreterRuntime::frequency_counter_overflow),
             rarg);
  //...
}

//interpreterRuntime.cpp
nmethod* InterpreterRuntime::frequency_counter_overflow(JavaThread* thread, address branch_bcp) {
  nmethod* nm = frequency_counter_overflow_inner(thread, branch_bcp);
  //...
}

IRT_ENTRY(nmethod*,
          InterpreterRuntime::frequency_counter_overflow_inner(JavaThread* thread, address branch_bcp))
  //...
  nmethod* osr_nm = CompilationPolicy::policy()->event(method, method, branch_bci, bci, CompLevel_none, NULL, thread);
  //...
IRT_END

//simpleThresholdPolicy.cpp
nmethod* SimpleThresholdPolicy::event(const methodHandle& method, const methodHandle& inlinee,
                                      int branch_bci, int bci, CompLevel comp_level, CompiledMethod* nm, JavaThread* thread) {
  //...
  if (bci == InvocationEntryBci) {
    method_invocation_event(method, inlinee, comp_level, nm, thread);
  } else {
    // method == inlinee if the event originated in the main method
    method_back_branch_event(method, inlinee, bci, comp_level, nm, thread);
    // Check if event led to a higher level OSR compilation
    nmethod* osr_nm = inlinee->lookup_osr_nmethod_for(bci, comp_level, false);
    if (osr_nm != NULL && osr_nm->comp_level() > comp_level) {
      // Perform OSR with new nmethod
      return osr_nm;
    }
  }
  return NULL;
}

void SimpleThresholdPolicy::method_invocation_event(const methodHandle& mh, const methodHandle& imh,
                                                      CompLevel level, CompiledMethod* nm, JavaThread* thread) {
  if (should_create_mdo(mh(), level)) {
    create_mdo(mh, thread);
  }
  CompLevel next_level = call_event(mh(), level, thread);
  if (next_level != level) {
    if (maybe_switch_to_aot(mh, level, next_level, thread)) {
      // No JITting necessary
      return;
    }
    if (is_compilation_enabled() && !CompileBroker::compilation_is_in_queue(mh)) {
      compile(mh, InvocationEntryBci, next_level, thread);
    }
  }
}

void SimpleThresholdPolicy::compile(const methodHandle& mh, int bci, CompLevel level, JavaThread* thread) {
  //...
  if (!CompileBroker::compilation_is_in_queue(mh)) {
    if (PrintTieredEvents) {
      print_event(COMPILE, mh, mh, bci, level);
    }
    submit_compile(mh, bci, level, thread);
  }
}

void SimpleThresholdPolicy::submit_compile(const methodHandle& mh, int bci, CompLevel level, JavaThread* thread) {
  int hot_count = (bci == InvocationEntryBci) ? mh->invocation_count() : mh->backedge_count();
  update_rate(os::javaTimeMillis(), mh());
  CompileBroker::compile_method(mh, bci, level, mh, hot_count, CompileTask::Reason_Tiered, thread);
}

nmethod* CompileBroker::compile_method(const methodHandle& method, int osr_bci,
                                         int comp_level,
                                         const methodHandle& hot_method, int hot_count,
                                         CompileTask::CompileReason compile_reason,
                                         DirectiveSet* directive,
                                         Thread* THREAD) {
        //...
        compile_method_base(method, osr_bci, comp_level, hot_method, hot_count, compile_reason, is_blocking, THREAD);
        //...
}

void CompileBroker::compile_method_base(const methodHandle& method,
                                        int osr_bci,
                                        int comp_level,
                                        const methodHandle& hot_method,
                                        int hot_count,
                                        CompileTask::CompileReason compile_reason,
                                        bool blocking,
                                        Thread* thread) {
        //...
        task = create_compile_task(queue,
                               compile_id, method,
                               osr_bci, comp_level,
                               hot_method, hot_count, compile_reason,
                               blocking);
        //...
}

CompileTask* CompileBroker::create_compile_task(CompileQueue*       queue,
                                                int                 compile_id,
                                                const methodHandle& method,
                                                int                 osr_bci,
                                                int                 comp_level,
                                                const methodHandle& hot_method,
                                                int                 hot_count,
                                                CompileTask::CompileReason compile_reason,
                                                bool                blocking) {
  CompileTask* new_task = CompileTask::allocate();
  new_task->initialize(compile_id, method, osr_bci, comp_level,
                       hot_method, hot_count, compile_reason,
                       blocking);
  queue->add(new_task);
  return new_task;
}

按照上面的调用顺序一路跟下来,我们可以看到最终会生成一个编译任务并放到一个队列里,显然会有另外一个线程从这个队列里取任务来执行。通过下断点,关键字搜索,再结合我们前面介绍的 JVM 初始化过程,不难发现最终编译的线程的入口是在 thread.cpp里定义的:

// thread.cpp
static void compiler_thread_entry(JavaThread* thread, TRAPS) {
  assert(thread->is_Compiler_thread(), "must be compiler thread");
  CompileBroker::compiler_thread_loop();
}

CompileBroker::compiler_thread_loop()就会不停的从编译任务队列里取任务来执行:

// compileBroker.cpp
void CompileBroker::compiler_thread_loop() {
    //...
    // Compile the method.
    if ((UseCompiler || AlwaysCompileLoopMethods) && CompileBroker::should_compile_new_jobs()) {
        invoke_compiler_on_method(task);
        thread->start_idle_timer();
    } else {
        // After compilation is disabled, remove remaining methods from queue
        method->clear_queued_for_compilation();
        task->set_failure_reason("compilation is disabled");
    }
    //...
}

void CompileBroker::invoke_compiler_on_method(CompileTask* task) {
    //...
    comp->compile_method(&ci_env, target, osr_bci, directive);
    //...
}

//c1_Compiler.cpp
void Compiler::compile_method(ciEnv* env, ciMethod* method, int entry_bci, DirectiveSet* directive) {
    BufferBlob* buffer_blob = CompilerThread::current()->get_buffer_blob();
    assert(buffer_blob != NULL, "Must exist");
    // invoke compilation
    {
        // We are nested here because we need for the destructor
        // of Compilation to occur before we release the any
        // competing compiler thread
        ResourceMark rm;
        Compilation c(this, env, method, entry_bci, buffer_blob, directive);
    }
}

//c1_Compilation.cpp
Compilation::Compilation(AbstractCompiler* compiler, ciEnv* env, ciMethod* method,
int osr_bci, BufferBlob* buffer_blob, DirectiveSet* directive)
//...
{
    //...
    compile_method();
    //...
}

void Compilation::compile_method() {
    //...
    if (InstallMethods) {
        // install code
        PhaseTraceTime timeit(_t_codeinstall);
        install_code(frame_size);
    }
    //...
}

void Compilation::install_code(int frame_size) {
    // frame_size is in 32-bit words so adjust it intptr_t words
    assert(frame_size == frame_map()->framesize(), "must match");
    assert(in_bytes(frame_map()->framesize_in_bytes()) % sizeof(intptr_t) == 0, "must be at least pointer aligned");
    _env->register_method(
        method(),
        osr_bci(),
        &_offsets,
        in_bytes(_frame_map->sp_offset_for_orig_pc()),
        code(),
        in_bytes(frame_map()->framesize_in_bytes()) / sizeof(intptr_t),
        debug_info_recorder()->_oopmaps,
        exception_handler_table(),
        implicit_exception_table(),
        compiler(),
        has_unsafe_access(),
        SharedRuntime::is_wide_vector(max_vector_size())
        );
}

// ciEnv.cpp
void ciEnv::register_method(ciMethod* target,
                            int entry_bci,
                            CodeOffsets* offsets,
                            int orig_pc_offset,
                            CodeBuffer* code_buffer,
                            int frame_words,
                            OopMapSet* oop_map_set,
                            ExceptionHandlerTable* handler_table,
                            ImplicitExceptionTable* inc_table,
                            AbstractCompiler* compiler,
                            bool has_unsafe_access,
                            bool has_wide_vectors,
                            RTMState  rtm_state) {
  //...
  // Allow the code to be executed
  method->set_code(method, nm);
  //...
}


// method.cpp
// Install compiled code.  Instantly it can execute.
void Method::set_code(const methodHandle& mh, CompiledMethod *code) {
  //...
  // Instantly compiled code can execute.
  if (!mh->is_method_handle_intrinsic())
    mh->_from_interpreted_entry = mh->get_i2c_entry();
}

编译完成后会将我们前面介绍的method_from_interpreted_entry修改到编译过的代码入口,这样下次方法执行的时候就会走即时编译的代码了。

期权价格的上下界

期权的定价模型很复杂, 不过价格的上下界是比较好确定的. 先简单介绍下期权, 期权是一种未来的权利, call 的期权是未来以一定价格买入资产的权利, put 的期权是未来以一定价格卖出资产的权利. 既然是一种权利, 那么就可以选择行使或者不行使这个权利. 需要注意的是, 卖出 call 或者 put 的期权一方, 是必须确保买入一方权利可以正常行使的, 保证的方式是通过保证金制度来进行的.
金融衍生品的价格需要满足的一个条件就是市场无法在不承担风险的情况下获得超过无风险利率的收益. 下面的讨论我们假设期权是欧式期权 (只能在期权到期日行权), 其底层的金融资产是股票, 现价是 S , 市场无风险利率为 r, 期权的行权价为 K, 期权到期时间为 t.

价格上界

先来看下 call 的期权, 我们假设现在 call 期权的价格为 C, 超过了其上界, 因为其价格超过了上界, 那么我们这时候就应该卖出期权, 这样我们就获得了现金 C, 卖出 call 期权意味着在最差情况下到了期权行权日需要卖出股票给买入期权的一方, 为了抵消风险我们需要在卖出期权的时候以现价 S 买入对应股票, 这样我们就付出了现金 S. 这样的资产组合我们在期权到期时是不用承担任何风险的. 只要 C > S 就存在套利空间, 所以 call 的期权的上界为:
\[ C \leq S \]
下面我们看 put 的期权, 假设现在 put 期权的价格为 P , 超过了其上界, 因为其价格超过了上界, 那么我们这时候就应该卖出, 这样我们就获得了现金 P, 卖出 put 期权意味这在最差情况下到了期权行权日需要以价格 K 购买买入期权一方的股票. 现在的现金 P 到了行权日会以无风险利率 r 增值到 \(Pe^{rt}\). 只要 \(Pe^{rt} > K\) 就存在套利空间, 所以 put 的期权的上界为:
\[ P \leq Ke^{-rt} \]

价格下界

下界的确定和上界的确定是类似的, 只是多了一个额外的情况, 就是期权的价格至少是大于等于零的, 因为如果期权的价格为负, 那么期权的买入方的收益肯定是正的. 我们先来考虑 call 的期权. 同样的, 假设现在 call 期权的价格为 C, 低于其下界, 因为价格低于下界, 我们这时应该买入期权, 这样我们就付出了现金 C, 买入期权意味着到了期权行权日我们至少可以以价格 K 买入股票, 所以我们可以在现在做空股票, 这样就获得了现金 S, 此时我们手上有现金 S - C. 到了期权行权日, 我们的现金将以无风险利率 r 增值到 \((S-C)e^{rt}\), 同时为了平仓之前的做空操作, 我们最多要付出 K 的现金来买入股票, 那么只要 \((S-C)e^{rt} > K\) 或者 C < 0 就存在套利空间, 所以 call 的期权的下界为:
\[ C \geq MAX(S - Ke^{-rt}, 0) \]
下面看 put 的期权, 假设现在 put 的期权的价格为 P, 低于其下界, 同样的, 我们买入期权, 这样就付出了现金 P, 买入期权意味着我们在期权行权日至少可以以价格 K 卖出股票, 所以我们可以做多股票, 为此我们付出了现金 S, 现在我们总共借入了现金 S + P, 因为无风险利率为 r, 所以到了期权行权日我们需要偿还 \((S+P)e^{rt}\) 的现金, 同时我们之前做多的股票至少可以卖出 K 的价格, 那么只要 \((S+P)e^{rt} < K\) 或者 P < 0 就存在套利空间, 所以 put 的期权的下界为:
\[ P \geq MAX(Ke^{-rt} - S, 0) \]

Pyhon decorator descriptor and metaclass

backtrader 支持在线实时的数据源输入, 目前支持海外的 IbPy (盈透), Oanda 等方式的接入, 不支持我准备用的富途, 于是准备看源码参照 IbPy, Oanda 接入的方式, 自己对接下富途. 看了下源码, 发现用到了 Python 的几个特性: 装饰器, 描述符, 元类. 以前简单接触过, 理解不是太深, 所以这次就稍微整理下.

装饰器 decorator

Decorator 还是比较好理解的, 它本质上来说就是个函数 (严格来说只要是 callable 的就行), 如果你把它放在方法上, 那么它的入参就是这个方法, 返回的新方法就会替代原本的方法定义. 如果放在类上, 那么它的入参就是这个类, 返回的新类会替代原本的类定义.

Decorator 有两种形式, 一种就是直接的 @decorator 形式, 这个就是我上面说的. 另一种是 @decorator(arg1, arg2, ...) 这种用法其实是前一种的变体, decorator(arg1, arg2, ...) 就是个简单的方法调用, Python 会把它的返回值当成真正的装饰器来解析.

描述符 descriptor

Descriptor 的定义很简单, 只要是一个类定义了下面几个方法, 那么它就是一个描述符:

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

如果一个描述符只定义了上述方法中的 __get__ 方法, 那么它就是一个 non-data 的描述符, 如果除了 __get__ 以外还定义了其它的方法 ( __set__ 或者 __delete__ ), 那么就是一个 data 的描述符. data 与 non-data 的描述符有什么区别? 要回答这个问题, 我们要先明白, 描述符的作用是什么. 首先描述符只有被定义为类变量 (class variable [1]) 时才有意义, 当你使用描述符时, Python 会调用相应的描述符的方法而不是直接操作对应的描述符. 可以理解为描述符提供了一种对类变量的 get ( Clz.descr ), set ( Clz.descr = x ), delete ( del Clz.descr ) 进行重载的机制. 不过这边有点不符合直觉的是, 一般我们重载方法的时候是重载的主语, 所以按道理来说我们要重载 Clz.descr 这个表达式的话, 应该是在 Clz 中定义 __get__ 来重载, 但是描述符的重载是在宾语上发生的, 我们是在 descr 上定义的 __get__ 方法. 不过这个也好理解, 在 descr 定义就不用在每个用到描述符的地方都重复定义一遍了.

下面来回答 data 与 non-data 的描述符的区别. 当我们使用 obj.descr  表达式时, 如果对象中找不到 descr  Python 就会找到 Clz.descr  然后走描述符那套处理逻辑, 那么对象中有同名的 descr  时会怎么样呢? 这时候就要看 descr  是 data 还是 non-data 了. 如果是 data 的, 那么 Clz.descr  的优先级就高于 obj.descr . 如果是 non-data 的, 那么 obj.descr  的优先级就高于 Clz.descr  不会走描述符那套逻辑. 具体解析顺序的实现在 object.__getattribute__()  中, 下面是用 Python 描述的实现逻辑:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

常见的 @staticmethod 与 @classmethod 就是将装饰器与描述符结合起来的一种应用. 下面是对应的 Python 的实现版本:

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f
    
class MethodType:
    "Emulate Py_MethodType in Objects/classobject.c"

    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)
    
class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            # 这段逻辑是 Python 3.9 新增的, 用于实现描述符的链式调用 [1]
            # 原文档里上面的 if 判断条件是 hasattr(obj, '__get__'), 这个是错误的
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

元类 metaclass

元类算是这几个概念里比较难理解的了, 不过只要我们记住在 Python 里任何东西都是对象这个知识点, 那么理解元类就比较简单了. 任何东西都是对象, 那么类自然也不例外. 类的类被称为元类, 一般情况下, 我们通过 class 定义的类的元类是 type , type 的元类是 type 自身. 我们通过 type(Clz) 表达式来查看一个类的元类是什么.

在 Python2 中要指定一个类的元类通过 __metaclass__ 这个类变量来指定, 要注意的是, 只有当我们使用 class 关键字来定义一个类的时候, __metaclass__ 才生效 [2]. Python3 中使用下面的语句来指定一个类的元类:

class Clz(metaclass=type):
    pass

哪些对象能作为元类的? 答案是任何 callable 的对象. 当你指定了元类, Python 在创建类的时候会调用你指定的元类, 并传入以下参数:

  1. name: 当前类的名字
  2. bases: 当前类的基类
  3. attrs: 当前类的属性


一个类的默认的元类是 type , 所以当你使用 class 关键字来定义类时, 等价于下面两条语句:

class Clz(object):
    hello = 'world'

# 等价
Clz = type('Clz', (object,), {'hello': 'world'})

# 等价
Clz = type.__new__(type, 'Clz', (object,), {'hello': 'world'})

上面两种写法有什么区别呢? 只要了解了 __init__ 和 __new__ 的区别就明白了. __init__ 我们都知道, 当我们执行 Clz(arg1, arg2) 创建对象时, Python 会执行 Clz.__init__(self, arg1, args) 其中 self 表示当前正在创建的对象. 很自然的一个问题就是, self 哪里来的呢? self 就是通过 __new__ 创建出来的. 总结下就是, __new__ 创建对象, __init__ 初始化对象, 所以当我们执行 Clz(arg1, arg2) 时等价于执行下面的语句:

# Clz(arg1, arg2) 等价于
obj = Clz.__new__(Clz, arg1, arg2)
obj.__init__(arg1, arg2)

顺便再多说几句, 官方文档里提到 [3]:

If new() does not return an instance of cls, then the new instance’s init() method will not be invoked.


也就是说, 如果我们重写 __new__  方法, 但是 __new__  返回的对象的类不是第一个参数指定的类,
那么新创建的对象的 __init__  方法不会被调用:

class Foo:
    def __new__(cls, *args, **kwargs):
        print('Foo new')
        return Bar.__new__(Bar, *args, **kwargs)


class Bar:
    def __init__(self):
        print('Bar init')

# 不会调用到 Bar 的 __init__ 方法
Foo()


说了这么多的废话, 好像和元类一点关系都没有. 其实不是的, 我们看下 type 的 __new__ 方法的定义, 第一个参数表示要创建的对象的类, 而我们现在创建的对象就是一个类, 类的类可不就是我们所说的元类吗? 需要说明的是, 如果使用 type 的 __new__ 方法来创建类并指定元类, 那么就仅仅是指定了新创建类的元类, 新类并不是通过 call 元类来创建的.

最后我们说下元类的继承问题, 一句话, 一个类的元类如果没有指定的话会从父类继承. 虽然说任何 callable 的对象都可以当做元类, 但是如果我们想要写个可以被继承的元类就需要用 type.__new__ 来创建类:

class MetaClz(type):
    def __new__(mcs, *args, **kwargs):
        return type.__new__(mcs, *args, **kwargs)

class Foo(metaclass=MetaClz):
    pass

class Bar(Foo):
    pass

MetaClz 的 __new__ 方法中创建了一个类, 并且指定了这个类的元类, 所以最终创建出来的 Foo 的元类是 MetaClz , Bar 会继承 Foo 的元类 MetaClz , 所以 Bar 的元类也是 MetaClz . 我们再看另外一种错误的写法:

class MetaClz(type):
    def __new__(mcs, *args, **kwargs):
        return type(*args, **kwargs)

class Foo(metaclass=MetaClz):
    pass

class Bar(Foo):
    pass

和之前的写法唯一的区别就是我们直接用了 type() 来创建类, 这样写的话, 最终创建出来的 Foo 的元类是 type , Bar 会继承 Foo 的元类 type , 所以 Bar 的元类也是 type . 那么谁的元类是 MetaClz 呢? 答案是下面这个语句的元类是 MetaClz :

# 这条语句的元类是 MetaClz
class Foo(metaclass=MetaClz):
    pass


[1] Descriptor HowTo Guide
[2] Inheritance of metaclass
[3] Data model

GS108PEv3 实现单线复用

更新

打电话给电信把光猫改成桥接模式,在路由器输入宽带账号和密码始终连接不上,摸索了一阵发现,原来使用 PPPoE 模式时,路由器 CPU 的口子不再是 8 口,而是 5 口。同时,从光猫来的数据送到 5 口时需要是 Untagged 的。另外就是在给 CPU 的口子配 Untagged 时必须强制指定 U,否则就是 T 的。


原文

家里网络很早之前就改造好了, 今天忽然想要改下交换机的配置, 但是因为时间太长, 一下次想不起来怎么连上交换机了. 折腾了好一会才连上, 所以决定把家里网络的相关改造记录下, 做个备份.

为什么需要单线复用?

现在我们上网通常只需要一个电信猫, 这个猫一般是具备直接拨号的和 WIFI 连入的, 只是性能和可玩性比较差, 所以我们还是会再接一个路由器, 我用的是淘宝淘的二手 NETGEAR R6300v2. 这样一来, 我们就需要把电信猫和路由器同时放到网络设备箱里, 如果你像我一样用的是 R6300v2 这种比较大的路由器可能就放不下了, 另外无线路由器放在网络设备箱里也会影响无线信号. 最好的办法就是, 无线路由器可以放到任意有有线的位置, 比如客厅. 但是像客厅这些地方, 一般只有一根网线用于连接路由器的 WAN 口, 这样其他地方的网线就无法直接与路由器相连了. 怎么办呢? 这时候就需要我们进行单线复用了, 也就是通过 WAN 口既可以与电信猫传输数据, 又可以与本地 LAN 的设备通信.

前置知识点

实现单线复用用到的关键手段是 VLAN.

VLAN

当我们本地网络有 N 个设备, 这些设备需要相互通信的话, 就需要借助于交换机. 有时候出于安全考虑, 我们希望本地网络中可以手动划分设备分组, 只有被划到一个分组中的设备可以相互通信, 不同组之间的设备相互隔离. 要实现这个功能, 就需要用到 VLAN. 现在通常用的 VLAN 协议是 802.1Q, 协议会在数据链路层的数据包中插入 VID (VLAN ID), 交换机根据 VID 进行相应的数据处理转发. 在 802.1Q 协议之前, 还有 Port-based VLAN, 使用这种 VLAN 协议, 数据包只在交换机内部有 VLAN 的概念, 出了交换机, 数据包就和普通的数据包没有区别了, 所以 Port-based VLAN 无法进行交换机的级联. 我们主要介绍 802.1Q 的 VLAN 协议. 对于交换机上的端口来说有三种类型:

  • Access
  • Trunk
  • Hybird


这几种类型有什么区别呢? 我们从端口收到数据的处理方式来看. 对于一个端口来说, 它可以从交换机外部接收到数据, 也可以从交换机内部接收到数据. 对于前面一种我们暂时把它叫做 input, 后一种叫做 receive. 对于 input 来说, 有可能接收到不带 VID 数据, 也可能接收到带 VID 的数据. 对于 receive 来说接收到的都是带 VID 数据. 顺便再说下, 每个端口还有一个 PVID (Port VLAN ID) 的属性, 一个端口可以属于多个 VLAN, 但是只能有一个 PVID. 下面我们就从前面说的几个方面来看几种端口类型的区别:

  • Access
    • input
      • 有 VID: 直接丢弃
      • 无 VID: VID 打上 PVID 的标, 再发送给属于同一个 VLAN 的端口
    • receive
      • VID 与 PVID 相同: 将 VID 剥离后转发出去
      • VID 与 PVID 不同: 直接丢弃
  • Trunk
    • input
      • 有 VID: 端口属于 VID 的 VLAN 就转发, 不属于就丢弃
      • 无 VID: 同 Access 处理方式
    • receive
      • VID 与 PVID 相同: 同 Access 处理方式
      • VID 与 PVID 不同: 端口属于 VID 的 VLAN 就直接转发 (不剥离 VID 信息), 否则丢弃
  • Hybird
    • input
      • 有 VID: 同 Trunk 处理方式
      • 无 VID: 同 Access 处理方式
    • receive
      • VID 与 PVID 相同: 同 Access 处理方式
      • VID 与 PVID 不同: 如果端口不属于 VID 的 VLAN 就直接丢弃, 如果属于 VID 所在 VLAN 还要看端口在该 VID 下是配置的 U (Untag) 还是 T (Tag), 如果是 U 就将 VID 剥离后转发, 否则就直接转发

WAN

之所以单线复用可以成功的另一个因素是, 路由器区分 WAN 口与 LAN 的底层是基于 VLAN 实现的. WAN 口与 LAN 口属于不同的 VLAN , CPU 根据 VID 来决定是按照 WAN 还是 LAN 来处理. 所以只要我们交换机将从电信猫过来的数据打上和 WAN 相同的 VID, 从本地网络的过来数据打上与 LAN 相同的 VID, 路由器就可以正常工作了.

实操

知道了原理, 剩下的就好办了.

交换机配置

先连上交换机, GS108PEv3 是可网管的交换机 (v2 是不行的, 只能用专用的工具连接). 如果不知道交换机的 IP 可以用 Netgear 提供的工具来设备发现 (需要与交换机直连, 中间不能有路由器) [1, 2, 3].

网件的端口都是 Hybird 类型的. 我这边是将 5 号口与路由器 WAN 口相连, 8 号口与电信光猫相连, 其余的口用于 LAN 设备连接.

VLAN ID Port Members
1 1U 2U 3U 4U 5T 6U 7U
2 5T 8U

VLAN 1 中的 5 号口设置成 T, 其余口设置成 U. VLAN 2 中 5 号口也设置成 T, 8 号口设置成 U. 最后把 8 号口的 PVID 设置成 2, 其余口设置成 1.

顺便说下, 网件交换机里, T 表示 Tagged, U 表示 Untagged, 没有标记表示这个端口不属于这个 VLAN [4].

路由器配置


我们先 SSH 到路由器, 对于 R6300v2 使用 robocfg show 查看 VLAN 设置:

# robocfg show
Switch: enabled 
Port 0:   DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 1:   DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 2: 1000FD enabled stp: none vlan: 1 jumbo: off mac: 00:11:32:bb:c3:45
Port 3:   DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 38:f9:d3:19:0a:6b
Port 5: 1000FD enabled stp: none vlan: 1 jumbo: off mac: dc:ef:09:94:2d:f7
Port 7:   DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 8: 1000FD enabled stp: none vlan: 1 jumbo: off mac: dc:ef:09:94:2d:f7
VLANs: BCM5301x enabled mac_check mac_hash
   1: vlan1: 0 1 2 3 4t 8t
   2: vlan2: 4t 8t
  56: vlan56: 8u
  57: vlan57: 2t 4t 5t 8t
  58: vlan58: 4 7 8t
  59: vlan59: 0 1t 2 3t 5
  60: vlan60: 0 4t
  61: vlan61: 0 2t 3 5 8u
  62: vlan62: 0t 1t 2t 3t

上面是我现在的配置, 很多是路由器默认的, 和我们这次相关的就是 vlan1 与 vlan2 的配置. (这里带 t 的表示 Tagged, 没有 t 的表示 Untagged, 与交换机有区别). 对于我的路由器来说, 4 口是 WAN 口, 8 口是什么呢? 8 口代表的是 CPU, 路由器把 CPU 也抽象成了一个端口.

Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 38:f9:d3:19:0a:6b 中的 vlan: 2 代表的是端口的 PVID, 我们可以用 robocfg port 4 tag 2 来配置 [5].

为了路由器每次重启都能自动配置 VLAN, 我们需要在 /jffs/scripts/services-start 中配置启动命令 [6]:

#!/bin/sh
robocfg vlan 2 ports "4t 8t"
robocfg vlan 1 ports "0 1 2 3 4t 8t"

这样, 单线复用就配置好了.

备注

家里各设备的网管地址 (年纪大了, 记性不好):

  • 路由器: 192.168.50.1
  • 交换机: 192.168.2.4 (密码要求有大小写与数字)
  • AP: 到路由器里找 AP-105 的 IP
  • 电信猫: 192.168.2.1, hcsk5


[1] GS108PEv3 — 8 Port Gigabit Ethernet PoE Smart Managed Plus Switch with 4-Ports PoE
[2] What is the default IP address of my NETGEAR Smart Managed Plus, Smart Managed Pro, or Insight Managed Smart Cloud switch?
[3] How do I access the admin page of my ProSAFE Web Managed Plus or Click Switch?
[4] 简单网管交换机的 VLAN 功能设置及应用
[5] 单线复用实践篇 #74
[6] 网件GS108PE交换机,搭配R8500梅林固件,实现单线复用

Mac 调试编译 OpenJDK9

几年前花费了很大精力在 Mac 下把 JDK7 编译了出来, 中间踩了无数的坑, 前几天想编译调试 JDK9 看看各种锁优化的实现. 本以为参考下之前的文章很快就能编译出来, 没想到很多工具都已经失效, Mac 系统的更新更是让编译雪上加霜.

放心, 这篇文章不会又是一篇填坑指南. 我自己填坑的时候忽然想到, 为什么不在 Docker 里编译呢?

使用 Docker 的好处就是环境统一, 另外采用 Linux 系统, 构建起来也更方便, 缺什么就安装什么. 下面就是我用的可以成功编译 OpenJDK9 的 Dockerfile:

FROM ubuntu:18.04

RUN apt update && \
    apt install -y vim gdb file build-essential unzip zip \
    openjdk-8-jdk libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev \
    libcups2-dev libfreetype6-dev libasound2-dev libelf-dev

CMD bash


编译环境有了, 还缺的就是源码了, 有很多下载方式, 我是从这里下载的 http://jdk.java.net/java-se-ri/9.

假设你下载解压后的代码放在 ~/Downloads/openjdk . 现在我们启动 Docker:

docker run -v ~/Downloads/openjdk:/openjdk -it -p 1234:1234 --rm --security-opt seccomp=unconfined jdkbuilder

其中 --security-opt seccomp=unconfined 是为了让后面用 gdb 顺利调试用的. 映射 1234 端口是为了后面远程 Debug.

启动镜像后, 切换到 /openjdk 目录, 执行下面命令:

 bash configure --with-jvm-variants=server \
     --disable-warnings-as-errors \
     --with-debug-level=slowdebug \
     --with-native-debug-symbols=internal

--with-debug-level=slowdebug 和 --with-native-debug-symbols=internal 是为了之后调试用的, --disable-warnings-as-errors 是让编译器忽略 warning, --with-jvm-variants=server 是编译服务器版本的 jvm.

configure 如果顺利执行, 就可以用 make images 来开始编译了, 我 docker 用 6 核 8 G 内存大概要 1 个多小时.

如果一切顺利, 编译好的 jdk 就在 /openjdk/build/.../jdk 下面, 可以用 gdb --args ./java -version 来试着调试. 调试的时候会提示有 received signal SIGSEGV, Segmentation fault. 的错误, 不用管, 直接 continue 就行.

如果觉得用 gdb 不方便, 也可以在 Docker 里启动 gdbserver 后用 CLion 远程 Debug:

# 容器里启动 gdbserver
gdbserver :1234 ./java -version

CLion 里创建一个 GDB Remote Debug 的配置, target 里填入: 127.0.0.1:1234 . Path mappings 里, Remote 填入 /openjdk, Local 里填入你对应的本地地址 /Users/.../Downloads/openjdk .

CLion 支持一整套的远程开发流程了, 包括代码同步, 编译, 部署, 调试. 不过只支持 CMake 的项目, 我就懒得弄了, 反正就是准备简单看看代码调试的, 而且 CLion 我也只是试用版, 就用 GDB 从命令行调试得了.