类结构,类加载器,加载,连接,初始化,双亲委派,热部署,隔离,堆,栈,方法区,计数器,内存回收,执行引擎,调优工具,JVMTI,JDWP,JDI,热替换,字节码,ASM,CGLIB,DCEVMhtml
做为三大工业级别语言之一的JAVA如此受企业青睐有加,离不开她背后JVM的默默复出。只是因为JAVA过于成功以致于咱们经常忘了JVM平台上还运行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython这样的语言。咱们享受着JVM带来跨平台“一次编译处处执行”台的便利和自动内存回收的安逸。本文从JVM的最小元素类的结构出发,介绍类加载器的工做原理和应用场景,思考类加载器存在的意义。进而描述JVM逻辑内存的分布和管理方式,同时列举经常使用的JVM调优工具和使用方法,最后介绍高级特性JDPA框架和字节码加强技术,实现热替换。从微观到宏观,从静态到动态,从基础到高阶介绍JVM的知识体系。java
咱们知道不仅JAVA文本文件,像Clojure/Groovy/Kotlin/Scala这些文本文件也一样会通过JDK的编译器编程成class文件。进入到JVM领域后,其实就跟JAVA没什么关系了,JVM只认得class文件,那么咱们须要先了解class这个黑箱里面包含的是什么东西。git
JVM规范严格定义了CLASS文件的格式,有严格的数据结构,下面咱们能够观察一个简单CLASS文件包含的字段和数据类型。github
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
详细的描述咱们能够从JVM规范说明书里面查阅类文件格式,类的总体布局以下图展现的。web
在个人理解,我想把每一个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,并且表之间也有关联,常量池则存放着其余表所须要的全部字面量。了解完类的数据结构后,咱们须要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。算法
在咱们探究JVM如何使用CLASS文件以前,咱们快速回忆一下编写好的C语言文件是如何执行的?咱们从C的HelloWorld入手看看先。spring
#include <stdio.h> int main() { /* my first program in C */ printf("Hello, World! \n"); return 0; }
编辑完保存为hello.c文本文件,而后安装gcc编译器(GNU C/C++)shell
$ gcc hello.c $ ./a.out Hello, World!
这个过程就是gcc编译器将hello.c文本文件编译成机器指令集,而后读取到内存直接在计算机的CPU运行。从操做系统层面看的话,就是一个进程的启动到结束的生命周期。数据库
下面咱们看JAVA是怎么运行的。学习JAVA开发的第一件事就是先下载JDK安装包,安装完配置好环境变量,而后写一个名字为helloWorld的类,而后编译执行,咱们来观察一下发生了什么事情?apache
先看源码,有够简单了吧。
package com.zooncool.example.theory.jvm; /** * Created with IntelliJ IDEA. User: linzhenhua Date: 2019/1/3 Time: 11:56 PM * @author linzhenhua */ public class HelloWorld { public static void main(String[] args) { System.out.println("my classLoader is " + HelloWorld.class.getClassLoader()); } }
编译执行
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld my classLoader is sun.misc.Launcher$AppClassLoader@2a139a55
对比C语言在命令行直接运行编译后的a.out二进制文件,JAVA的则是在命令行执行java classFile,从命令的区别咱们知道操做系统启动的是java进程,而HelloWorld类只是命令行的入参,在操做系统来看java也就是一个普通的应用进程而已,而这个进程就是JVM的执行形态(JVM静态就是硬盘里JDK包下的二进制文件集合)。
学习过JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜执行java命令时JVM对该入口方法作了惟一验证,经过了才容许启动JVM进程,下面咱们来看这个入口方法有啥特色。
去掉public限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application
说名入口方法须要被public修饰,固然JVM调用main方法是底层的JNI方法调用不受修饰符影响。
去掉static限定
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: main 方法不是类 com.zooncool.example.theory.jvm.HelloWorld 中的static, 请将 main 方法定义为: public static void main(String[] args)
咱们是从类对象调用而不是类建立的对象才调用,索引须要静态修饰
返回类型改成int
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: main 方法必须返回类 com.zooncool.example.theory.jvm.HelloWorld 中的空类型值, 请 将 main 方法定义为: public static void main(String[] args)
void返回类型让JVM调用后无需关心调用者的使用状况,执行完就中止,简化JVM的设计。
方法签名改成main1
$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application
这个我也不清楚,多是约定俗成吧,毕竟C/C++也是用main方法的。
说了这么多main方法的规则,其实咱们关心的只有两点:
关于JVM如何使用HelloWorld下文咱们会详细讲到。
咱们知道JVM是由C/C++语言实现的,那么JVM跟CLASS打交道则须要JNI(Java Native Interface)这座桥梁,当咱们在命令行执行java时,由C/C++实现的java应用经过JNI找到了HelloWorld里面符合规范的main方法,而后开始调用。咱们来看下java命令的源码就知道了
/* * Get the application's main class. */ if (jarfile != 0) { mainClassName = GetMainClassName(env, jarfile); ... ... mainClass = LoadClass(env, classname); if(mainClass == NULL) { /* exception occured */ ... ... /* Get the application's main method */ mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); ... ... {/* Make sure the main method is public */ jint mods; jmethodID mid; jobject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE); ... ... /* Build argument array */ mainArgs = NewPlatformStringArray(env, argv, argc); if (mainArgs == NULL) { ReportExceptionDescription(env); goto leave; } /* Invoke main method. */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
上一节咱们留了一个核心的环节,就是JVM在执行类的入口以前,首先得找到类再而后再把类装到JVM实例里面,也便是JVM进程维护的内存区域内。咱们固然知道是一个叫作类加载器的工具把类加载到JVM实例里面,抛开细节从操做系统层面观察,那么就是JVM实例在运行过程当中经过IO从硬盘或者网络读取CLASS二进制文件,而后在JVM管辖的内存区域存放对应的文件。咱们目前还不知道类加载器的实现,可是咱们从功能上判断无非就是读取文件到内存,这个是很普通也很简单的操做。
若是类加载器是C/C++实现的话,那么大概就是以下代码就能够实现
char *fgets( char *buf, int n, FILE *fp );
若是是JAVA实现,那么也很简单
InputStream f = new FileInputStream("theory/jvm/HelloWorld.class");
从操做系统层面看的话,若是只是加载,以上代码就足以把类文件加载到JVM内存里面了。可是结果就是乱糟糟的把一堆毫无秩序的类文件往内存里面扔,没有良好的管理也无法用,因此须要咱们须要设计一套规则来管理存放内存里面的CLASS文件,咱们称为类加载的设计模式或者类加载机制,这个下文会重点解释。
根据官网的定义A class loader is an object that is responsible for loading classes. 类加载器就是负责加载类的。咱们知道启动JVM的时候会把JRE默认的一些类加载到内存,这部分类使用的加载器是JVM默认内置的由C/C++实现的,好比咱们上文加载的HelloWorld.class。可是内置的类加载器有明确的范围限定,也就是只能加载指定路径下的jar包(类文件的集合)。若是只是加载JRE的类,那可玩的花样就少不少,JRE只是提供了底层所需的类,更多的业务须要咱们从外部加载类来支持,因此咱们须要指定新的规则,以方便咱们加载外部路径的类文件。
Bootstrap class loader
做用:启动类加载器,加载JDK核心类
类加载器:C/C++实现
类加载路径: <java_home>/jre/lib
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasig.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes
实现原理:本地方法由C++实现
Extensions class loader
做用:扩展类加载器,加载JAVA扩展类库。
类加载器:JAVA实现
类加载路径:<java_home>/jre/lib/ext
System.out.println(System.getProperty("java.ext.dirs")); /Users/linzhenhua/Library/Java/Extensions: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext: /Library/Java/Extensions: /Network/Library/Java/Extensions: /System/Library/Java/Extensions: /usr/lib/java
实现原理:扩展类加载器ExtClassLoader本质上也是URLClassLoader
Launcher.java
//构造方法返回扩展类加载器 public Launcher() { //定义扩展类加载器 Launcher.ExtClassLoader var1; try { //一、获取扩展类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } ... } //扩展类加载器 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; } //四、构造扩展类加载器具体实现 private static Launcher.ExtClassLoader createExtClassLoader() throws IOException { try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { public Launcher.ExtClassLoader run() throws IOException { //五、获取扩展类加载器加载目标类的目录 File[] var1 = Launcher.ExtClassLoader.getExtDirs(); int var2 = var1.length; for(int var3 = 0; var3 < var2; ++var3) { MetaIndex.registerDirectory(var1[var3]); } //七、构造扩展类加载器 return new Launcher.ExtClassLoader(var1); } }); } catch (PrivilegedActionException var1) { throw (IOException)var1.getException(); } } //六、扩展类加载器目录路径 private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4