深入学习Java虚拟机——类加载机制

  1. 云栖社区>
  2. 博客>
  3. 正文

深入学习Java虚拟机——类加载机制

江左煤郎 2018-08-27 15:53:56 浏览2435
展开阅读全文

当Java源码编译为字节码文件Class类文件即一串2进制字节流时虚拟机是如何将字节码文件加载到虚拟机中成为一个Class对象的

1. 类加载

    1. 类从被加载到虚拟机内存中开始到卸出内存为止它的整个生命内存周期包括加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个阶段统称为连接。

    2. 在类的加载过程的7个阶段中加载、验证、准备、初始化、卸载这5个阶段的顺序是固定的类加载过程中这5个阶段必须按照这个顺序开始顺序开始指这几个阶段都是互相交叉的混合式进行也就是说会在一个阶段执行过程中调用另一个阶段而不是按顺序完成每个阶段而解析阶段则不一定解析在某种情况下可以在初始化阶段之后执行这是为了支持Java的运行时绑定动态绑定或晚期绑定。

    3. 对于类加载过程中加载阶段的执行时机并没有明确指定但初始化阶段加载、验证、准备自然会在这之前开始的时机有以下几项

1遇到new、getstatic、putstatic、invokestatic这4条字节码指令时如果类没有进行过初始化则必须进行初始化。这四条指令的源代码场景为使用new关键字实例化对象读取或设置一个静态变量的值调用静态方法。

2使用java.lang.reflect包的方法对类进行反射调用时如果类没有进行过初始化则必须进行初始化。

3当初始化一个类的时候如果其父类还没有初始化则先进行父类的初始化。

4当虚拟机启动时用户需要指定一个要执行的主类包含main方法的类虚拟机会先初始这个类。

5使用jdk1.7的动态语言支持时如果有一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄并且这个方法句柄对应的类没有初始化则会触发该类的初始化。

    注意类的初始化必须满足以上条件中任何一个的所有要求不能少也不能加比如static静态变量前再加上final修饰符之后访问或设置其值就不会引起类的初始化因为对于常量的引用会直接将该引用转化成常量值存储进入调用该常量的所在类的常量池中使用时也只会使用常量池中的值而不是原静态常量的reference也就与静态常量所在类无关了所以不会对静态常量所在的类初始化

如果调用静态字段只有直接定义该字段的类会被初始化如果在其子类中调用不会初始化子类只会初始化父类。

