一篇文章带你深入了解Java类加载
作者:Serendipity sn
1.类加载
<1>.父子类执行的顺序
1.父类的静态变量和静态代码块(书写顺序)
2.子类的静态变量和静态代码块(书写顺序)
3.父类的实例代码块(书写顺序)
4.父类的成员变量和构造方法
5.子类的实例代码块
6.子类的成员变量和构造方法
<2>类加载的时机
如果类没有进行初始化,则需要先进行初始化,虚拟机规范则是严格规定有且只有5种情况必须先对类进行初始化(而加载,验证,准备要在这个之前开始)
1.创建类的实例(new的方式),访问某个类的静态变量,或者对该静态变量赋值,调用类的静态方法
2.反射的方式
3.初始化某个类的子类,则其父类也会被初始化
4.java虚拟机启动时被标记为启动类的类,直接使用java.exe来运行的某个主类(如main类)
5.使用jdk1.7的动态语言支持时
<3>类的生命周期
七个阶段:加载,验证,准备,解析,初始化,使用和卸载。其中验证,准备和解析三个部分被称为连接
解析阶段在某些情况下可以在初始化阶段之后再进行,这是为了支持java语言的运行时绑定(动态绑定)
<4>类加载的过程
接下来我们详细讲解一下Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
1.加载
<1>通过一个类的全限定名来获取定义此类的二进制字节流。
<2>将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
<3>在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
4.解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经在内存中存在。
5.初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。
了解:
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问:
public class Test{ static{ i=0; //给变量赋值可以正常编译通过 System.out.print(i); //这句编译器会提示"非法向前引用" } static int i=1; }
1.()方法(Class类的构造方法)与类的构造函数(或者说实例构造器()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
2.()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。
3.接口中定义的变量使用时,接口才会初始化:接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
4.虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
<5>类加载器
类加载器可以分为:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。他们的关系一般如下:
1.启动类加载器(BootstrapClassLoader)
这个类由C++语言实现,是虚拟机自身的一部分,并不继承ClassLoader,不能操作它。用来加载Java的核心类。
2.扩展类加载器(ExtClassLoader)
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
3.应用程序类加载器(AppClassLoader)
它负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。
4.2 自定义加载器
用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器
<6>类加载机制——双亲委派模型
双亲委派模型的过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求信息最终都会传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(即它的搜索范围没有找到所需要的类)时,子加载器才会尝试自己去完成加载
先查找,再进行加载
(1)从下往上找
(2)从上往下加载
双亲委派模型的好处:双亲委派模型对于java程序的稳定运行极为重要
劣势:无法满足灵活的类加载方式。(解决方案:自己重写loadClass破坏双亲委派模型 例如SPI机制)
总结
本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注脚本之家的更多内容!