今天搞一下jvm的运行原理,类的加载机制。
java的代码是运行在java虚拟机上的,首先我们会把java代码打包成虚拟机可识别的.class文件,然后类加载器把“.class”字节码文件中的类给加载到JVM中,接着是JVM来执行我们写好的那些类中的代码,整体是这么个顺序。
jvm的类加载机制分为 加载->验证->准备->解析->初始化->使用->卸载
验证,准备,解析阶段:
验证阶段:这一步主要是为了把加载到内存中的.class文件进行校验,看是否符合一些java虚拟机的规范,如果字节码文件中有不符合规范的代码,那么jvm是没法执行这个字节码文件的。主要的校验有文件格式校验,元数据验证,字节码验证,符号引用验证等。
准备阶段:这个阶段主要是做了对类中的变量进行初始化赋值,分配内存空间。这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为: public static int value = 12;那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
解析阶段:虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。类、接口的解析 ,字段解析 ,类方法解析 ,接口方法解析。
初始化阶段:
在类的加载机制中,初始化是核心阶段。这个阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。也就是说,我们在准备阶段,有一个int类型变量,这个时候我们给他赋值0并且分配内存空间,但是在初始化阶段会给他重新赋值为程序指定的值,比如 public static int a=5,准备阶段给a分配空间并赋值0,然后再初始化阶段赋值为5。还有一个非常重要的规则,如果类加载的时候,发现父类还未初始化,那么会先去初始化父类。
到这里可以说,类的加载已经完事了,剩下的就是使用,卸载。
类的加载与双亲委派机制:
现在相信大家都搞明白了整个类加载从触发时机到初始化的过程了,接着给大家说一下类加载器的概念。因为实现上述过程,那必须是依靠类加载器来实现的。
那么Java里有哪些类加载器呢?简单来说有下面几种:
(1)启动类加载器
Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的
相信大家都知道,如果你要在一个机器上运行自己写好的Java系统,无论是windows笔记本,还是linux服务器,是不是都得装一下JDK?
那么在你的Java安装目录下,就有一个“lib”目录,大家可以自己去找找看,这里就有Java最核心的一些类库,支撑你的Java系统的运行。
所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。
(2)扩展类加载器
Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录
这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。
那么你的JVM一旦启动,是不是也得从Java安装目录下,加载这个“lib\ext”目录中的类?
(3)应用程序类加载器
Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类
其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。
(4)自定义类加载器
除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。
(5)双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
大家看下图:
然后,基于这个亲子层级结构,就有一个双亲委派的机制
什么意思呢?
就是假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载
但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
听完了上面一大堆绕口令,是不是很迷茫?别着急,咱们用一个例子来说明一下。
比如你的JVM现在需要加载“ReplicaManager”类,此时应用程序类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到这个类吗?
然后扩展类加载器直接问自己的爸爸,启动类加载器,你能加载到这个类吗?
启动类加载器心想,我在Java安装目录下,没找到这个类啊,自己找去!
然后,就下推加载权利给扩展类加载器这个儿子,结果扩展类加载器找了半天,也没找到自己负责的目录中有这个类。
这时他很生气,说:明明就是你应用程序加载器自己负责的,你自己找去。
然后应用程序类加载器在自己负责的范围内,比如就是你写好的那个系统打包成的jar包吧,一下子发现,就在这里!然后就自己把这个类加载到内存里去了。
这就是所谓的双亲委派模型:先找父亲去加载,不行的话再由儿子来加载。
这样的话,可以避免多层级的加载器结构重复加载某些类。
最后,给大家来一张图,感受一下类加载器的双亲委派模型。
参考 狸猫技术窝
网友评论