如果是new某一个类的数组类型的变量同样不会初始化该类在虚拟机指令中进行newarray指令执行的是  "[*.*.ClassName" 这样一个类的初始化也就是由虚拟机自动生成的用  “ [ ” +类的全限定名 生成的直接继承于Object类的子类所以对于一维数组对象的引用我们可以使用length属性和clone方法。

2. 类加载的过程

2.1 加载

    1. 加载阶段虚拟机需要完成

1通过一个类的全限定名来获取定义此类的二进制字节流。

2将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。

3在内存准确说是方法区中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。

对于非数组类的加载过程其字节码文件可以有多种不同的方式来获取而非数组类的字节码文件可以使用虚拟机系统提供的引导类加载器完成也可以使用自己定义的类加载器完成。

    2. 对于数组类的加载与非数组类使用字节码文件创建有所不同它是由虚拟机直接创建的但数组类的元素类型去掉所有维度的类型仍然需要类加载器去创建一个数组类比如说数组类C的创建过程有以下原则

1如果数组的组件类型是引用类型那就递归采用上面所说的加载过程去加载这个组件类型数组C将在加载该组件类型的类加载器的类名称空间上被标识。

2如果数组的组件类型不是引用类型如int[]数组Java虚拟机将会把数组C标记为与类引导加载器关联。

3数组类的可见性与它的组件类型的可见性一致如果数组组件类型不是引用类型那数组的可见性默认为public。

    3. 加载阶段与连接验证、准备、解析阶段是交叉进行的但加载阶段与连接阶段的先后开始顺序是固定的。

2.2 验证

    1. 这一阶段的目的是为了确保Class文件的字节流中的信息符合当前虚拟机的要求并且不会危害虚拟机的安全。

    2. 需要完成4个检验工作

1文件格式检验检验字节流是否符合Class文件格式的规范比如以魔数0xCAFEBABE开头等验证点。

2元数据验证对字节码描述的信息进行语义分析保证其描述的信息符合Java语言规范比如这个类是否有父类是否继承了不允许集成的类等验证点。

3字节码验证通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。这一过程是最复杂的。

4符号引用验证发生在虚拟机将符号引用转化为直接引用的时候这个动作将在类加载过程的第四阶段解析中发生该验证是为了对类自身以外的信息进行匹配性校验。

2.3 准备

    1. 准备阶段是正式给类变量分配内存并设置变量初始值的阶段这些变量所使用的内存都在方法区中分配。这里的变量是指类变量也就是有static修饰的变量而不包括实例变量实例变量将通过new生成对象时一同在堆中分配空间。此外对于仅有static修饰的变量在通常情况下准备阶段中的设置变量初始值将统一设置为 0 、false、或者 null 值如果在源码中有赋值语句如下

public static int i=111;

那么会在类的初始化阶段再进行赋值操作而对于有 static 和 final共同修饰的常量如下

public static final int i=111;

那么就会在准备阶段就将 i 直接赋值为111因为此时 i 在编译时会成为ConstantValue属性根据这个属性虚拟机会直接将将 i 赋值为111。

2.4 解析

    1. 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info。

    2. 符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能准确的定位到目标即可。有符号引用但内存不一定有符号引用所指向的目标。

    3. 直接引用可以是指向目标的指针、相对偏移量或间接定位到目标的句柄。如果直接引用存在那么内存中必定由直接引用所指向的目标。

    4. 类或接口的解析过程

    1.如果当前代码所处的类为D要把一个从未解析的符号引用N作为一个类或接口C的直接引用步骤如下

1如果C不是一个数组类型那么虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C在加载C类过程中由于C中可能还会涉及其他类的加载所以会触发其他相关类的加载例如父类或接口一旦有任何一个加载过程出现异常解析过程就失败了。

2如果C是一个数组类型并且数组的元素类型为对象也就是N的描述符会是“[Ljava/lang/Ingeter”L 代表的是引用类型然后按照上面第1点的规则加载数组元素类型。

3如果以上步骤没有异常则类C以及其相关的类或接口已经在虚拟机内存中存在是一个有效的类或接口了但还需要最后一步验证类D是否对C具有访问权限如果不具备则抛出java.lang.IllegalAccessError异常。

    5. 字段解析

    1. 要解析一个从未解析的字段符合引用首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析也就是字段所属的类或接口的符号引用如果在这个过程中发生异常那么整个字段解析的过程都会结束如果该解析成功完成如果将这个字段被声明的类或接口用C表示那么对C进行后续解析的步骤如下

1如果C本身就包含了简单名称和字段描述符都与目标匹配的字段则返回这个字段的直接引用查找结束。

2否则如果C实现了接口或类但C不能是Object类就会按照继承关系从下往上搜索其父类或接口如果找到了简单名称和字段描述符都与目标匹配的字段则返回这个字段的直接引用查找结束。

3以上都查找不到则查找失败抛出java.lang.NoSuchFieldError异常。

注意实际上如果在父类或接口中出现了与子类中相同的名称那么将无法通过编译。

如果成功找到并返回了直接引用还需要对字段进行权限验证如果不具备对此字段的访问权限则抛出java.lang.IllegalAccessError异常。

    6. 类方法解析

    1. 首先要依据类方法表中的class_index项中索引代表的类或接口的符号引用而这个类或接口的符号引用就是当前方法所属的类或接口如果解析成功用C来表示这个类或接口后续步骤如下

1如果发现类方法表中class_index中索引指向的C是个接口那就直接抛出java.lang.IncompatibleClassChangeError异常。

2如果通过第一步则在类C中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。

3否则则在类C中的父类中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。

4否则则在类C的父接口列表以及父接口所继承的父接口中查找是否有简单名称与描述符都与目标匹配的方法如果有则说明C是抽象类查找结束抛出java.lang.AbstractMethodError

5否则方法查找失败抛出java.lang.NoSuchMethodError

如果成功找到并返回了直接引用还需要对方法进行权限验证如果不具备对此方法的访问权限则抛出java.lang.IllegalAccessError异常。

    7. 接口方法解析

    1. 首先要依据接口方法表中的class_index项中索引代表的类或接口的符号引用而这个类或接口的符号引用就是当前方法所属的类或接口如果解析成功用C来表示这个类或接口后续步骤如下

1如果发现类方法表中class_index中索引指向的C是个类那就直接抛出java.lang.IncompatibleClassChangeError异常。

2如果通过第一步则在接口C中查找是否有简单名称与描述符都与目标匹配的方法如果有则返回这个方法的直接引用查找结束。

3否则则在类C的父接口列表以及父接口所继承的父接口中递归查找是否有简单名称与描述符都与目标匹配的方法直到Object类为止如果有则返回这个方法的直接引用查找结束。

4否则方法查找失败抛出java.lang.NoSuchMethodError

接口中所有方法默认都是public不存在权限问题。

2.5 初始化

    1. 初始化是类加载过程的最后一步在准备阶段虚拟机已经对类变量进行了一次初始化过程在之前的阶段过程中除了加载阶段用户可以自定义类加载器之外其余阶段的工作完全由虚拟机主导和控制而在初始化阶段才是真正执行字节码的阶段。初始化阶段由程序员自己去决定初始化的类变量和其他数据。或者说初始化就是执行类构造器方法<client>()的过程。

    2. 类构造器方法<client>()运行中的细节

1<client>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{ }块中的语句合并而成编译器收集顺序有语句在源文件中出现的先后顺序决定静态语句块中可以访问并且赋值在静态语句块之前出现的类变量而对于定义在静态语句块之后类变量静态语句块中只能进行赋值而不能进行访问。

2<client>()方法与类的对象构造器<init>( )方法不同它不需要显示调用父类构造器虚拟机会在子类<client>()执行之前执行完毕父类的<client>()。因此虚拟机中第一个被执行的肯定是Object类。

3由于父类的<client>()先执行那么父类的静态语句块和类变量的赋值要优先于子类中的。

4<client>()对于类或接口不是必须的如果类或接口中不含对类变量或静态语句块那么编译器可以不生成该类的<client>()方法

5接口中不能使用静态语句块但仍然有类变量的初始化赋值操作因此接口也会生成<client>()方法。但接口与类不同的是执行接口的<client>()不需要执行父接口的<client>()只有当父接口中的类变量使用时才会执行父接口的<client>()另外接口的实现类在初始化时也不会执行接口的<client>()

6虚拟机会保证一个类的<client>()方法再多线程环境中被正确的加锁、同步如果多个线程去初始化一个类那么只有一个线程去执行该类的<client>()方法其他线程都会阻塞等待直到活动线程执行<client>()完毕。如果<client>()方法执行时间很久就有可能造成多个进程阻塞。

3.  类加载器

    通过一个类的全限定名来获取描述此类的二进制字节流这一动作会在虚拟机的外部实现而实现这个动作的代码块就是类加载器。

3.1 类与类加载器

    1. 类加载器虽然是用于实现类的加载动作但其作用不限于类的加载阶段。对于任何一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性也就是说比较两个类Class对象是否相等必须是在这两个类都在同一个类加载器加载的前提下才有意义否则即使这两个类来自于同一个Class文件被同一个虚拟机加载是要类加载器不同那么这两个类就不相等。判断两个类是否相等可以通过类的Class对象的equals( )方法、isAssignableFrom()方法、isInstance()方法返回的结果或者使用类的实例对象通过instanceof关键字来运算产生的结果。

public class ClassLoaderTest {
	public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
		ClassLoader myLoader=new ClassLoader() {
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
				try{
					String filename=name.substring(name.lastIndexOf(".")+1)+".class";
					System.out.println(filename);//执行结果中的前4行输出都来自这里
					InputStream is=getClass().getResourceAsStream(filename);
					if(is==null){
						return super.loadClass(name);
					}
					byte[] b=new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				}catch (IOException e) {
					throw new ClassNotFoundException(name);
				}
				
			}
		};
		Class<?> c=myLoader.loadClass("About_Jvm.ClassLoaderTest");//此行代码的执行结果为1、2行
		Object obj=c.newInstance();              //为何这里会引起执行结果中的3、4行
        /*
		 * 个人猜测当前代码的类ClassLoaderTest定义为A在A类中使用myLoader这个自定义类加载器加载后会生成另一个
		 * ClassLoaderTest类称为B虽然他们都来自于同一个字节码文件但他们在虚拟机中并不是同一个Class对象。
		 * 所以我们将A和B应该看做两个类更方便分析。
		 * 对于类A它由jdk中的应用程序类加载器来加载所以加载A类不会有任何输出而对于B类
		 * 是在A类中采用自定义的类加载器匿名内部类Ca类的对象myLoader来加载的Ca类属于A类。
		 * 
		 * B类的加载阶段完成后B类中与A类有相同的代码也有一个类加载器匿名内部类CbCb属于B类中的
		 * 当对B类进行实例化时会进行对B类的后续步骤验证解析准备然后初始化再调用无参
		 * 构造方法生成对象但进行这几个阶段时会发现Cb类并未加载或者说依据Cb类在常量池中的符号
		 * “ClassLoaderTest$1”无法在方法区中找到对应B类中的ClassLoaderTest$1类的Class对象
		 * 所以此时使用B类的类加载器也就是A类中的myLoader来进行尝试加载首先加载
		 * ClassLoaderTest$1的父类ClassLoader然后在加载ClassLoaderTest$1类。
		 * 所以会出现执行结果中的3、4行
		 */
		System.out.println(obj.getClass());
		System.out.println(obj instanceof About_Jvm.ClassLoaderTest);
	}
}

