jvm类加载

四、类加载

编写的Java代码需要经过编译器编译为class文件(从本地机器码转变为字节码的过程),class文件是一组以8位字节为基础的二进制流,这些二进制流分别以一定形式表示着魔数(用于标识是否是一个能被虚拟机接收的Class文件)、版本号、字段表、访问标识等内容。代码编译为class文件后,需要通过类加载器把class文件加载到虚拟机中才能运行和使用。

1、类加载步骤
类从被加载到内存到使用完成被卸载出内存,需要经历加载、连接、初始化、使用、卸载这几个过程,其中连接又可以细分为验证、准备、解析

(1)加载

在加载阶段,虚拟机主要完成三件事情:
① 通过一个类的全限定名(比如com.danny.framework.t)来获取定义该类的二进制流;
② 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构;
③ 在内存中生成一个代表这个类的java.lang.Class对象,作为程序访问方法区中这个类的外部接口。

(2)验证

验证的目的是为了确保class文件的字节流包含的内容符合虚拟机的要求,且不会危害虚拟机的安全
文件格式验证:主要验证class文件中二进制字节流的格式,比如魔数是否已0xCAFEBABY开头、版本号是否正确等。

元数据验证:主要对字节码描述的信息进行语义分析,保证其符合Java语言规范,比如验证这个类是否有父类(java.lang.Object除外),如果这个类不是抽象类,是否实现了父类或接口中没有实现的方法,等等。

字节码验证:字节码验证更为高级,通过数据流和控制流分析,确保程序是合法的、符合逻辑的。

符号引用验证:对类自身以外的信息进行匹配性校验,举个栗子,比如通过类的全限定名能否找到对应类、在类中能否找到字段名/方法名对应的字段/方法,如果符号引用验证失败,将抛出“java.lang.NoSuchFieldError”、“java.lang.NoSuchMethodError”等异常。

(3)准备

正式为【类变量】分配内存并设置类变量【初始值】,这些变量所使用的内存都分配在方法区。注意分配内存的对象是“类变量”而不是实例变量,而且为其分配的是“初始值”,一般数值类型的初始值都为0,char类型的初始值为’\u0000’(常量池中一个表示Nul的字符串),boolean类型初始值为false,引用类型初始值为null。
但是加上final关键字比如public static final int value=123;在准备阶段会初始化value的值为123;

(4)解析

解析是将常量池中【符号引用】替换为【直接引用】的过程。

符号引用是以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。比如在com.danny.framework.LoggerFactory类引用了com.danny.framework.Logger,但在编译期间是不知道Logger类的内存地址的,所以只能先用com.danny.framework.Logger(假设是这个,实际上是由类似于CONSTANT_Class_info的常量来表示的)来表示Logger类的地址,这就是符号引用。

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,如果有了直接引用,那引用的目标一定在内存中存在。

解析的时候class已经被加载到方法区的内存中,因此要把符号引用转化为直接引用,也就是能直接找到该类实际内存地址的引用。

(5)初始化

在准备阶段,已经为类变量赋了初始值,在初始化阶段,则根据程序员通过程序定制的主观计划去初始化类变量的和其他资源,也可以从另一个角度来理解:初始化阶段是执行类构造器()方法的过程,那()到底是什么呢?
我的理解是,java在生成字节码时,如果类中有静态代码块或静态变量的赋值操作,会将类构造器()方法和实例构造器 () 方法添加到语法树中(可以理解为在编译阶段自动为类添加了两个隐藏的方法:类构造器——()方法和实例构造器——()方法,可以用javap命令查看),()主要用来构造类,比如初始化类变量(静态变量),执行静态代码块(statis{})等,该方法只执行一次;()方法主要用来构造实例,在构造实例的过程中,会首先执行(),这时对象中的所有成员变量都会被设置为默认值(每种数据类型的默认值和类加载准备阶段描述的一样),然后才会执行实例的构造函数(会先执行父类的构造方法,再执行非静态代码块,最后执行构造函数)。

类加载

2、类加载器

(1)类加载器的作用

  • 加载class:类加载的加载阶段的第一个步骤,就是通过类加载器来完成的,类加载器的主要任务就是“通过一个类的全限定名来获取描述此类的二进制字节流”,在这里,类加载器加载的二进制流并不一定要从class文件中获取,还可以从其他格式如zip文件中读取、从网络或数据库中读取、运行时动态生成、由其他文件生成(比如jsp生成class类文件)等。
    从程序员的角度来看,类加载器动态加载class文件到虚拟机中,并生成一个java.lang.Class实例,每个实例都代表一个java类,可以根据该实例得到该类的信息,还可以通过newInstance()方法生成该类的一个对象。

  • 确定类的唯一性:类加载器除了有加载类的作用,还有一个举足轻重的作用,对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才“相等”,这里的“相等”是指代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof关键字对对象所属关系的判定结果。

(2)类加载器的分类

以开发人员的角度来看,类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器(User ClassLoader),其中启动类加载器属于JVM的一部分,其他类加载器都用java实现,并且最终都继承自java.lang.ClassLoader。

启动类加载器(Bootstrap ClassLoader)是由C/C++编译而来的,看不到源码,所以在java.lang.ClassLoader源码中看到的Bootstrap ClassLoader的定义是native的“private native Class findBootstrapClass(String name);”。启动类加载器主要负责加载JAVA_HOME\lib目录或者被-Xbootclasspath参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)”来查看。

扩展类加载器(Extension ClassLoader)由sun.misc.Launcher.ExtClassLoader实现,负责加载JAVA_HOME\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的所有类库,可以用通过“System.getProperty(“java.ext.dirs”)”来查看具体都加载哪些类。

应用程序类加载器(Application ClassLoader)由sun.misc.Launcher.AppClassLoader实现,负责加载用户类路径(我们通常指定的classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。

自定义类加载器(User ClassLoader),JVM提供的类加载器只能加载指定目录的类(jar和class),如果我们想从其他地方甚至网络上获取class文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承ClassLoader或者它的子类来实现,但无论是通过继承ClassLoader还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用java.lang.ClassLoader.getSystemClassLoader()作为父加载器,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即应用程序类加载器。

当一个类加载器加载类的时候,如果有父加载器就先尝试让父加载器加载,如果父加载器还有父加载器就一直往上抛,一直把类加载的任务交给启动类加载器,然后启动类加载器如果加载不到类就会抛出ClassNotFoundException异常,之后把类加载的任务往下抛,如下图:

通过上图的类加载过程,就引出了一个比较重要的概念——双亲委派模型,如下图展示的层次关系,双亲委派模型要求除了顶层的启动类加载器之外,其他的类加载器都应该有一个父类加载器,但是这种父子关系并不是继承关系,而是像上面代码所示的组合关系。

双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会加载类,而是把这个请求委派给它上一层的父加载器,每层都如此,所以最终请求会传到启动类加载器,然后从启动类加载器开始尝试加载类,如果加载不到(要加载的类不在当前类加载器的加载范围),就让它的子类尝试加载,每层都是如此。

那么双亲委派模型有什么好处呢?最大的好处就是它让Java中的类跟类加载器一样有了“优先级”。前面说到了对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性,比如java.lang.Object类(存放在JAVA_HOME\lib\rt.jar中),如果用户自己写了一个java.lang.Object类并且由自定义类加载器加载,那么在程序中是不是就是两个类?所以双亲委派模型对保证Java稳定运行至关重要。

posted @ 2020-07-13 10:59  athony  阅读(102)  评论(0编辑  收藏  举报