标签:就是 java代码 间接 tail 抽象类 类型 完成 final 方式
在Java中,类加载都是在运行期间执行的,这种策略虽然令类加载稍微增加一些性能,但是会给java应用程序提供高度的灵活性。
类加载的过程
和其他语言一样,java编译器同样能够将.java文件编译成.class,但是对于JVM来讲,它并不关心,是哪种语言经过编译形成的。
JVM类加载工作原理:就是把类的class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成被虚拟机使用的java类型。
类加载的生命周期包括以下几个部分:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证、准备、解析三个部分统称链接。
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类加载过程必须按照这种顺序开始,而解析阶段不一定,它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性(JIT例如接口只在调用的时候才知道具体的实现的是哪个子类)。值得注意的是:这些阶段通常都是交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另一个阶段。
加载
加载这个阶段通常也被称为“装载”,它的主要任务主要有一下几点:
1、通过“类全名”来获取定义此类的二进制字节流
2、将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3、在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
相对于类加载过程的其他阶段,加载阶段是通过类加载(ClassLoader)来完成的,而类加载器也可以由用户自定义完成,因此,开发人员可以通过定义类加载器去控制字节流的获取方式。加载之后,二进制文件会被读入到虚拟机所需的格式存储在方法区中,方法区中存储格式由虚拟机自行定义,然后在java堆中实例化一个java.lang.Class类对象,通过这个对象就可以访问方法区中的数据。
验证
验证阶段是链接阶段的第一步,目的就是确保class文件的字节流中包含的信息符合虚拟机的要求,不能危害虚拟机自身安全。验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1、文件格式验证
验证class文件格式规范
2、元数据验证
就是对字节码描述的信息进行语义分析,保证描述的信息符合java语言规范。验证点可能包括(这个类是否有父类(除Object)、这个类是否继承了不允许被继承的类(final修饰的)、如果这个类的父类是抽象类,是否实现了父类或接口中要求实现的方法)。
3、字节码验证
进行数据流和控制流分析,这个阶段对类的方法体进行校验,保证被校验的方法在运行时不会做出危害虚拟机的行为。
4、符号引用验证
符号引用中通过字符串描述的权限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private protected public default)是否能被当前类访问。
准备
这个阶段就是为类变量分配内存并设置类变量初始值的阶段,这些内存将在方法区中进行分配。要注意的是,进行分配内存的只是包括类变量,而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。通常情况下,初始值为零值,假设public static int value=2;那么value在准备阶段过后的初始值为0,不为2,这时候只是开辟了内存空间,并没有运行java代码,value赋值为2的指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以value被赋值为2是在初始化阶段才会执行。对于一些特殊情况,如果类字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ContstantValue属性所指的值,那么对于上面value,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue将value设置为2
解析
解析阶段是虚拟机常量池的符号引用替换为直接引用的过程。
符号引用:符号引用是一组符号来描述所引用的对象,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄(一种特殊的智能指针)。直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。
解析过程主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
初始化
类的初始化阶段是加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度表达:初始化阶段是执行类构造器<clinit>()方法的过程。在以下四种情况下初始化过程会被触发执行:
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化。
4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。
*类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。
*类构造器<clinit>()方法与类的构造函数(实例构造函数<init>()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中的第一个执行的<clinit>()方法的类肯定是java.lang.Object。
*由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。
*<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成<clinit>()方法。
*接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
*虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞。
类加载器
JVM设计者把类加载阶段中的通过类全名来获取此类的二进制字节流这个动作放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类。实现这个动作的代码模块成为“类加载器”。
类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类来确定其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。
双亲委派模型
从虚拟机的角度来说,有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该加载器使用C++语言实现,属于虚拟机自身的一部分。另一部分就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承抽象类java.lang.ClassLoader.
从java开发人员的角度看,大部分java程序会用到以下三种系统提供的类加载器:
1、启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库加载到JVM内存中,如果名称不符合的类库即使在lib目录中也不会被加载。该类加载器无法被java程序直接引用。
2、扩展类加载器(Extension ClassLoader):该加载器主要负责加载JAVA_HOME\lib\ext目录中的类库,开发者可以使用扩展加载器。
3、应用程序类加载器(Application ClassLoader):该列加载器也称为系统加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
当然除了以上三种类加载器,我们还能自己定义类加载器。这些类加载器之间的关系如下。
上面的这种模型,就称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器不是以继承的关系来实现,而是通过组合关系来复用父加载器的代码。
双亲委派模型的工作过程为:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。
双亲委派模型的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:Object,无论那个类加载器去加载该类,最终都是由启动类加载器进行加载的,因此Object类在程序的各种类加载环境中都是一个类。如果不用改模型,那么java.lang.Object类存放在classpath中,那么系统中就会出现多个Object类,程序变得很混乱。
类加载java.lang.ClassLoader类中,有一个loadClass方法源码如下:
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先检查该name指定的class是否有被加载 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 如果parent不为null,则调用parent的loadClass进行加载 c = parent.loadClass(name, false); } else { // parent为null,则调用BootstrapClassLoader进行加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果仍然无法加载成功,则调用自身的findClass进行加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
从源代码中,我们不难看出,先检查是否已经加载,如果没有,就调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出ClassNotFountException,然后再调用findClass()方法加载。
自定义类加载器
若要实现自定义类加载器,只需要继承java.lang.ClassLoader类,并且重写findClass()方法即可。java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成对应的字节码,然后从这些字节码中定义出一个Java类,即java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等,ClassLoader中与加载类相关的方法如下:
方法 说明
getParent() 返回该类加载器的父类加载器
loadClass(String name) 加载名称为二进制名称为name的类,返回的结果是java.lang.Class类的实例。
findClass(String name) 查找名称为name的类,返回的结果是java.lang.Class类的实例。
findLoaderClass(String name) 查找名称为name的已经被加载过的类,返回的结果是java.lang.Class类的实例。
resolveClass(Class<?> c) 链接指定的Java类。
---------------------
作者:A coding monkey
来源:CSDN
原文:https://blog.csdn.net/qq_36795474/article/details/79439206
版权声明:本文为博主原创文章,转载请附上博文链接!
标签:就是 java代码 间接 tail 抽象类 类型 完成 final 方式
原文地址:https://www.cnblogs.com/yichenscc/p/10359180.html