zoukankan      html  css  js  c++  java
  • java的类加载机制

    1. 什么是类的加载

    类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区的数据结构.类的加载的最终产品使位于堆区的Class对象,Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口.

    类加载并需要等到某个类被"首次主动使用"时再加载,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预加载的过程中遇到了.class文件缺失或存在错误,类加载必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类之一没有被程序主动使用,那么类加载器就不会报告错误
    加载.class文件的方式

    • 从本地系统中直接加载
    • 通过网络下载.class文件
    • 从.zip,jar等归档文件中加载.class文件
    • 从专有数据库中提取.class文件
    • 将java源文件动态编译成.class文件

    2. 类的生命周期


    其中类加载的过程包括了加载,验证,准备,解析,初始化五个阶段.在这五个阶段中,加载,验证,准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 java语言的运行时绑定(也称为动态绑定).另外注意这里的几个阶段是按顺序开始的,而不是按顺序进行或者完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个 阶段.

    • 加载:查找并加载类的二进制数据
      加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成一下三件事:

      1. 通过类的权限定名来获取其定义的二进制字节流
      2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
      3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
        相对于类加载的其他阶段而言,加载阶段(准确的说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器 来完成加载,也可以自定义自己的类加载器来完成加载.

      加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,而且在java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据

    • 连接

      • 验证:确保被加载的类的正确性
        验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.验证阶段大致会完成4个阶段的检验动作:

      文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以OxCAFEBABE开头,主次版本号是否在当前虚拟机的处理范围之内,常量池中的常量是否有不被支持的类型.

      元数据验证:对字节码描述的信息进行语文分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外

      字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的

      符号引用验证:确保解析动作能正确执行

    验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短类加载的时间

    -准备: 为类的静态变量分配内存,并将其初始化为默认值
    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,对于该阶段有以下几点需要注意:
    1. 这时候进行内存分配的只包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中.
    2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0,0L,null,false等),而不是被在java代码中被显示的赋予的值.
    假设一个类变量的定义为:public static int value = 3;
    那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何java方法,而把value赋值为3的putstatic指令是在程序编译后,存放在类构造器()方法之中,所以把value赋值为3的动作将在初始化阶段才会执行.

    • 这里还需要注意几点:

    • 对于基本数据类型来说,对于类变量(static)和全局变量,如果不显示地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用之前必须显示的为期赋值,否则编译时不通过.

    • 对于同时被static和final修饰的常量,必须在声明的时候就为其显示的赋值,否则编译时不通过;而只是被final修饰的常量则既可以在声明时为其显示的赋值,也可以在初始化时为其显示的为其赋值,总之,在使用前必须为其显示的赋值,系统不会为其赋予默认零值

    • 对于引用数据类型reference来说,如数组引用,对象引用等,如果没有对其进行显式的赋值而直接使用,系统都会为其赋予默认的零值,即null

    • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值

      1. 如果类字段的字段属性表中存在 ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value机会被初始化为ConstValue属性所指定的值

      假设上面的类变量value被定义为:public static final int value = 3;

      编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3.

    • 解析:把类中的符号引用转化为直接引用
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或者接口,字段,类方法,方法类型,方法句柄和调用点限定符7类符号引用类型.符号引用就是一组符号来描述目标,可以是任何字面量.
      直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄

    • 初始化
      为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要类变量进行初始化.在java中对类变量进行初始值设定有两种方式:

      1. 声明类变量时指定初始值
      2. 使用静态代码块为类变量指定初始值
        JVM初始化步骤
    1. 假如这个类还没有被加载或者连接,则程序先加载或连接该类
    2. 假如该类的直接父类还没有没初始化,则先初始化其父类
    3. 假如类中有初始化语句,则系统依次执行这些初始化语句
      类初始化的时机:只有对类主动使用的时候才会导致类的初始化,类的主动使用包括以下6种:
      - 创建类的实例,也就是new的方式
      - 访问某个类或接口的静态变量,或者对该静态变量赋值
      - 调用类的静态方法
      - 反射(如Class.forName)
      - 初始化某个类的子类,其父类也会被初始化
      - java虚拟机启动时被标明为启动类的类(java test),直接时候java.exe命令来运行某个类的主类
      结束生命周期
      在如下几种情况下,java虚拟机将结束生命周期
      - 执行了system.exit()
      - 程序正常执行结束
      - 程序在执行过程中遇到了异常或错误而异常终止
      - 由于操作系统出现错误而导致虚拟机进程终止

    3. 类加载器

          public class ClassLoaderTEST{
        	public static void main(String[] args) {
        		ClassLoader loader = Thread.currentThread().getContextClassLoader();
        		System.out.println(loader);
        		System.out.println(loader.getParent());
        		System.out.println(loader.getParent.getParent);
        	}
        }
    
    运行结果为:  
    
    	sun.misc.Launcher$AppClassLoader@64fef26a
    	sun.misc.Launcher$ExtClassLoader@1ddd40f3
    	null 
    

    从结果可以看出.并没有获取到ExtClassLoader的付父Loader,原因是Bootstrap Loader(引导类加载器)使用C语言写的,找不到一个确定的返回Loader的方式,于是就返回null.
    这几种类加载器的层次关系如下图:

    注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的.
    站在java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有许多其他的虚拟机是用java语言写的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.classLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他类.
    站在java开发人员的角度来讲,类加载器可以大致划分为三类:

    启动类加载器:负责加载存放在JDK/jre/lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如:rt.jar,所有的java.开头的类均被Bootstarp ClassLoader加载).启动类加载器是无法被java程序直接引用的.
    扩展类加载器:该加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载JDK/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如java.
    开头的类),开发者可以直接使用扩展类的加载器.
    应用程序加载器:该类加载器由sun.misc.Launcher$AppClassLoader来实现,他负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器.

    应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们可以加入自定义的类加载器.因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

    • 在执行非置信代码之前,自动验证数字签名.
    • 动态地创建符合用户特定需要的定制化构建类.
    • 从特定的场所取得class,例如数据库中和网络中

    JVM类加载机制:

    • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用过的其他Class也将由该类加载器负责载入,除非显示使用另外一个类的加载来载入
    • 父类委托,先让父类加载器视图加载该类,只有在父类加载器无法加载该类的时候才尝试从自己的路径中加载该类
    • 缓存机制,缓存将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中寻找该Class,只有缓存区不存在,系统才会读取该类的二进制数据,并将其转换成Class对象,存入缓存区,这就是为什么为什么修改了Class后,必须重启JVM,程序的修改才会生效.

    4. 类的加载

    类加载有三种方式:

    • 命令行启动应用程序的时候由JVM初始化加载
    • 通过Class.forName()加载
    • 通过ClassLoader.loaderClass()方法来加载
      例:
    public class loaderClass{
    	public static void main(String[] args) throws ClassNotFoundException{
    		ClassLoader loader = HelloWorld.class.getClassLoader();
    		System.out.println(loader);
    		//使用ClassLoader.loadClass()来加载类,不会执行初始代码块
    		loader.loadClass("Test2");
    		//使用Class.forName来加载类,默认会执行初始块
    		Class.forName("Test2");
    		//使用Class.forName来加载类,并指定Classloader,初始化时不执行静态块
    		Class.forName("Test2",false,loader);
    	}
    }
    

    demo类

    public class Test2{
    	static{
    			System.out.println("静态初始化块执行了");
    }
    

    分别切换不同的加载方式,会出现不同的结果.

    Class.forName()和ClassLoader.loadClass()区别

    • class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static代码块
    • classLoader.loadClass():只干一件事,就是将.class文件加载到虚拟机中,不会执行static中的内容,只有在newInstance才会执行static块
    • Class.forName(name,initialize,loader)带参函数也可控制是否加载static块.并且只有调用newKInstance()调用构造函数,创建类对象.

    5. 双亲委派模型

    双亲委派模型的流程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的类加载器中,只有在父类加载器在它的搜索范围中没有找到所需要的类时,即无法完成该加载,子加载器才会尝试自己去加载该类

    双亲委派机制:

    • 1.当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成.
    • 2.ExtClassLoader加载一个class时,它首先也不会尝试自己加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成.
    • 3.如果BootStrapClassLoader加载失败(例如$JAVA_HOME/jre/lib里未查到该class,会使用ExtClassLoader来尝试加载)
    • 4.若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出ClassNotFoundException

    ClassLoader源码分析:

    public Class<?> loadClass(String name) throws ClassNotFoundException{
    	return loadClass(name,false);
    }
    
    protected syschronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    	//先判断该类型是否已经被加载
    	Class c = findLoadedClass(name);
    	if(c == null){
    		//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
    		try{
    			if (parent != null) {
    				//如果存在父类加载器,就委派给父类加载器加载
    				c = parent.loadClass(name,false);
    			}else{
    				//如果不存在父类加载器,就检查是否由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
    				c = findBootstrapClass0(name);
    			}
    		}catch(ClassNotFoundException e){
    			//如果父类加载器和启动类加载器都不能完成加载任务,才调用自身加载功能
    			c = findClass(name);
    		}
    		if (resolve) {
    			resolveClass(c);
    		}
    		return c;
    	}
    }
    

    6. 自定义类加载器

    自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法分析来看,我们只需要重写findClass方法即可.

    public class MyClassLoader extends ClassLoader{
    	private String root;
    
    	protected Class<?> findClass(String name) throws ClassNotFoundException{
    		byte[] classData = loadClassData(name);
    		if (classData == null) {
    			throws new ClassNotFoundException();
    		}else{
    			return defineClass(name,classData,0,classData.length);
    		}
    	}
    
    	private byte[] loadClassData(String className){
    	String fileName = root + File.separatorChar + className.replace('.',File.separatorChar) + ".class";
    	try{
    		InputStream ins = new FileInputStream(fileName);
    		ByteArraryOutputStream baos = new ByteArraryOutputStream();
    		int bufferSize = 1024;
    		byte[] buffer =  new byte[bufferSize];
    		int length = 0;
    		while((length = ins.read(buffer)) != -1){
    			baos.write(buffer,0,length);
    		}
    		return baos.toByteArray();
    	}catch(IOException e){
    		e.printStackTrace();
    	}
    	return null;
    	}
    
    	public String getRoot(){
    		return root;
    	}
    
    	public void setRoot{
    		this.root = root;
    	}
    
    	public static void main(String[] args) {
    		MyClassLoader classLoader =new MyClassLoader();
    		classLoader.setRoot("E:\temp");
    
    		Class<?> testClass = null;
    		try{
    			testClass classLoader.loadClass("com.neo.classLoader.Test2");
    			Object object = testClass.newInstance();
    			System.out.println(object.getClass().getClassLoader());
    		}catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    	}
    }
    

    自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密,这里有几点需要注意.

    • 这里传递的文件名需要的类的全限定名,因为defineClass方法是按这种格式进行处理的
    • 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式
    • 这类Test类本身可以被AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class类路径下.否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义的类加载器.
  • 相关阅读:
    Quick Sort
    Binary Search
    trollcave解题
    Openvas简介
    SMB扫描-Server Message Block 协议、nmap
    漏洞基本概念
    防火墙识别、负载均衡识别、waf识别
    Centos7下部署Python项目
    Python-Redis数据类型操作
    MySQL的事务隔离级别
  • 原文地址:https://www.cnblogs.com/zddsblog/p/7543275.html
Copyright © 2011-2022 走看看