Java-第十四部分-JVM-JVM简述和类加载器子系统

417 阅读14分钟

JVM全文

JVM简述

  • JVM,Java Virtual Machine,Java虚拟机,一台执行java字节码的虚拟计算机,拥有独立的运行机制
  • Java相比于C++,自动管理内存,实现内存动态分配和垃圾收集技术
  • write one, run anywhere
  • 跨语言的平台,可以运行其他语言的字节码文件,遵守jvm规范;只关心字节码文件
  • 类比 自变量(字节码文件),通过一个函数(jvm),生成结果(产生的代码效果) image.png
  • 虚拟机,虚拟的计算机。系统虚拟机,对物理计算机的仿真;程序虚拟机,专门为执行单个计算机程序而设计的
  • jvm字节码,只要遵循jvm规范,就可以在jvm上运行
  • JVM的位置,运行在操作系统上,与硬件没有直接的交互 image.png
  • 整体结构,执行引擎,充当了高级语言翻译成机器语言的翻译者 image.png
  • Java代码执行流程 image.png
  • java反编译,找到out目录下的.class文件,javap -v StackStruTest.class
  1. javap -v StackStruTest.class > xxx.txt 输出到txt中

架构模型

  • Java编译器输入指令流基于栈的指令集架构
  • 基于栈的指令集架构,性能比寄存器差
设计和实现更简单,适用于资源受限的系统
避开了寄存器的分配难题,使用零地址指令方式分配。零地址指令,没有地址,只有操作数
指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器更好实现
不需要硬件支持,可以执行更好
  • 基于寄存器的指令集架构
典型的应用是x86的二进制指令集,传统pc和Android的Davlik虚拟机
完全依赖硬件,可移植性差
性能优秀,执行高效
花更少的指令去完成一项操作
往往以一地址指令,二地址指令和三地址指令为主

生命周期

  • 启动,通过引导类bootstrp class loader创建一个初始类initial class来完成的,这个类由虚拟机的具体实现指定的
  • 执行,真正执行的是一个在java虚拟机上的进程。jps查看执行情况
  • 退出,程序正常结束;执行过程中遇到异常或错误而异常终止;由于操作系统出现错误而导致java虚拟机进程中指;某线程调用runtime类或system类的exit方法,或runtime类的halt方法,并且java安全管理器也允许这次exithalt操作
  • JVM 的生命周期是和 Java 程序的运行一样,当程序运行结束,JVM实例也就跟着消失了

虚拟机介绍

  • JIT编译器,just-in-time compilation,寻找热点代码(多次重复的代码),翻译成机器指令存储到缓存区,如果全部都用JIT,会导致暂停的时间过长,类比公交车,坐一会等一辆
  • 解释器,逐行解释字节码,翻译成机器指令,响应速度较快,类比步行,一直走
  • sun classic vm 第一款商用虚拟机,hotspot内置了这款虚拟机。内部只提供了解释器,效率比较低下,逐行解释执行
  • exact vm,exact memory management,准确式内存管理,虚拟机可以知道内存中某个位置的数据具体是什么类型的;具备了现代高性能虚拟机的雏形,热点探测,编译器与解释器的混合工作模式
  • hotspot vm,只有这个虚拟机有方法区的概念,热点代码探测技术。通过计数器找到最具编译价值代码,触发即时编译(在本地缓存)或栈上替换(对象分配在栈上);通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡
  • BEA jrockit,针对服务器端应用,不太关注程序启动速度,不包含解释器实现,全部代码都靠即时编译器编译后执行。
  • IBM j9,IBM Technology for Java Virtual Machine,IT4J
  • CDC/CLDC hotspot移动端;KVM,简单、轻量、高度可移植,智能控制器、传感器
  • Azul vm 与特定硬件平台绑定、软硬件配合的专有虚拟机。每个实例都可以管理至少数十个cpu和数百GB内存的硬件资源,提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性
  • Liquid vm,jrockit的虚拟化版本,不需要操作系统的支持,本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持
  • Apache Harmony / Microsoft jvm
  • TaobaoJVM,基于hotspot vm,深度定制且开源。创新的GCIH(GC invisible heap)技术实现了off-heap,将生命周期较长的java对象从heap(堆空间)移到heap之外,降低GC的回收频率,提升回收效率;在多个java虚拟机进程中实现共享;crc32(校验)指令实现jvm intrinsic(java内置方法),降低了jni(java本地书写接口)的调用开销
  • Dalvik vm,5.0之前安卓开发的虚拟机,没有遵循java虚拟机规范,基于寄存器架构模型,字节码文件为dex文件;5.0后为art vm,支持提前编译
  • Graal vm,run programs faster anywhere;支持不同语言混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件;工作原理,将这些语言的源代码或原代码编译后的中间格式,通过编译器转换为能被graal vm接受的中间表示,提供truffle工具快速构建面向一种新语言的解释器,运行时还能进行即时编译优化

