openjdk使用_深入地理解

2022-09-22 14:38:44 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

目录

  • 前言
  • 一、从JVM源码看类加载器
    • 1.1 Java层面的类加载器
    • 1.2 JVM是如何启动的
    • 1.3 C 层面的类加载器
      • 1.3.1 初始化BootStrapClassLoader
      • 1.3.2 BootStrapClassLoader如何加载类
  • 二、总结

前言

  关于JVM类加载的基础理论知识,请参照《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(上)和《深入理解Java虚拟机》读书笔记(六)–虚拟机类加载机制(下)。

一、从JVM源码看类加载器

  注:使用的是openjdk8

1.1 Java层面的类加载器

  我们都知道在Java类加载中,除了BootStrap加载器,App和Ext加载器都是Java实现的,具体实现在sun.misc.Launcher中:

代码语言:javascript复制
public class Launcher{ 
   
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    ......
    public Launcher() { 
   
        Launcher.ExtClassLoader var1;
        try { 
   
            //没有显示设置父类加载器,为BootStrapClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) { 
   
            throw new InternalError("Could not create extension class loader", var10);
        }

        try { 
   
            //设置ExtClassLoader为父类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) { 
   
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置TCCL
        Thread.currentThread().setContextClassLoader(this.loader);
        ......
    }
    ......
}

  在Launcher类中,有一个静态私有成员变量launcher的赋值,调用本例的构造方法生成一个Launcher实例。因为launcher是一个类级别属性,所以这个操作会被收敛到类构造器<clinit>()方法,在该类被加载的初始化阶段被执行。

  在Launcher的构造方法中,分别初始化了Launcher.ExtClassLoader和Launcher.AppClassLoader加载器,将Launcher.loader属性(ClassLoader.getSystemClassLoader方法返回的就是这个属性)设置为了AppClassLoader,并且将TCCL设置为了AppClassLoader。ExtClassLoader和AppClassLoader初始化如下:

代码语言:javascript复制
		//ExtClassLoader
		static class ExtClassLoader extends URLClassLoader { 
   