//执行结果
ClassLoaderTest.class   
Object.class           
ClassLoader.class       
ClassLoaderTest$1.class
class About_Jvm.ClassLoaderTest
false

上述代码中的ClassLoaderTest 类由两个类加载器完成加载一个是应用程序类加载器另一个是自定义加载器所以当进行比较时结果为false。而对于其中类加载器会涉及到双亲委派模型。

3.2 双亲委派模型

ç±»å è½½å¨çå亲å§æ´¾æ¨¡å

    1. 类加载器在虚拟机角度来说有两种一种是启动类加载器这个类加载器使用C++语言实现是虚拟机自身的一部分另一种就是其他所有的类加载器由Java实现是虚拟机外的模块并且全部继承自抽象类java.lang.ClassLoader。

    2. 在开发者角度来看虚拟机可以划分为更细致的4中类加载器

1启动类加载器Bootstrap ClassLoader负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootclassbath参数所指定的路径并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。

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

3应用程序类加载器Application ClassLoader这个类加载器由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值所以一般也称为系统加载器负责加载用户类路径上所指定的类库开发可以直接使用这个类加载器一般默认该类加载器。

4自定义类加载器User ClassLoader开发时自定义的加载器可以加载指定路径中的类。

    3. 双亲委派模型双亲委派模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器。类加载器之间的父子关系一般不会以继承关系而是以组合关系来复用父加载器的代码。

    4. 双亲委派模型的工作过程如果一个类加载器收到了类加载的请求他不会立即自己去尝试加载这个类而是会先将这个请求委派给父类加载器去完成每一个层次的类加载器都是如此因此最终所有的加载请求都会传给顶层的启动类加载器中当父加载器无法完成这个请求时子加载器就会自己去加载。

    5. 优点双亲委派模型使Java类随着它的类加载器一起具备了一种优先级的层次关系。例如java.lang.Object类它存放在rt.jar中无论哪一个类加载器加载这个类最终都会委派给启动类加载器加载所以Object类在程序中永远都是同一个类。相反如果没有这种双亲委派机制如果用户自己也编写了一个Object类并放在程序的classpath中那么系统将会出现多个Object类应用程序就会变得混乱。所以如果编写一个与rt.jar类库中已有的Java类可以被编译但永远无法加载运行。

    6. 实现自定义类加载器双亲委派的代码都在java.lang.ClassLoader的loadClass()方法中因此如果要编写自己的ClassLoader类就必须继承java.lang.ClassLoader抽象类重写loadClass()方法来实现自己的类加载器比如在 3.1 中的代码实例就是一个标准的实现自定义类加载器的例子。

网友评论

登录后评论
0/500
评论
江左煤郎
+ 关注