Java:跨平台的语言
java程序是以.java结尾的源文件
会先被编译为字节码文件,而字节码文件,可以在不同的平台上解释运行,针对不同的操作系统去安装不同的JVM。
而不用担心字节码文件的兼容性,因为所有的jvm虚拟机都全部遵守java虚拟机的规范,都可以解释执行
JVM:跨语言的平台
很多语言都可以在java平台上运行,只需要不同的语言提供不同的编译器,把他们的源文件编译为符合java虚拟机规范(二进制开头为cafebabe等格式)的字节码(.class)文件即可,就可以在jvm虚拟机上运行
jvm虚拟机也不在乎所编译字节码文件是不是java文件。它只关心字节码文件是否符合自己的规范。
Java虚拟机的特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
HotSpot
- java默认的虚拟机,Sun/Oracle JDK和OpenJDK的默认虚拟机
- 从服务器、桌面到一定段、嵌入式都有应用。
- 热点代码探测技术
通过计数器找到最具编译价值代码,触发即时编译或栈上替换
通过编译器和解释器协同工作,在最优化的程序响应时间和最佳执行性能中取得平衡
JVM的生命周期
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrop class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的
虚拟机的执行
- 一个运行中的java虚拟机有着一个清晰的任务:执行java程序。
- 程序开始执行时他才运行,程序结束时就停止,虚拟机此时也停止
- 执行一个所谓java程序的时候,真真正正在执行的是一个叫做java虚拟机的进程
虚拟机的退出
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而带制java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作
JVM虚拟机的结构
1.编译器
编译器粗略分为词法分析,语法分析,类型检查,中间代码生成,代码优化,目标代码生成,目标代码优化,
而中间代码生成和其之前的阶段为前端编译,之后的称为后端编译。
前端编译主要指与源代码有关但与目标机无关的部分
后端编译主要指与目标机有关的部分,包括代码优化和目标代码生成
在java中,把java文件转化为class文件这个过程就是前端编译,中间代码就是class文件。
而在执行引擎中的JIT即时编译阶段为后端编译,在这一过程中,目标机为自己的计算机,先把字节码文件解析执行,再通过JIT即时编译器把class文件编译转化为二进制机器码(此过程还会把热点代码缓存到方法区),让计算机可以看懂我们的代码。
前端编译:
把java源文件编译为字节码文件的过程,也就是把满足java语言规范的程序转化为满足JVM规范所要求格式的功能。
在这一过程中,需要词法分析,语法分析,语法/抽象语法树,语义分析,注解抽象语法树,字节码生成器, 最终生成字节码文件
优点:许多java语法新特性(语法糖:泛型,内部类等等),都是靠前端编译器实现的,而不是依赖虚拟机
编译成的class文件可以直接给JVM虚拟机解释执行,省了编译时间。
缺点:这一过程对代码运行效率没有任何优化措施,解释执行效率低下
后端编译:(后面总结)。
java编译器输入的指令流是一种基于栈的指令集架构,还有一种基于寄存器的指令集架构
基于寄存器的指令集架构
- 直接由CPU来执行。
- 指令集架构直接依赖硬件,与硬件强耦合。不同平台就可能导致硬件的不同,实现方式也各不同。可移植性差。
- 性能优秀和执行更高效
- 相对于栈模型,它的性能和效率要好,同时指令集更大,实现同一个功能所要执行的指令更少。
- 典型的有×86的二进制指令集,比如传统的pc和android的Davlik虚拟机。
- 大部分情况下,寄存器指令集都以一地址指令,二地址指令,三地址指令为主。采用双字节(16位)方式进行对齐
基于栈的指令集架构:
- 由于跨平台性的设计,java的指令都是根据栈来设计的,因为不同的平台CPU架构不同,而寄存器依赖于CPU,所以不能用寄存器指令集。
- 避开了寄存器的分配难题:使用零地址指令方式配,每8位字节进行对齐
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。但是指令更多。
- 不需要硬件支撑,可移植性更好,更好实现跨平台
- 执行性能比寄存器差
- 因为是基于栈的,只需要考虑入栈出栈和执行等操作,设计实现都非常简单,适用于资源受限的系统(嵌入式)
- 在非资源受限的场景中也可以使用。
类的加载器子系统
注:此下的方法区为统称,挖个坑
作用:
- 负责从文件系统或者网络中加载class文件
- 加载器只负责class文件的加载,是否可以运行则是由执行引擎决定的
- 加载的类信息存放在一块称为方法区的内存空间,除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面值和数字常量
加载一个未装载过的类文件的简单过程:
- 一个字节码文件存在于本地硬盘上,相当于一个模板,用类加载器通过二进制流的方式加载到JVM,存放到其中的方法区,成为了一个DNA元数据模板。
- 类加载器相当于一个运输工具,从硬盘上的一个文件------>JVM-------->元数据模板
- 通过元数据模板,调用其构造器,可以创建多个一样的类对象实例,存放于堆中
- 元数据模板通过getClassLoader()可以得到其构造器
- 实例对象可以通过getClass()获取到其元数据模板本身
类的整体加载过程
加载---->链接(验证,准备,解析)----->初始化
加载:
java虚拟机把类文件加载到内存中,并对class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中创建一个代表这个类的java.lang.Class对象,这个对象用来封装类在方法区内的数据结构,相当于一个大的class实例**(HotSpot虚拟机其实把这个对象放在了方法区,而不是放在堆中)**,也作为方法区这个类的各种数据的访问入口
验证
如果验证失败,会抛出java.lang.VerifyError异常
其目的时确保class文件的字节流中包含的信息符合当前虚拟机的要求,并保证不会危害到虚拟机的安全,主要保证被加载类的正确性。
主要工作:
文件格式验证:
验证是否以魔数0xCAFEBABE开头
主、次版本号是否在当前虚拟机处理范围之内
常量池中的常量是否有不被支持的常量类型(检查常量tag标志)
指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
class文件中各个部分及文件本身是否有被删除的或附加的其他信息等等
该验证阶段的主要目的是保证输入的字节流能正确的解析并存储到方法区内,至于通过了这个阶段的验证后,字节流才能存入内存中的方法区,而往后的三个验证阶段都是基于方法区的存储结果进行的,不会再与字节流有直接交互。
元数据验证
主要是对字节码描述的信息进行语义分析,对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
包括是否有父类、是否是抽象类、是否是接口,是否继承了不允许被继承的类(final类)、是否实现了父类或者接口的方法等。
字节码验证
是整个验证过程最复杂的,主要进行数据流和控制流分析,确保程序语义是合法的、符合逻辑的。在元数据阶段对其信息中的数据结构做完校验后,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
如保证跳转指令不会跳转到方法体之外的字节码指令、数据类型转换安全有效等
符号引用验证:
发生在虚拟机将符号引用转化为直接引用的时候(连接解析阶段进行符号引用转换为直接引用),符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类异常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
- 验证阶段对于虚拟机来说非常重要,但不是一个必需的基底段,如果所运行的代码已经反复被使用和验证过了,可以通过-Xverify:none参数关闭大部分的验证措施,以提高虚拟机运行时间
准备
- 这个阶段是正式为类变量分配内存并且设置该类变量的默认初始值,即初值。这些类变量所使用的内存都将在方法区中进行分配
注意:此阶段只为类变量即被static修饰的变量进行内存分配,不包含实例变量,实例变量会在对象实例化的时候随着对象一起分配到java堆空间中。
- 这里不包括用final修饰的static,因为final在编译阶段就会被分配了,准备阶段会显式初始化,如public static final int a=123;在准备阶段过程的初始值直接就是123了,不需要准备为零值。
在编译的时候javac将被static和final修饰的常量生成ConstantValue属性,在此时类加载的准备阶段便会根据ConstantValue的值为常量设置相应的值,而ConstantValue修饰的字段只限于基本类型和string类型。因为常量池中只能引用到基本类型和string类型的字面值
这一过程可以理解为在编译器就把其结果放入了常量池中,类还未被加载的时候,static final字段就已经有了值,这个值不需要对类进行初始化就可以读取。
- 对于普通非final的类变量,如public static int a= 123;在准备阶段过后的初始值是0(数据类型的零值),而不是123,在初始化阶段才会把123赋值给a
解析
- 将常量池内的符号引用转换为直接引用的过程
- 这一阶段往往在初始化阶段之后执行
- 符号引用就是一组符号来描述所引用的目标,符号可以是任何形式的字面量,符号引用和虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。一般符号引用的字面量形式已经明确的定义在java虚拟机规范的Class文件各式中。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,直接引用与虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出的直接引用一般不会相同。
- 如果有了直接引用,那么引用的目标一定都存在于内存中了。
- 解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等
初始化
- 初始化阶段就是执行类构造器方法()的过程
- 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行
- 不同于类的构造器
- 若该类具有父类,JVM会保证子类的类变量初始化之前,先对父类的类变量初始化
- 虚拟机必须保证一个类的方法在多线程下被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,一直到活动线程执行类初始化方法完毕
类加载器
引导类加载器和自定义类加载器
只要不是引导类加载器,就是自定义类加载器。
java虚拟机规范中把所以派生于ClassLoader的加载器通称为自定义类加载器
而ExtClassLoader拓展类加载器和System Class loader 系统类加载器(又叫AppClassLoader应用程序类加载器)都是派生于ClassLoader的,所以也归为自定义类加载器
bootstropClassLoader
- 使用C,C++编写的,用getClassLoader()方法是获取不到的。
- 是所有类加载器的最顶层,完全由JVM控制,我们既不能访问,也不能控制,也没有子加载器
- 只用来加载java的核心类库(jre/lib/rt.jar、resources.jar等等),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类加载器和应用程序类加载器,并作为他们的父类加载器
- 只加载报名为java、javax、sun等开头的类
ExtClassLoader
- 负责加载java的扩展类库,加载目录为(jre/lib/ext)
- 他们也是对象,也需要被加载,父类加载器为启动类加载器
- 他为java语言编写的。所以我们可以访问的到。
- 如果我们手动创建jar包放在此目录下,也会自动由扩展类加载器加载。
AppClassLoader
- java语言编写,派生于ClassLoader类,是程序中默认的类加载器,一般的类都有它来完成加载
- 父类加载器为扩展类加载器
- 负责加载环境变量classPath目录下的所有jar和class文件,或系统属性java.class.path指定路径下的类库
类和类加载器
类加载器的深层意义
类加载器虽然只用于加载类,但他还有更深层次更重要的作用:
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性
要比较两个类是否相等,必须建立在这两个类是由同一个类加载器加载的基础上,否则他们不可能相等。