JVM总结(2)JVM的Classloader

java源代码编译以后生成的class文件如何跑到JVM中的呢?对,就是Classloader把她们加载到JVM中去的。

接下来呢,就从3个方面对Classloader进行说明:Classloder的时机;Classloader的加载过程;Classloader的原理及分类。

1.Classloader的时机
类的生命周期:加载,验证,准备,解析,初始化,使用,卸载。
其中验证,准备,解析三个阶段可以称做连接(linking)
注意,以上的阶段中虽然开始时间分先后(注意:为了支持java中的运行时绑定解析阶段有可能在初始化后才开始执行),不过各个阶段通常都是交叉执行的。

什么时候需要加载一个类呢?虚拟机规范中明确规定了4种初始化阶段的触发时机(而加载,验证,准备自然需要在此之前开始):
1)遇到new、getstatic、putstatic或invokestatic这4个字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成4条指令最常用的java代码场景是:new一个对象时,读或者设置一个static字段时(final static除外)调用一个类的static方法时
2)使用java.lang.reflect包的方法对类进行反射调用的时候。
3)当初始化一个类,父类未初始化,则先初始化父类
4)jvm启动时,app执行的入口(包含main的类)会先被初始化。
以上4种场景被称为对一个类的主动引用,除此之外都是被动引用,不会触发初始化动作,举例:
1)通过子类引用父类的静态字段,不会导致子类初始化
2)通过数组定义来引用类,不会触发该类的初始化
3)常量(static final)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,不会触发该类初始化

注意:接口和类有不一样的地方,就是一个类初始化前会要求其所有父类先初始化,但是接口在初始化时,并不要求其父接口全部都完成初始化,只有当真正使用父类接口(如引用接口中定义的常量)时才初始化。

2.Classloader的加载过程
1)类加载:
步骤:(1)通过一个类的全限定名来获取定义此类的二进制流
      (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
      (3)在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的操作入口

其中,被加载的class有很多种来源,举例:从ZIP等class文件包中读取;在运行时生成,如通过动态代理产生的代理类;有其他文件生成的class,如JSP应用等等。

2)连接:连接又分为以下几个步骤
(1)验证:
验证是连接阶段的第一步,是为了确保class文件的字节流符合JVM的规范。通常有4个阶段的验证过程:文件格式验证、元数据验证、字节码验证、符合引用验证。
1》文件格式验证:是否符合Class文件规范格式,符合才能进入方法区
2》元数据验证:是否符合Java语言规范的要求,主要进行字节码的语义分析
3》字节码验证:对类的方法体进行校验,主要进行数据流和控制流的分析
4》符合引用验证:最后一个阶段的校验发生在JVM将符合引用转换成直接引用(下文有提到)的时候

(2)准备:该阶段正式为类(static,不过final static--会直接赋=号后面的值,除外)变量分配内存并设置类变量默认值(这个时候没有实例变量什么事,因为还没有实例)

(3)解析:把常量池内的符合引用(只是和内存无关的符号标记)转换为直接引用(内存地址的引用)。解析的动作主要针对类或接口、字段(static)、类方法(static方法)、接口方法4类符合引用进行,分别对应于常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_INterfaceMethodref_info四种常量类型。
  
3)类的初始化:将类的静态变量(static)赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。

类的初始化阶段是类加载的最后一步,前面的类加载过程中,除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其他的工作都是由JVM主导和控制的。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

3.类加载器的分类

注意:对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在Java虚拟机中唯一性(也就是说,即使同一个class文件,若不同的classloader去加载,在JVM中就是两个不同的类)。

1)符合双亲委派模型的类加载器
主要分为三类:
(1)启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将放到%JAVA_HOME%/lib目录中的,或被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到JVM内存中。启动类加载器无法被Java程序直接使用。

(2)扩展类加载器(Extension ClassLoader)
这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载%JAVA_HOME%/lib/ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

(3)应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法返回的值,所有一般也成为系统类加载器。它负责加载用户路径(ClassPath)中所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下,这个就是程序中的默认类加载器。

双亲委派模型(Parents Delegation Model),要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里的父子关系是通过组合来实现。

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子类加载器才会尝试自己加载。

双亲委派模型对应保证java程序的稳定运行很重要,但他的实现却非常简单,代码都集中在java.lang.ClassLoader的loadClass()方法中,
   
protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name);
	if (c == null) {
	    try {
		if (parent != null) {
		    c = parent.loadClass(name, false);
		} else {
		    c = findBootstrapClass0(name);
		}
	    } catch (ClassNotFoundException e) {
	        // If still not found, then invoke findClass in order
	        // to find the class.
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }

执行逻辑:先检查是否被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认启动类加载器为父加载器,如果父加载器加载失败(抛出ClassNotFoundException),再调用自己的findClass()方法进行加载。

2)OSGi的类加载器
OSGi实现模块化热部署的关键则是它自定义的类加载器的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
是OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构,当收到类加载请求时,OSGi将按照如下的顺序进行类的搜索:
(1)将以java.*开头的类,委派给父类加载器加载;
(2)将委派给列表名单(org.osgi.framework.bootdelegation)内的类,委派给父类加载器加载;
(3)将Import-package列表中的类,委派给Export这个类的Bundle的类加载器加载;
(4)将Required-Bundle列表中的类,委派各个类对应Bundle的类加载器加载;
(4)查找当前Bundle的ClassPath,使用自己的类加载器加载;
(5)查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
(6)查找Dynamic Import(动态导入)列表的Bundel,委派给对应Bundle的类加载加载
(7)否则,类查找失败

你可能感兴趣的:(ClassLoader)