1 简单介绍
1.1 跨平台运行
Java的编译和平台独立性
首先Java是平台独立性语言(C/C++就不是,java一次编译在各个平台上都能执行),这关键就在它的字节码和JVM机制。Java程序编译后不是直接生成硬件平台的可执行代码,而是生成.class的字节码文件,再交由JVM翻译成对应硬件平台可执行的代码。(也就是说.java文件被javac指令编译为.class的字节码文件,再由JVM执行)。
1.2 编译机制
Java字节码的执行分为:即时编译和解释执行,通常采用解释执行方式
-
解释执行:是指解释器通过每次解释并执行一小段代码来完成.class程序的所有操作 -
即时编译:则是以方法为单位,将字节码.class文件一次性翻译为机器码后执行HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化 -
静态提前编译(Ahead Of Time,AOT编译)程序运行前,直接把Java源码文件(.java)编译成本地机器码的过程;优点: 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动; 把编译的本地机器码保存磁盘,不占用内存,并可多次使用;缺点:因为Java语言的动态性(如反射)带来了额外的复杂性,影响了静态编译代码的质量; 一般静态编译不如JIT编译的质量,这种方式用得比较少;
2 类加载机制
Java语言是一种具有动态性的解释性语言,类(Class)只有被加载到JVM中才能运行。JVM会将编译生成的.class文件加载到内存中,并组织成为一个完整的Java程序。 这个加载过程则是由类加载器(ClassLoader和它的子类)来完成的,其实质是把类文件从硬盘读到内存中。
2.1 加载方式
在Java中类的加载是动态的,它不会一次性加载所有类然后运行,而是先把保证程序能运行的基类先加载到JVM中,其他类则是在需要时再加载,这样就加快了加载速度,而且节约了程序运行过程中内存的开销
类的加载方式分为:
-
隐式加载:程序使用new等方式创建对象,会隐式的调用类加载器。 -
显式加载:直接调用class.forName()方法
2.2 加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化 3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化
2.2.1 加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载
class文件,这是前面绝大部分示例程序的类加载方式。 - 从
JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。 - 通过网络加载
class文件。 - 把一个
Java源文件动态编译,并执行加载。
2.2.2 链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段:验证,准备,解析
2.2.2.1 验证
验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。
验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
四种验证做进一步说明:
2.2.2.1.1 文件格式验证
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。
例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
2.2.2.1.2 元数据验证
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
2.2.2.1.3 字节码验证
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。
主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
2.2.2.1.4 符号引用验证
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段
主要去确定访问类型等涉及到引用的情况
主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2.2.2.2 准备
类准备阶段负责为类的静态变量分配内存,并设置默认初始值
2.2.2.3 解析
将类的二进制数据中的符号引用替换成直接引用。
说明一下符号引用和直接引用区别:
-
符号引用:是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关 -
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
2.2.3 初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的
如果类中有语句:private static int a = 10,它的执行过程是这样的:
首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10
2.2.4 类加载总结
类的加载主要分为3步:
-
装载:根据查找路径找到相应的class文件,然后倒入。 -
链接:
- 检查:检查待记载的
class文件的正确性。- 准备:给类中的静态变量分配存储空间。(这里用到了
static关键字的知识)- 解析:将符号引用转换成直接引用(此步是可选的)
-
初始化:对静态变量和静态代码块执行初始化工作。这个阶段才是真正开始执行类中的字节码
2.3 类加载时机
2.3.1 类初始化的条件
什么时候需要对类进行初始化:
- 使用
new该类实例化对象的时候; - 读取或设置类静态字段的时候(但被
final修饰的字段,在编译器时就被放入常量池的静态字段除外static final); - 调用类静态方法的时候;
- 使用反射
Class.forName(“全限定类名”)对类进行反射调用的时候,该类需要初始化; - 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
- 被标明为启动类的类(即包含
main()方法的类)要初始化; - 当使用
JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
2.3.2 对象初始化顺序
java对象初始化顺序:
-
本类:静态变量,静态初始化块,变量,初始化块,构造函数 -
继承类:父类静态变量,父类静态初始化块,子类静态变量,子类静态初始化块,父类变量,父类初始化块,父类构造函数,子类变量,子类初始化块,子类构造函数
3 类加载器
3.1 了解类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
3.2 类加载器分类
类加载器的图示:
JVM预定义有三种类加载器,当一个JVM启动的时候,Java开始使用如下三种类加载器:
- 根类加载器(
bootstrap class loader):它用来加载Java的核心类,是用原生代码来实现的,并不继承自java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 -
扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null
假如当我们使用根加载器加载的对象使用此方法获取到的ClassLoader是null,为什么是这样呢?前面已经说了,根类加载器是使用C++编写的,JVM不能够也不允许程序员获取该类,所以返回的是null,还有一点,如果此对象表示的是一个基本类型或void,则返回null,其实进一步的含义就是:Java中所有的基本数据类型都是由根加载器加载的 -
系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader
3.3 类加载机制
3.3.1 类加载机制分类
JVM的类加载机制主要有如下3种:
-
全盘负责:是指当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 -
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才给子类去加载。 -
缓存机制:会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
3.3.2 双亲委派机制
3.3.2.1 双亲原理
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
3.3.2.2 双亲优点
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要让子ClassLoader再加载一次
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。