面试

  • 为什么 Java 研发系统需要 JVM

因为 Java 是一门抽象的语言,并且有自动内存管理机制。而操作系统无法去进行自动垃圾回收等操作,所以就有了虚拟机。虚拟机可以对字节码加载、自动垃圾回收、并发等。而 JVM 只是一个规范,定义了.class文件的结构、加载机制、数据存储、运行时栈等诸多内容,最常用的 JVM 实现就是 Hotspot

类加载子系统

  • 整体过程
  1. 加载class文件
  2. 验证文件格式,验证跟加载交替进行
  3. 加载class文件本身进方法区,生成Class对象,作为方法区这个类的访问入口
  4. 验证元数据,字节码
  5. 准备,零值初始化
  6. 解析,符号引用验证,可能在初始化之后进行
  • java执行整体结构 image.png
  • 类加载器子系统负责从文件系统或者网络中加载class文件,class文件开头有特定的文件标识;classLoader只负责class文件的加载,由Execution Engine决定是否可以运行;加载的类信息存放于方法区的内存空间,方法区中除了类信息,还存放运行时常量池信息,还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)
  • 具体流程
  1. class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,最终这个模板在执行的时候要加载到jvm中,根据这个文件实例化出n个一模一样的实例
  2. class file 加载到jvm中,被称为dna元数据模板,放在方法区
  3. .class -> JVM -> 元数据模板,此过程由类加载器 class loader 负责 image.png

类加载过程

image.png

  • 加载,通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构;在内存(堆中)中生成一个代表这个类的java.lang.Class对象(反射),作为方法区这个类的各种数据的访问入口
  • 加载.class文件的来源,本地系统;网络获取,web applet;zip压缩包,jar/war;运行时计算生成,动态代理技术(Proxy);其他文件生成,jsp;专有数据库提取;加密文件提取,防class文件被反编译的保护措施
  • 链接
  1. 验证,每个class文件开头都是CA FE BA BE,确保class文件的字节流中包含信息符合虚拟机规范,保证加载类的正确性;文件格式验证,元数据验证,字节码验证,符号引用验证
  2. 准备,为类变量(static修饰)分配内存并且设置该类变量的默认初始值(零值),隐式赋值;这里不包含final修饰的static,因为finanl在前端编译的时候就会分配,准备阶段会显示初始化;不会为实例变量分配初始化,类变量分配在方法区,而实例变量会随着对象一起分配到java堆
  3. 解析,将常量池内的符号引用转换为直接引用的过程,伴随jvm在执行完初始化后在执行;符号引用是一组符号来描述所引用的目标,直接引用直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等
  4. 虚拟机在class文件时才会进行动态链接,class文件中不会保存各个方法和字段的最终内存布局信息,这些字段和方法的符号引用不经过转换无法直接被虚拟机使用;当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段,将其替换成直接引用,翻译到具体的内存地址中
  • 初始化,执行类构造器方法<clinit>(),是javac编译器自动收集类中的所有类变量(static修饰)赋值动作静态代码块中的语句合并生成的(显式赋值),如果没有前面的操作(没有静态static修饰)就不会生成;构造器方法中的指令按语句在源文件中出现的顺序执行;不同类的构造器<init>();若该类具有父类,会保证父类的初始化执行完毕;虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁,只需要被加载一次
//打印的结果为1,链接过程中先赋零值;初始化过程中,顺序执行,先赋0,再赋1
static {
    number = 0;
    //会报错 Illegal forward reference,非法的前向引用,因为是在后面声明的
    System.out.println(number);
}
private static int number = 1;

类加载器

  • 引导类加载器 BootStrap ClassLoader,C/C++实现,嵌套在jvm内部,用来加载java的核心库,包名为java/javax/sun...;并不继承自ClassLoader;加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • 自定义类加载器 User-Defined ClassLoader,所有派生于抽象类ClassLoader的类加载器,java实现
  1. 扩展类加载器,Extension Classloader,父类为启动类加载器,从java.ext.dirs系统属性所指定的目录加载类库,或从JDK安装目录的jre/lib/ext子目录下加载类库,用户创建的jar放在此目录下,也会由扩展类加载器加载
  2. 应用程序类加载器,AppClassLoader,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;程序中默认的类加载器
  • 包含关系,不是继承关系 image.png
  • java-8,获取加载器。sun.misc.Launcher是java虚拟机的入口应用
// 系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取上层,扩展类加载器 sun.misc.Launcher$ExtClassLoader@12a3a380
ClassLoader platformClassLoader = systemClassLoader.getParent();
System.out.println(platformClassLoader);
// null 获取不到引导类加载器
ClassLoader parent = platformClassLoader.getParent();
System.out.println(parent);
// 获取用户自定义类的类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);
// null 获取不到引导类加载器 -> java的核心类库都是使用引导类加载器进行加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);
  • 加载器路径
