Java虚拟机重点之类加载机制
虚拟机类加载机制
Java程序运行机制步骤
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
一、类加载的时机
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。请注意,这里是按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
什么时候加载,由虚拟机决定,但是,初始化则是有要求。有且仅有以下6种情况。
- 1.遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。具体情况分以下3种:a.使用new关键字实例化对象的时候。b.读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。c.调用一个类型的静态方法的时候。
- 2.使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
- 3.当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 5.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 6.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。(java8新增!!!!)
这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。比如:引用某个类的final定义的常量,则不会触发该类的初始化。或者你访问一个静态字段,也不会导致某个类初始化。
二、类加载的过程
1.加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
也就是把对应的.class文件拿过来,放到该放到的地方,然后等待后续处理。
2.验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3.准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。(永生代是和新生代和老年代一起的说法,生命周期不同)
元空间是方法区的在HotSpot jvm 1.8中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。1.7的时候用永久代实现的。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
这里的静态变量也只是划分区域,还没有你要的赋值,而是赋值为0即初始值。而对于常量,在编译器就会创建常量的空间(final),而在准备阶段(本阶段)就进行了赋值.
4.解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。(就好比,你用文字表示,a节点和b节点是相连的。而我真的就用用根线把两个节点连起来了)
主要有:1.类或接口的解析。2.字段解析。3.类方法解析。4.接口方法解析
5.初始化
之前都是准备工作。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码。初始化阶段就是执行类构造器clinit()方法的过程。clinit()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
初始化根据语句的顺序执行,并且有父类没初始化会先初始化父类。clinit方法是加锁了的方法
三、类加载器
1.类与类加载器
每个都有个自己专门的类加载器。只要加载某两个类的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
2.双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
!!!!总结:注意上面的图,其实正常情况下,我们写的类都是由应用程序类加载器加载的,除非你自己写自定义类加载器。一个类只有一个类加载器。所以虚拟机中是允许出现多个同名的类,但是是由不同的类加载器加载的,所以这里必须是你手动写一个类加载器,也就是用你自己的方法加载,否则只用系统中的类加载器,是不能有多个同名的类的(系统加载一个,你加载一个)。使用双亲,是为了更好的分层,启动是启动类,扩展是扩展类,应用程序是应用程序类,并且启动类是最高级的,该有的层次感都很强。还有种的理解:这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。(这样就是,系统类早就加载了,你要加载就不行,就只能用你自己的类加载器,而你自己的又是子类,优先级不高,所以父类先加载,父类就把源码加载,这样你就改不了源码)
3.破坏双亲委派
历史上有3次破坏了。
第1次破坏,刚开始大家都是自己写加载器的,为了兼容这些部分,才破坏了。解决方式:为了兼容这些已有代码,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。1.2之前都是重写loadClass(),后来就建议重写findClass()。
第2次破坏:双亲委派是有缺陷的,虽然双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,就出问题了。(如JDBC.解决方法是弄一个上下文类加载器,父类去请求子类的加载器去加载,这样就破坏了双亲委派)
第3次破坏:是由于用户对程序动态性的追求而导致的。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故。使用的技术是OSGi。