面试必问:JVM的类加载机制
共 3476字,需浏览 7分钟
·
2020-12-16 18:22
1 类加载过程
想要使用一个类,首先需要将其加载到JVM中,类加载到JVM需要经过三个步骤:加载->链接->初始化。其中链接又分为验证,准备,解析三步。
1.1 加载
类加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法这个类的各个数据的入口。注意相关信息不是一定要从Class文件中获取,它即可从ZIP中读取(如从Jar包,War包中读取),也可以在运算时生成(动态代理),也可以由其它文件生成(如将JSP文件转换成对应的Class类)。
1.2 链接
链接过程分为验证,准备,解析三步。
1.2.1 验证
JAVA是一种相对安全的语言,验证的意义是为了确保Class文件的字节流中包含的信息符合当前虚拟机要求。不会危害到虚拟机的安全。验证主要包含文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证:验证字节流是否符合Class文件格式规范,并且能被当前JVM加载处理。如常量类型是否支持。
元数据验证:字节码信息进行语言分析,分析是否符合java语言规范。
字节码验证:最重要的验证环节,元数据验证后对方法体验证,保证类方法在运行时不会有危害发生。
符号引用验证:验证符号引用,保证能够访问到,不会出现无法访问的情况。
1.2.2 准备
准备阶段正式对类变量分配内存,并设置初始默认值。如定义 private static int s = 2020;经过此阶段s=0,而不是2020.但是如果定义为private final static int s = 2020,在准备阶段会直接将s设置成2020而不是初始值0;
1.2.3 解析
解析阶段JVM将常量池中的符号引用替换为直接引用。准备阶段只是分配了内存,但是类变量并没有指向那一块内存,这一步就是完成实际指向的工作。
1.3 初始化
初始化阶段为类变量设置正确的初始值。如上文 private static int s = 2020中将s赋值2020的动作便在这一步完成。同时初始化阶段也会执行静态代码块。如果有超类,则先对超类执行初始化。
初始化阶段是执行类构造器
以下几种情况不会触发初始化:
子类引用父类的静态变量,只会触发父类的初始化,不会触发子类的初始化
定义对象数组,不会触发初始化
常量在编译过程中直接存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类的初始化。如A引用B类的常量final static x = 3。此时不会对B进行初始化。
通过类名获取Class对象,不会触发类的初始化。
通过Class.forName加载指定类时,如果initalize为false,不会触发初始化。
通过ClassLoader默认的loadClss方法,也不会触发初始化。
类加载的动作由类加载器完成。对于JVM来说,类的唯一性是通过类的全限定名+类加载器来区分的。不同的类加载器加载的同一个类并不被认为是同一类。如有一个类C,分别用类加载器CL1,CL2加载。一个参数需要CL1的C实例,传入的确是CL2的C实例,就会报错java.lang.ClassCastException:C cannot be cast to C。
JVM提供了三种类加载器:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader).
启动类加载器:用来加载Java的核心库,主要加载的是JVM自身所需要的类,使用C++实现,并非继承于java.lang.ClassLoader,是JVM的一部分。负责加载JAVA_HOME\lib目录中的,或者-Xbootclasspath参数指定的路径中的,且被虚拟机认可[注1]的类。开发者无法直接获取到其引用。
注1:JVM是按文件名识别的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包放在lib目录下也没有作用,同时启动加载器只加载包名为java,javax,sun等开头的类。且java是特殊包名,开发者的包名不能以java开头,如果自定义了一个java.***包来让类加载器加载,那么就会抛出异常java.lang.SecurityException: Prohibited package name: java.***扩展类加载器:用来加载Java的扩展库。负责加载JAVA_HOME\lib\ext目录中的,或通过系统变量java.ext.dirs指定路径中的类库。由java语言实现。开发者可以直接使用。
应用程序类加载器:负责加载用户路径(classpath)上的类库。开发者可以直接使用。可以通过ClassLoader.getSystemClassLoader()获得。一般情况下程序的默认类加载器就是该加载器。
除了提供的加载器外,开发者可以通过继承ClassLoader类的方式实现自己的类加载器。
JVM的类加载机制为双亲委派机制,除了顶层的启动类加载器,双亲委派机制要求每一个类加载器都要有自己的父加载器。这个父加载器并不是指继承,而是一种委派关系。双亲委派机制下一个类加载器收到类加载请求不会直接自己去加载,而是先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,则继续委托,一直委托到最顶层的启动类加载器。父加载器可以加载目标类的话就由父加载器完成加载任务,如果父加载器无法完成则子加载器自己尝试加载。这就是双亲委派机制。
双亲委派机制的优势:双亲委派机制使得java类随着他的类加载器具备了一种带有带有优先级的层级关系。通过这种层级关系可以避免类的的重复加载,当父加载器已经加载过目标类时,子加载器无需重复加载一次。
其次是安全方面,通过双亲委派机制可以避免核心类不会被随意替换,例如网络传递一个名为java.lang.Integer的类,在双亲委派机制下,加载请求会被传递到顶层的启动类加载器,启动类加载器发现这个名字的类已经加载过了,就会直接返回已经加载过的Integer.class。这样就可以防止核心API被修改。
OSGI(Open Service Gateway Initiative)是面向java的动态模型系统。OSGI能够提供无需重启的动态改造功能,基于OSGI的程序很可能可以实现模块级的热插拔功能,当程序进行升级时,只需停用,重新安装然后启动程序的一部分。但并非所有的程序都适合OSGI架构,其在提供强大功能的同时也提高了复杂度,因为OSGI不支持双亲委派机制。
eclipse就是基于OSGI技术来构建的。
OSGI每个模块都自己的类,每个模块可以声明其需要模块的类(导入),也可以声明自己的类以供其它模块使用(导出),每个模块都有自己的类加载器,他负责加载本模块的类,对于非本模块的类:核心库的类会代理给父类加载器(通常是启动类加载器),其他模块导入的类则代理给对应模块由其加载。对于java开头的类,默认都是由父类加载器完成的,可以通过org.osgi.framework.bootdelegation设置某些包或者类必须由父类加载器加载。如设置org.osgi.framework.bootdelegation = com.my.* 此时com.my下的所有类都由父类加载器进行加载
例如由两个模块:M-A和M-B,分别有类C-A和C-B,C-A继承自C-B,M-A启动时,对C-A进行加载,因为继承关系继而需要加载C-B,此时由于M-A声明了C-B是由M-B导入的,那么就会将C-B的加载交由M-B执行,M-B对C-B进行加载,所得的类实例可以被所有声明导入此类的模块使用。
本文链接:https://blog.csdn.net/yue_hu/article/details/109622483
end
*版权声明:转载文章和图片均来自公开网络,版权归作者本人所有,推送文章除非无法确认,我们都会注明作者和来源。如果出处有误或侵犯到原作者权益,请与我们联系删除或授权事宜。
长按识别图中二维码
关注获取更多资讯
不点关注,我们哪来故事?
点个再看,你最好看