类加载机制

导语

Java语言中类型的加载,连接和初始化都在程序运行期完成的。一个类的生命周期顺序:加载,连接(验证,准备,解析),初始化,使用,卸载。本文从这几个过程来分析。

加载

  • 找文件:通过一个类的全限定名(包名+类名)来获取定义此类的二进制文件,即我们所说的class文件
  • 加载到内存:将该字节码文件所代表的静态存储(字节码位于磁盘中,我们认为是静态存储)转为方法区的运行时数据结构
  • 在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据访问入口

加载的过程是由类加载器ClassLoader完成的,但是数组类本身不通过类加载器创建,它由JVM直接创建

连接

验证

确保class文件中的信息符合当前虚拟机的要求,验证包括文件格式验证,元数据验证,符号引用验证等

准备

为类变量分配内存并设置该类变量的初始值(这个初始值并不是我们代码中赋的值,而是所谓的“零值”)

解析

将符号引用变为直接引用,在生成的class文件中,我们不知道一个变量将来加载到内存后的实际位置,所以我们先用符号引用来指向这个变量。等class文件加载到内存后,地址被确定好了,就可以将符号引用换成直接引用(具体内存地址)。但是解析是有条件的,只有非虚方法才能被解析(静态方法,私有方法,实例构造器,父类方法和final方法)

初始化

编译好的字节码文件有一个< clinit >方法,该方法的作用就是对类变量进行初始化(我们代码中指定的值)。在类初始有以下要求:

  • 子类的方法执行前,父类的< clinit >方法已执行完毕
  • 执行子接口的方法前不需要先执行父接口的< clinit >方法
  • < clinit >方法执行是线程安全的,且只会被执行一次

类加载器

类加载器与JVM是独立的,类的加载需要类加载器完成。一个类只能被同一个类加载器加载一次,但是可以被多个类加载器加载。比如Test类可以被A加载器加载,又可以再次被B加载器加载器加载,这个内存中会有两个Test类,但是对于JVM来说这两个类不同,因为它们是由不同的加载器加载的。

字节码执行

先来看几个概念:

物理机

可执行文件由执行引擎加载执行,引擎与处理器指令集,操作系统息息相关。

虚拟机

字节码文件由虚拟机引擎加载,该引擎脱离了对处理器,操作系统的依赖。这也是为什么Java能实现一次编译,到处运行的原因。

栈帧

栈帧的概念不仅限于Java,C中也有栈帧。程序的执行其实是方法的不断调用(从main函数开始)。方法被调用时会将局部变量,传入参数等信息压栈。这样就形成了一个栈帧。可以理解为一个方法对应一个栈帧。Java中的栈帧包括局部变量表,操作数栈,动态连接,方法返回地址。

局部变量表

存储方法参数和方法内的局部变量,局部变量表中的最小存储单位为槽(slot)。槽的大小并没有明确规定,但是槽中必须能放下byte,char,boolean,short,int float,double和reference类型。

操作数栈

局部变量表只是记录了局部变量的信息,但是对局部变量的操作需要在栈上实现,比如c=a+b。
a,b,c都在局部变量表中,但+操作和赋值操作是在操作数栈上完成的。

C中的常识

方法的调用

方法的调用不等同于方法的执行,方法调用只关心方法的声明,并不关心将来被调用方法在内存中的具体位置

静态链接

C文件编译后有个链接的过程,编译时只是确定方法版本,然后建立符号索引。例如:在方法A中调用方法B,只要B被声明过,即使没有方法的具体实现也能编译通过。而在链接阶段则是将符号引用替换为直接引用(B方法在内存中的具体起始位置),这时如果方法B不存在则无法替换,链接失败。

动态链接

如果程序中了使用动态库,将符号引用转为直接引用的工作会由加载器完成,而不是链接器。

注意

静态类型与实际类型:

Human man = new Man();

man是一个变量,它有两种类型,即静态类型与实际类型。静态类型是Human类型,这个在编译的时候就能确定。实际类型在程序运行时由其指向的对象所确定的,这里的实际类型就是Man了。区分静态类型和实际类型与域的访问,方法的调用有关。下面看一个简单的例子:

public class A extends B{
	int a = 1;

 	public void p1() {
    	B b = new A();
     	System.out.println(b.a);
	}
}

class B {}

上面的代码中我们编译会出现错误,说B中没有a这个变量。这是因为对于对象中属性的访问都使用静态类型。b的静态类型是B,而B中没有a属性,所以编译会报错。并且静态类型也会对代码的运行产生影响。如果B中有变量a=0,上面的代码就会编译通过,但是输出结果是0。

分派

因为方法能被重载和复写,分派的任务就是找到一个合适的方法来执行。

静态分派:静态分派对应的是函数重载,编译器在编译阶段,会根据我们在调用某个方法时传入的参数(静态类型)来确定调用那个方法。

动态分派:对应的是方法复写(override),当一个对象向上转型后(Base b = new Child()),调用b的某个方法时,会涉及到动态分配。这时候需要用实际类型来定位了。假如Base中test()方法,Child()中也有test()方法,那么会调用Child()中的test()方法。即方法的调用是由实际类型决定的。

java指令集参考

第一个博文写有有点问题
http://blog.csdn.net/chenzhp/article/details/1798166
http://www.cnblogs.com/frank-pei/p/5432949.html

双亲委派模型

https://blog.csdn.net/zhangliangzi/article/details/51338291

为什么使用双亲委派

第一个原因是防止重复加载,子ClassLoader要加载一个类时看父ClassLoader是否加载,如果加载过就没必要加载了。还有的就是安全问题,如果不使用委托模式,那么可以自定义一个核心类(比如String类)来动态替代Java核心类,这样存在非常大的安全隐患。

原文地址:https://www.cnblogs.com/xidongyu/p/6433982.html