1.问题
1、如何理解类文件结构布局?
2、如何应用类加载器的工作原理进行将应用辗转腾挪?
3、热部署与热替换有何区别,如何隔离类冲突?
4、JVM如何管理内存,有何内存淘汰机制?
5、JVM执行引擎的工作机制是什么?
6、JVM调优应该遵循什么原则,使用什么工具?
7、JPDA架构是什么,如何应用代码热替换?
8、JVM字节码增强技术有哪些?
2.关键词
类结构,类加载器,加载,链接,初始化,双亲委派,热部署,隔离,堆,栈,方法区,计数器,内存回收,执行引擎,调优工具,JVMTI,JDWP,JDI,热替换,字节码,ASM,CGLIB,DCEVM
3.全文概要(文末有惊喜,PC端阅读代码更佳)
作为三大工业级别语言之一的JAVA如此受企业青睐有加,离不开她背后JVM的默默复出。只是由于JAVA过于成功以至于我们常常忘了JVM平台上还运行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython这样的语言。我们享受着JVM带来跨平台“一次编译到处执行”台的便利和自动内存回收的安逸。本文从JVM的最小元素类的结构出发,介绍类加载器的工作原理和应用场景,思考类加载器存在的意义。进而描述JVM逻辑内存的分布和管理方式,同时列举常用的JVM调优工具和使用方法,最后介绍高级特性JDPA框架和字节码增强技术,实现热替换。从微观到宏观,从静态到动态,从基础到高阶介绍JVM的知识体系。
4.类的装载
4.1类的结构
我们知道不只JAVA文本文件,像Clojure/Groovy/Kotlin/Scala这些文本文件也同样会经过JDK的编译器编程成class文件。进入到JVM领域后,其实就跟JAVA没什么关系了,JVM只认得class文件,那么我们需要先了解class这个黑箱里面包含的是什么东西。
JVM规范严格定义了CLASS文件的格式,有严格的数据结构,下面我们可以观察一个简单CLASS文件包含的字段和数据类型。
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规范说明书里面查阅类文件格式(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),类的整体布局如下图展示的。
JVM核心知识体系
在我的理解,我想把每个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,而且表之间也有关联,常量池则存放着其他表所需要的所有字面量。了解完类的数据结构后,我们需要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。
4.2加载机制
4.2.1类的入口
在我们探究JVM如何使用CLASS文件之前,我们快速回忆一下编写好的C语言文件是如何执行的?我们从C的HelloWorld入手看看先。
#include
int main() {
/* my first program in C */
printf(“Hello, World! n”);
return 0;
}
编辑完保存为hello.c文本文件,然后安装gcc编译器(GNU C/C++)
$ gcc hello.c
$ ./a.out
Hello, World!
这个过程就是gcc编译器将hello.c文本文件编译成机器指令集,然后读取到内存直接在计算机的CPU运行。从操作系统层面看的话,就是一个进程的启动到结束的生命周期。
下面我们看JAVA是怎么运行的。学习JAVA开发的第一件事就是先下载JDK安装包,安装完配置好环境变量,然后写一个名字为helloWorld的类,然后编译执行,我们来观察一下发生了什么事情?
先看源码,有够简单了吧。
package com.zooncool.example.theory.jvm;
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方法的规则,其实我们关心的只有两点:
HelloWorld类是如何被JVM使用的
HelloWorld类里面的main方法是如何被执行的
关于JVM如何使用HelloWorld下文我们会详细讲到。
我们知道JVM是由C/C++语言实现的,那么JVM跟CLASS打交道则需要JNI(Java Native Interface)这座桥梁,当我们在命令行执行java时,由C/C++实现的java应用通过JNI找到了HelloWorld里面符合规范的main方法,然后开始调用。我们来看下java命令的源码就知道了
/*
上一节我们留了一个核心的环节,就是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++实现
类加载路径: /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
实现原理:本地方法由C++实现
Extensions class loader
作用:扩展类加载器,加载JAVA扩展类库。
类加载器:JAVA实现
类加载路径:/jre/lib/ext
System.out.println(System.getProperty(“java.ext.dirs”));
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:
实现原理:扩展类加载器ExtClassLoader本质上也是URLClassLoader
Launcher.java
//构造方法返回扩展类加载器
public Launcher() {
//定义扩展类加载器
Launcher.ExtClassLoader var1;
try {
//1、获取扩展类加载器
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;
//2、获取扩展类加载器实现
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
//3、构造扩展类加载器
instance = createExtClassLoader();
}
}
}
return instance;
}
//4、构造扩展类加载器具体实现
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction
public Launcher.ExtClassLoader run() throws IOException {
//5、获取扩展类加载器加载目标类的目录
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
//7、构造扩展类加载器
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
//6、扩展类加载器目录路径
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] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
//8、扩展类加载器构造方法
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
}
System class loader
作用:系统类加载器,加载应用指定环境变量路径下的类
类加载器:sun.misc.Launcher$AppClassLoader
类加载路径:-classpath下面的所有类
实现原理:系统类加载器AppClassLoader本质上也是URLClassLoader
Launcher.java
//构造方法返回系统类加载器
public Launcher() {
try {
//获取系统类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError(“Could not create application class loader”, var9);
}
}
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
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
//系统类加载器构造方法
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
通过上文运行HelloWorld我们知道JVM系统默认加载的类大改是1560个,如下图
JVM核心知识体系
自定义类加载器
内置类加载器只加载了最少需要的核心JAVA基础类和环境变量下的类,但是我们应用往往需要依赖第三方中间件来完成额外的业务,那么如何把它们的类加载进来就显得格外重要了。幸好JVM提供了自定义类加载器,可以很方便的完成自定义操作,最终目的也是把外部的类文件加载到JVM内存。通过继承ClassLoader类并且复写findClass和loadClass方法就可以达到自定义获取CLASS文件的目的。