JVM核心知识体系

JVM核心知识体系

1.问题

  • 1、如何理解类文件结构布局?
  • 2、如何应用类加载器的工作原理进行将应用辗转腾挪?
  • 3、热部署与热替换有何区别,如何隔离类冲突?
  • 4、JVM如何管理内存,有何内存淘汰机制?
  • 5、JVM执行引擎的工作机制是什么?
  • 6、JVM调优应该遵循什么原则,使用什么工具?
  • 7、JPDA架构是什么,如何应用代码热替换?
  • 8、JVM字节码增强技术有哪些?

2.关键词

类结构,类加载器,加载,链接,初始化,双亲委派,热部署,隔离,堆,栈,方法区,计数器,内存回收,执行引擎,调优工具,JVMTI,JDWP,JDI,热替换,字节码,ASM,CGLIB,DCEVM

3.全文概要

作为三大工业级别语言之一的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规范说明书里面查阅类文件格式,类的整体布局如下图展示的。

java-class-file-internal-structure

在我的理解,我想把每个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,而且表之间也有关联,常量池则存放着其他表所需要的所有字面量。了解完类的数据结构后,我们需要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。

4.2加载机制

4.2.1类的入口

在我们探究JVM如何使用CLASS文件之前,我们快速回忆一下编写好的C语言文件是如何执行的?我们从C的HelloWorld入手看看先。

#include <stdio.h>

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; /** * 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方法的规则,其实我们关心的只有两点:

  • 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", "([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); 
4.2.2类加载器

上一节我们留了一个核心的环节,就是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 { //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<Launcher.ExtClassLoader>() { 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

转载于:https://www.cnblogs.com/TestMa/p/10647630.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值