        private static volatile Launcher.ExtClassLoader instance;

        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { 
   
            if (instance == null) { 
   
                Class var0 = Launcher.ExtClassLoader.class;
                synchronized(Launcher.ExtClassLoader.class) { 
   
                    if (instance == null) { 
   
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }
}

		//AppClassLoader
		static class AppClassLoader extends URLClassLoader { 
   
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { 
   
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { 
   
                public Launcher.AppClassLoader run() { 
   
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
}

  从初始化方法就可以看出,ExtClassLoader没有显示设置父加载器,所以其父类加载器是BootStrap,AppClassLoader设置ExtClassLoader为自己的父加载器。

注:加载器的父子关系不是继承上的父子关系,而是通过成员变量引用,以组合的方式实现的父子关系

1.2 JVM是如何启动的

  程序的主要入口点在main.c,代码中有大量的条件编译,我们直接看JLI_Launch函数:

代码语言:javascript复制
return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), const_jargs,
                   sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
                   FULL_VERSION,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
                   const_cpwildcard, const_javaw, const_ergo_class);

  JLI_Launch函数定义在java.h中,java.c中有该函数的实现,其中会调用LoadJavaVM函数,LoadJavaVM函数对于不同的平台(win、mac、solaris等)有不同的实现,我们这里看看windows的版本,实现在java_md.c中,主要是从jvmpath加载dll,并且初始化调用函数:

代码语言:javascript复制
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{ 
   
	//加载Microsoft环境c运行时库,提供必要的函数库调用和启动函数
	//后面创建线程启动JVM就使用的c运行时库函数_beginthreadex
    LoadMSVCRT();
    /* 根据jvmpath加载dll文件 */
    if ((handle = LoadLibrary(jvmpath)) == 0) { 
   
        JLI_ReportErrorMessage(DLL_ERROR4, (char *)jvmpath);
        return JNI_FALSE;
    }
    /* Now get the function addresses */
    ifn->CreateJavaVM =
        (void *)GetProcAddress(handle, "JNI_CreateJavaVM");
    ifn->GetDefaultJavaVMInitArgs =
        (void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");
    if (ifn->CreateJavaVM == 0 || ifn->GetDefaultJavaVMInitArgs == 0) { 
   
        JLI_ReportErrorMessage(JNI_ERROR1, (char *)jvmpath);
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

  加载Microsoft环境c运行时库后,会根据jvmpath加载jvm的dll文件(在jre目录存有动态链接文件,若将jrebinserver下的jvm.dll移除,也启动不了JVM)。InvocationFunctions定义在java.h中,有三个JNI函数:

代码语言:javascript复制
typedef struct { 
   
    CreateJavaVM_t CreateJavaVM;
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
    GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;

  关于jvmpath,需要再回到JLI_Launch函数中,在该函数中会调用CreateExecutionEnvironment函数创建执行上下文,在其中会初始化jvmpath:

代码语言:javascript复制
CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg,  sizeof(jvmcfg));

  同样的在java_md.c中找到该函数win平台的实现:

代码语言:javascript复制
void CreateExecutionEnvironment(int *pargc, char ***pargv,
                           char *jrepath, jint so_jrepath,
                           char *jvmpath, jint so_jvmpath,
                           char *jvmcfg,  jint so_jvmcfg) { 
   
	......
	/* 寻找要使用的JRE路径*/
    if (!GetJREPath(jrepath, so_jrepath)) { 
   
        JLI_ReportErrorMessage(JRE_ERROR1);
        exit(2);
    }
    /*获取jvm类型*/
    jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE);
	jvmpath[0] = '';
	/*根据JRE路径和jvm类型获取JVMPath*/
	if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath)) { 
   
        JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);
        exit(4);
    }
	......
}

  我们可以使用XXaltjvm参数传递JVM类型,在CheckJvmType函数中会完成检查工作。回到LoadJavaVM函数中,会根据jvmpath加载dll文件,然后初始化JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs函数地址。   回到JLI_Launch函数中,除了LoadJavaVM之外,还会选择JRE版本、解析参数、设置classpath等等。准备工作做完之后,进入JVMInit函数执行JVM初始化流程,同样进入win的实现,在java_md.c中:

代码语言:javascript复制
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);
}

  然后进入ContinueInNewThread函数,实现在java.c:

代码语言:javascript复制
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                    int argc, char **argv,
                    int mode, char *what, int ret)
{ 
   
	......
    { 
    /* 创建一个新的线程去创建JVM并且调用main方法*/
      JavaMainArgs args;
      int rslt;

      args.argc = argc;
      args.argv = argv;
      args.mode = mode;
      args.what = what;
      args.ifn = *ifn;
      rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
  	  ......
    }
}

