类加载过程
Java源文件 —编译器—> 字节码文件 —-解释器—> 机器码
加载时机
以下情况一个类会被加载,在jvm生命周期中每个类只会加载一次。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字,所以加载类时是线程安全的。
- 生成该类对象的时候,会同时加载该类及该类的所有父类;
- 访问该类的静态成员或者方法的时候;
-
反射机制
并不是所有的Class都能在编译时明确,因此在某些情况下需要在运行时再发现和确定类型信息(比如:基于构建编程,这就是RTTI(Runtime Type Information,运行时类型信息)。在java中,有两种RTTI的方式,一种是传统的,即假设在编译时已经知道了所有的类型;还有一种,是利用反射机制,在运行时再尝试确定类型信息。
严格的说,反射也是一种形式的RTTI,不过,一般的文档资料中把RTTI和反射分开,因为一般的,大家认为RTTI指的是传统的RTTI,通过继承和多态来实现,在运行时通过调用超类的方法来实现具体的功能(超类会自动实例化为子类,或使用instance of)。传统的RTTI有3种实现方式: 向上转型或向下转型(upcasting and downcasting),在java中,向下转型(父类转成子类)需要强制类型转换
- Class对象(用了Class对象,不代表就是反射,如果只是用Class对象cast成指定的类,那就还是传统的RTTI)
- instanceof或isInstance()
传统的RTTI与反射最主要的区别,在于RTTI在编译期需要.class文件,而反射不需要。传统的RTTI使用转型或Instance形式实现,但都需要指定要转型的类型,比如:Toy y = (Toy) Object;注意其中的obj虽然是被转型了,但在编译期,就需要知道要转成的类型Toy,也就是需要Toy的.class文件。相对的,反射完全在运行时在通过Class类来确定类型,不需要提前加载Toy的.class文件。
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。然而, 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class,我们可以通过java提供的接口获取该Class对象的所有信息,包括修饰符,方法,属性等。
// 可以从一个实例中获取一个类对象
Random generator = new Random():
Class cl = generator.getClass();
// 类对象的名字获取这个类
String className = cl.getName();
Class cl = Class.forName(className);
// 直接从类中获取类对象
Class cl = Random.class;
Class cl 2 = int.class;
Class cl 3 = Double[].class
// 虚拟机为每个类型管理一个 Class 对象,可以利用==运算符实现两个类对象比较
generator.getClass() == Random.class();
java.lang.Class
- static Class forName(String className) 返回描述类名为 className 的 Class 对象
- Object newlnstance() 调用构造方法创建一个新实例
- native int getModifiers() 返回一个编码成int的类修饰符信息
- Constructor[] getConstructors() 返回该类的构造方法
- Constructor[] getDeclaredConstructors() 返回包含 Constructor 对象的数组, 其中包含了 Class 对象所描述的类的所有公有构造器(getConstructors ) 或所有构造器(getDeclaredConstructors)
- Method[] getMethods()
- Method[] getDeclareMethods() 返回包含 Method 对象的数组,getMethods 将返回所有的公有方法, 包括从超类继承来的公有方法;getDeclaredMethods 返回这个类或接口的全部方法, 但不包括由超类继承了的方法
- Method getMethod(String name, Class… parameterTypes) 获取执行名字与参数类型的方法
- Field[] getFields 方法将返回一个包含 Field 对象的数组, 这些对象记录了这个类或其超类的公有域
- Field[] getDeclaredFields 方法也将返回包含 Field 对象的数组, 这些对象记录了这个类的全部域
- Field getField(String name) 返回指定名称的公有域
- Field getDeclaredField(String name) 返回类中声明的给定名称的域
java.Iang.reflect.Constructor
- Object newlnstance(Object[] args) 构造一个这个构造器所属类的新实例
java.Iang.reflect.Field
- Object get(Object obj) 返回 obj 对象中用 Field 对象表示的域值
- void set(Object obj ,Object newValue) 用一个新值设置 Obj 对象中 Field 对象表示的域
java.Iang.reflect.Method
- public Object invokeCObject implicitParameter,Object[] explicitParamenters) 调用这个对象所描述的方法, 传递给定参数,并返回方法的返回值。对于静态方法,把 mill 作为隐式参数传递。 在使用包装器传递基本类型的值时, 基本类型的返回值必须是未包装的。
java.lang.reflect.Modifier
static String toString(int modifiers) 返回对应 modifiers 中位设置的修饰符的字符串表
类加载器
顾名思义,它是用来加载 Class 的,它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 .class,也可以是 jar 包里的 .class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。CLassLoader有以下特点:
每个类不相同,每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的,不同ClassLoader加载的加载的类不相同。
- 延迟加载,JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。
各司其职,JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。
Class Loader类型
引导类加载器(BootstrapClassLoader ),引导类加载器负责加载系统类(通常从JAR文件rt.jar中进行加载)。他是虚拟机不可分割的一部分,而且通常是C语言实现的。
- 扩展类加载器(ExtensionClassLoader ),扩展类加载器同于从jre/lib/ext目录加载”标准的扩展”。可以将JAR文件放入该目录,这样即使没有Classpath,扩展类加载器也可以找到其中的各个类。
- 应用类加载器(AppClassLoader),直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
双亲委派
前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。
AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 不可以加载,它就会搜索 Classpath尝试加载类 。
而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 不可以加载。否则它就会搜索 ext 路径下的 jar 包尝试加载类。
为什么要使用双亲委派
双亲委派机制有效的解决了,同一个类只会被同一个加载器加载。在java中判断两个对象相同要满足以下条件
- 都是用同名的类完成实例化的。
- 两个实例各自对应的同名的类的加载器必须是同一个
加载过程
加载
通过类加载器将外部的class文件加载到java虚拟机,存储到方法区内
- 通过类的全限定名来获取此类的二进制字节流
- 全限定名相当于类的绝对路径,对于不同的类有不同的全限定名命名方式,一般为包名.外部类名$内部类名
- 将这个字节流所代表的静态存储结构转换为方法区运行时的数据结构
在内存中生成一个java.lang.Class对象,作为方法区该类的各种数据访问入口
验证
验证是保证二进制字节码在结构上的正确性,具体来说,工作包括检测类型正确性,接入属性正确性(public、private),检查final class 没有被继承,检查静态变量的正确性等。
准备
准备阶段主要是创建静态域,分配空间,给这些域设默认值。在准备阶段不会执行任何代码,仅仅是设置默认值
原生类型全部设为0,如:float:0f,int 0, long 0L, boolean:0
- 引用类型设置为null
-
解析
解析的过程就是对类中的接口、类、方法、变量的符号引用进行解析并定位,解析成直接引用,并保证这些类被正确的找到。解析的过程可能导致其它的类被加载,根据不同的解析策略,这一步不一定是必须的,有些解析策略在解析时递归的把所有引用解析,这是early resolution,要求所有引用都必须存在;还有一种策略是late resolution,这也是Oracle 的JDK所采取的策略,即类只是被引用了,还没有被真正用到时,并不进行解析,只有当真正用到了,才去加载和解析这个类。
将常量池中的符号引用转换为直接引用。符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址
初始化
初始化类变量与静态语句块
代码执行
虚拟机栈
每个线程都有自己的栈,类中的方法都是在栈中执行的,执行过程如下:
- JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈和出栈
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出
栈内存结构
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息,在这个线程上正在执行的每个方法都各自有对应的一个栈帧。
每个栈帧中存储着:
局部变量表
局部变量表是一组变量值存储空间,主要用于存储方法参数和定义在方法体内的局部变量,包括编译器可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址,已被异常表取代)
操作数栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop),某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如,执行复制、交换、求和等操作。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
分派
分派是实现面向对象编程多态的方式,即在Java中的重载与重写,简单理解就是确定选择执行方法的过程。
虚方法表
在面向对象编程中,会频繁的使用到动态分派,如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能,JVM 采用在类的方法区建立一个虚方法表(virtual method table),使用索引表来代替查找。非虚方法不会出现在表中。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。
外观类型与实际类型
Map map = new HashMap();
System.out.println((Object)map);
map = new HeapMap();
如上面代码,其中”Map”称为变量外观类型(Apparent Type),”HashMap”称为变量的实际类型(Actual Type)。外观类型和实际类型在程序中都可以发生变化,区别是外观类型的变化仅仅在使用时发生变化,并且外观类型是在编译期可知的。而实际类型变化的结果在运行期才能确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
静态分派
依赖外观类型来定位方法执行版本的分派动作称为静态分派,Java里面的静态分派的具体体现是方法的重载。
public class Test {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello, guy!");
}
public void sayHello(Man guy) {
System.out.println("hello gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();
test.sayHello(man); // hello, guy!
test.sayHello(woman); // hello, guy!
}
}
动态分派
依赖动态类型来定位方法执行版本的分派动作称为动态分派,动态分派发生在运行期。Java里面的动态分派主要体现在“重写”上。
public class DynamicDispatch {
static class Shape {
protected void draw(){
System.out.println("It is shape!");
}
}
static class Circle extends Shape {
protected void draw(){
System.out.println("It is circle!");
}
}
public static void main(String[] args) {
Shape shape = new Shape();
Shape circle = new Circle();
shape.draw(); // It is shape!
circle.draw(); // It is circle!
}
}
方法返回地址
一个方法的结束,有两种方式
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
**
附加信息
参考文档:
https://www.cnblogs.com/zhguang/p/3091378.html
http://www.importnew.com/29389.html
类加载过程:https://www.jianshu.com/p/3ca14ec823d7**
http://blog.itpub.net/31561269/viewspace-2222522/
https://www.jianshu.com/p/8743d8062bb6
https://www.cnblogs.com/zhguang/p/3154584.html