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 ),类的整体布局如下图展示的。
在我的理解,我想把每个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,而且表之间也有关联,常量池则存放着其他表所需要的所有字面量。了解完类的数据结构后,我们需要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。
4.2加载机制
4.2.1类的入口
在我们探究JVM如何使用CLASS文件之前,我们快速回忆一下编写好的C语言文件是如何执行的?我们从C的HelloWorld入手看看先。
#include int main() { /* my first program in C */ printf("Hello, World! "); return 0;}
编辑完保存为hello.c文本文件,然后安装gcc编译器(GNU C/C++)
$ gcc hello.c$ ./a.outHello, 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.HelloWorldmy 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命令的源码就知道了
/** 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