  记住这里ContinueInNewThread0函数传的第一个参数是JavaMain。然后我们进入win下的实现,在java_md.c中看看ContinueInNewThread0函数的实现:

代码语言:javascript复制
/* * Block current thread and continue execution in a new thread */
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { 
   
    int rslt = 0;
    unsigned thread_id;

#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
#define STACK_SIZE_PARAM_IS_A_RESERVATION (0x10000)
#endif

    /* * STACK_SIZE_PARAM_IS_A_RESERVATION is what we want, but it's not * supported on older version of Windows. Try first with the flag; and * if that fails try again without the flag. See MSDN document or HotSpot * source (os_win32.cpp) for details. */
    HANDLE thread_handle =
      (HANDLE)_beginthreadex(NULL,
                             (unsigned)stack_size,
                             continuation,
                             args,
                             STACK_SIZE_PARAM_IS_A_RESERVATION,
                             &thread_id);
    if (thread_handle == NULL) { 
   
      thread_handle =
      (HANDLE)_beginthreadex(NULL,
                             (unsigned)stack_size,
                             continuation,
                             args,
                             0,
                             &thread_id);
     ......

  _beginthreadex是一个c运行时库函数,其中:

  • arg1:安全属性,NULL为默认安全属性
  • arg2:线程堆栈大小,如果为0,则线程堆栈大小和创建它的线程的相同,在JVM参数- 中可以使用-Xss参数影响,该参数的解析在java.c的AddOption函数中,还包括-Xmx、-Xms的解析,这里就不贴代码了
  • arg3:线程调用的函数地址(函数名称),在ContinueInNewThread函数中传入的是JavaMain,所以最终执行的是JavaMain函数
  • arg4:传递给线程的参数指针
  • arg5:线程初始状态
  • arg6:记录threadId地址

  关于STACK_SIZE_PARAM_IS_A_RESERVATION,在os_windows.cpp中能找到说明:

Windows XP added a new flag ‘STACK_SIZE_PARAM_IS_A_RESERVATION’ for CreateThread() that can treat ‘stack_size’ as stack size. However we are not supposed to call CreateThread() directly according to MSDN document because JVM uses C runtime library. The good news is that the flag appears to work with _beginthredex() as well.

  我们只需要关心线程调用的函数:JavaMain。该函数实现在java.c中,该函数内容不少,主要是初始化JVM,加载MainClass,调用函数入口方法(main方法)等,下面是部分代码:

代码语言:javascript复制
int JNICALL JavaMain(void * _args)
{ 
   
	......
    RegisterThread();

    /* 初始化虚拟机*/
    start = CounterGet();
    if (!InitializeJVM(&vm, &env, &ifn)) { 
   
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    //加载主类
    mainClass = LoadMainClass(env, mode, what);
    //获取main方法
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");
    CHECK_EXCEPTION_NULL_LEAVE(mainID);
    /* 调用main方法 */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    ......
}

  我们进入InitializeJVM函数简单看看初始化虚拟机的逻辑。InitializeJVM方法同样实现在java.c文件中:

代码语言:javascript复制
static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{ 
   
    JavaVMInitArgs args;
  	......
    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}

  其中调用的CreateJavaVM函数,在前面已经初始化,使用JNI来调用JNI_CreateJavaVM(jvm.dll)。

代码语言:javascript复制
    [DllImport("jvm.dll")]      public unsafe static extern int  JNI_CreateJavaVM(void** ppVm, void** ppEnv, void* pArgs);

1.3 C 层面的类加载器

  前面我们简单介绍了jvm的启动过程,进入jni.cpp中,调用Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);函数创建jvm,对于can_try_again参数,代码中给出了注释说明:

Certain errors during initialization are recoverable and do not prevent this method from being called again at a later time (perhaps with different arguments). However, at a certain point during initialization if an error occurs we cannot allow this function to be called again (or it will crash). In those situations, the ‘canTryAgain’ flag is set to false, which atomically sets safe_to_recreate_vm to 1, such that any new call to JNI_CreateJavaVM will immediately fail using the above logic.

  我们直接进入Threads::create_vm函数中,实现在thread.cpp,该函数内容很多,会初始化很多东西,这里只关注类加载器的部分,这部分逻辑在init_globals函数中:

代码语言:javascript复制
// Initialize global modules
  jint status = init_globals();

  init_globals的工作是初始化一些公共模块,其中包括类加载器,该函数在init.cpp中:

代码语言:javascript复制
jint init_globals() { 
   
  HandleMark hm;
  management_init();
  bytecodes_init();
  classLoader_init();//初始化类加载器
  codeCache_init();
  VM_Version_init();
  os_init_globals();
  stubRoutines_init1();
......

  初始化类加载器的函数是classLoader_init,该函数在classLoader.cpp:

代码语言:javascript复制
void classLoader_init() { 
   
  ClassLoader::initialize();
}

1.3.1 初始化BootStrapClassLoader

  同样在classLoader.cpp中找到initialize()函数的实现,贴出部分代码:

代码语言:javascript复制
void ClassLoader::initialize() { 
   
  //lookup zip library entry points
  load_zip_library();
  // initialize search path
  setup_bootstrap_search_path();
  if (LazyBootClassLoader) { 
   
  	// set up meta index which makes boot classpath initialization lazier
    setup_meta_index();
  }
}

  我们暂时不管zib library,直接看setup_bootstrap_search_path函数是如何搜索BootStrap加载器的加载目录的:

代码语言:javascript复制
void ClassLoader::setup_bootstrap_search_path() { 
   
  //获取sysclasspath,然后使用strdup函数拷贝一份
  char* sys_class_path = os::strdup(Arguments::get_sysclasspath());
  
  int len = (int)strlen(sys_class_path);
  int end = 0;

  // Iterate over class path entries
  for (int start = 0; start < len; start = end) { 
   
    while (sys_class_path[end] && sys_class_path[end] != os::path_separator()[0]) { 
   
      end  ;
    }
    char* path = NEW_C_HEAP_ARRAY(char, end-start 1, mtClass);
    strncpy(path, &sys_class_path[start], end-start);
    path[end-start] = '';
    //添加path
    update_class_path_entry_list(path, false);
    FREE_C_HEAP_ARRAY(char, path, mtClass);
    while (sys_class_path[end] == os::path_separator()[0]) { 
   
      end  ;
    }
  }
}

  该函数中,有两处语句相对比较核心,分别是:

代码语言:javascript复制
char* sys_class_path = os::strdup(Arguments::get_sysclasspath());

代码语言:javascript复制
update_class_path_entry_list(path, false);
  • get_sysclasspath函数

  我们首先来看get_sysclasspath,看名字就知道是获取classpath,strdup是一个字符串拷贝函数,不用理会它。我们进入artuments.hpp中看看aget_sysclasspath函数是如何实现的:

代码语言:javascript复制
static char *get_sysclasspath() { 
    return _sun_boot_class_path->value(); }

  可以看到获取的是_sun_boot_class_path的值,而_sun_boot_class_path是通过函数set_sysclasspath设置的:

代码语言:javascript复制
  static void set_sysclasspath(char *value) { 
    _sun_boot_class_path->set_value(value); }

   那么是在哪里设置的呢?我们能在os.cpp中找到答案:

代码语言:javascript复制
static const char classpath_format[] =
      "%/lib/resources.jar:"
      "%/lib/rt.jar:"
      "%/lib/sunrsasign.jar:"
      "%/lib/jsse.jar:"
      "%/lib/jce.jar:"
      "%/lib/charsets.jar:"
      "%/lib/jfr.jar:"
      "%/classes";
  //格式化
  char* sysclasspath = format_boot_path(classpath_format, home, home_len, fileSep, pathSep);
  if (sysclasspath == NULL) return false;
  //设置sysclasspath
  Arguments::set_sysclasspath(sysclasspath);
  • update_class_path_entry_list函数

  现在找到了classpath,需要再看看是如何存储的,update_class_path_entry_list函数的实现还是在classLoader.cpp中:

代码语言:javascript复制
void ClassLoader::update_class_path_entry_list(char *path,
                                               bool check_for_duplicates) { 
   
  struct stat st;
  if (os::stat(path, &st) == 0) { 
   
    // File or directory found
    ClassPathEntry* new_entry = NULL;
    Thread* THREAD = Thread::current();
    //以ClassPathEntry表示
    new_entry = create_class_path_entry(path, &st, LazyBootClassLoader, CHECK);
    // Add new entry to linked list
    if (!check_for_duplicates || !contains_entry(new_entry)) { 
   
      //添加到链表
      add_to_list(new_entry);
    }
  }
}
//添加一个ClassPathEntry到链表中
void ClassLoader::add_to_list(ClassPathEntry *new_entry) { 
   
  if (new_entry != NULL) { 
   
    if (_last_entry == NULL) { 
   
      _first_entry = _last_entry = new_entry;
    } else { 
   
      _last_entry->set_next(new_entry);
      _last_entry = new_entry;
    }
  }
}

  从代码中可以看到,每个定义的目录都是以ClassPathEntry为表现形式,以链表结构存储:

代码语言:javascript复制
ClassPathEntry* ClassLoader::_first_entry  = NULL;
ClassPathEntry* ClassLoader::_last_entry   = NULL;

  再回到ClassLoader::initialize()函数,关于LazyBootClassLoader,是一个懒加载配置参数,在globals.hpp中配置为true:

代码语言:javascript复制
product(bool, LazyBootClassLoader, true,"Enable/disable lazy opening of boot class path entries") 

1.3.2 BootStrapClassLoader如何加载类

  这里我们直接看ClassLoader::load_classfile(Symbol* h_name, TRAPS)函数就可以了:

代码语言:javascript复制
instanceKlassHandle ClassLoader::load_classfile(Symbol* h_name, TRAPS) { 
   
  ResourceMark rm(THREAD);
  EventMark m("loading class %s", h_name->as_C_string());
  ThreadProfilerMark tpm(ThreadProfilerMark::classLoaderRegion);

  stringStream st;
  // st.print() uses too much stack space while handling a StackOverflowError
  // st.print("%s.class", h_name->as_utf8());
  st.print_raw(h_name->as_utf8());
  st.print_raw(".class");
  char* name = st.as_string();

  // Lookup stream for parsing .class file
  ClassFileStream* stream = NULL;
  int classpath_index = 0;
  { 
   
    PerfClassTraceTime vmtimer(perf_sys_class_lookup_time(),
                               ((JavaThread*) THREAD)->get_thread_stat()->perf_timers_addr(),
                               PerfClassTraceTime::CLASS_LOAD);
    //BootStralClassLoader加载的包是存在_first_entry中的
    //这个是一个链表结构
    ClassPathEntry* e = _first_entry;
    while (e != NULL) { 
   
      stream = e->open_stream(name, CHECK_NULL);
      if (stream != NULL) { 
   
      	//找到了文件
        break;
      }
      e = e->next();
        classpath_index;
    }
  }
  instanceKlassHandle h;
  if (stream != NULL) { 
   
    //找到了类文件,这里做解析操作
    ClassFileParser parser(stream);
    ClassLoaderData* loader_data = ClassLoaderData::the_null_class_loader_data();
    Handle protection_domain;
    TempNewSymbol parsed_name = NULL;
    //解析class文件
    instanceKlassHandle result = parser.parseClassFile(h_name,
                                                       loader_data,
                                                       protection_domain,
                                                       parsed_name,
                                                       false,
                                                       CHECK_(h));

    // add to package table
    if (add_package(name, classpath_index, THREAD)) { 
   
      h = result;
    }
  }
  return h;
}

  逻辑很简单,代码中加了几个注释,这里就不赘述了~   parseClassFile函数定义在classFileParser.cpp中:

代码语言:javascript复制
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    Handle protection_domain,
                                                    KlassHandle host_klass,
                                                    GrowableArray<Handle>* cp_patches,
                                                    TempNewSymbol& parsed_name,
                                                    bool verify,
                                                    TRAPS) { 
   ......}

  ClassFileParser::parseClassFile函数内容非常多,期间会解析字节码文件,检查魔数、版本号,解析常量池、字段表等等等等,在解析的过程中,解析之后都会对字节码的有效性做检查,比如是否继承了final类等等,代码很长,这里就不分析了。   在所有的解析和检查操作做完之后,就可以通过函数InstanceKlass::allocate_instance_klass在方法区创建Klass*了,这里涉及了二分模型(oop-klass),不是一两句话能说清楚的,先不管。然后会通过函数java_lang_Class::create_mirror初始化静态字段,填充oop_maps等等。

二、总结

  根据我们前面的源码分析,可以看到JVM源码层面类加载的主要逻辑在classLoader文件中实现了系统类的加载。   本文的最后,将前面的源码分析总结一个简单的走向图:

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169720.html原文链接:https://javaforall.cn

0 人点赞