//获取引导类能加载的路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
    System.out.println(urL);
}
// /Users/apple/Library/Java/JavaVirtualMachines/corretto-1.8.0_302/Contents/Home/jre/lib/jsse.jar
// 解压,查询到 Provider.class
// null 为引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);

//扩展类加载器
String extDirs = System.getProperty("java.ext.dirs");
for (String s : extDirs.split(":")) {
    System.out.println(s);
}
///Users/apple/Library/Java/JavaVirtualMachines/corretto-1.8.0_302/Contents/Home/jre/lib/ext
//SunEC sun.misc.Launcher$ExtClassLoader@61bbe9ba
ClassLoader extLoader = SunEC.class.getClassLoader();
System.out.println(extLoader);
  • 获取加载器的几种方式
//引导类
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
//当前线程获取上下文,系统类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//扩展类加载器
ClassLoader parent = systemClassLoader.getParent();

自定义加载器

隔离加载类,避免中间键类名冲突,用不同的类加载器,加载进不同的环境; 修改类加载的方式,动态加载; 扩展加载源,本地/网络/机顶盒; 防止源码泄漏,加密字节文件,自定义加载器进行解密

  • 继承ClassLoader,重写findClass()方法,以字节数组的形势加载进内存
  • 没有复杂的需求,直接继承URLClassLoader

双亲委派机制

  • java虚拟机对class文件采用按需加载的方式,当需要使用该类时才会将它的class文件加载进内存,生成class对象,而加载某个class文件时,采用双亲委派模式,即把请求交由父类处理
  • 工作原理
  1. 如果一个类加载器收到了类加载请求,不会自己加载,而是将这个请求委托给父类的加载器去执行
  2. 如果父类加载器的父类还存在父类记载器,则进一步向上委托,请求最终将到达顶层的启动类加载器
  3. 如果父类加载器可以完成类加载,成功返回;如果无法完成此加载任务,由子加载器自己尝试加载
  • 如果加载核心类库中的类String,会一层层返回至引导类加载器,由引导类加载器进行加载
  • 如果加载自定义类,会先递归找到引导类加载器,之后如果不能加载,一层层向下,最后再次回到由系统类加载器进行加载
  • 优势,避免类的重复加载;保护程序安全,防止核心API被随意篡改,因为核心类库,会被向上委托由引导类加载,而如果自定义java.lang,引导类会禁止在这个包下自定义类
  • 沙箱安全机制,加载与核心类库同包同名的类,会加载jdk自带的核心类库,保证对java核心源代码的保护,相当于把核心类库的环境封闭起来,只能调用

触发类加载

  • 第一次需要使用类信息时加载
  • 原则,延迟加载,能不加载就不加载
  1. 调用静态成员时,会加载静态成员真正所在的类及其父类。通过子类调用父类的静态成员时只会加载父类而不会加载子类。
  2. 第一次new对象的时候加载(第二次再new同一个类时,不需再加载)。
  3. 加载子类会先加载父类。(覆盖父类方法时所抛出的异常不能超过父类定义的范围)
  4. 如果静态属性有final修饰时,则不一定会加载,当成常量使用
//如果等式右值改成表达式(且该表达式在编译时不能确定其值)时则会加载类
public static final int a =123;
//如果访问的是类的公开静态常量,那么如果编译器在编译的时候能确定这个常量的值,就不会被加载;如果编译时不能确定其值的话,则运行时加载
public static final int a = Math.PI;

类加载顺序

  • 加载静态成员/代码块,先递归地加载父类的静态成员/代码块(Object的最先);再依次加载到本类的静态成员;同一个类里的静态成员/代码块,按写代码的顺序加载
  • 加载非静态成员/代码块,实例块在创建对象时才会被加载,而静态成员在不创建对象时可以加载;先递归地加载父类的非静态成员/代码块(Object的最先),再依次加载到本类的非静态成员
  • 调用构造方法,先递归地调用父类的构造方法(Object的最先),也就是上溯下行;默认调用父类空参的;再依次到本类的构造方法;构造方法内,也可在第一行写明调用某个本类其它的构造方法

其他

  • 两个Class对象是否为同一个类,类的完整类名必须一致,包括包名;加载这个类的classLoader(类加载器实例对象)必须相同
  • jvm必须知道一个类型由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,jvm将这个类加载器的一个引用作为类型信息的一部分保存在方法区;当解析一个类型到另一个类型的引用的时候,需要保证这两个类型的类加载器是相同的
  • java对类的使用,主动使用(要操作这个类)和被动使用 image.png