本文已参与「新人创作礼」活动,一起开启掘金创作之路
类的加载机制
主要关注点:
- 什么是类的加载
- 类的生命周期
- 类加载器
- 双亲委派模型
什么是类的加载
- java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
- 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,通过这个class对象,就能访问二进制数据文件二进制码,然后将它翻译成java指令。
类的生命周期
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;
- 加载,将类的二进制数据加载进内存当中,并且在Java堆中也创建一个java.lang.Class类的对象( 任何一个类被加载后都会生成两个文件,一个是类的二进制数据文件存放在方法区的实现元空间中(metaSpace)、一个是该类的Class类对象存放在堆中,通过该类对象就能访问二进制数据文件二进制码,然后将它翻译成java指令 )
- 连接,把类的二进制数据合并到JRE中,又包含三块内容:验证、准备、解析。
-
- 1)验证,检查载入Class文件数据的正确性 ,比如加载的class文件前面四个字节不是CAFE、BABE,就不会让他加载, 为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全 ( 文件格式、元数据、字节码、符号引用验证);
- 2)准备,准备阶段是正式为类变量分配内存并设置类变量初始值的阶段, 但是普通成员变量还没 ( 至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue 时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0 ), 准备阶段只对static类变量分配内存和设置初始值非static的类变量到创建对象执行init方法时才分配空间进行初始化
- 3)解析,把类中的符号引用转换为直接引用
- 初始化,为类变量(包括类静态变量和普通成员变量,如果普通成员变量没有设置值则初始化为默认值,如果已经设置了,则初始化为设置的值)赋予正确的初始值,执行静态方法
- 使用,new出对象程序中使用
- 卸载,执行垃圾回收
- 总结:
-
- 类的生命周期可以比喻为女娲造人:
-
-
- 类加载阶段:赋予了人类的概念,有鼻子有眼有性别等,只是个雏形
- 使用阶段:人的雏形模版定义好了,可以准备的造一个人了,赋予他灵魂,这时候人就有了具体的名字,特征等
- 卸载:理解为人死亡了,要被拉到火葬场
-
为什么java中静态方法不能调用非静态方法和变量
- 静态方法是属于类的,动态方法属于实例对象,静态方法在类加载的时候就会分配内存,可以 通过类名直接去访问,非静态成员(变量和方法)属于类的对象,所以只有该对象初始化之后才存在,然后通过类的对象去访问。
- 也就是说如果我们在静态方法中调用非静态成员变量会超前,可能会调用了一个还未初始化的变量。因此编译器会报错。
几个小问题?
1、JVM初始化步骤 ? 2、类初始化时机 ?3、哪几种情况下,Java虚拟机将结束生命周期?
答案参考这篇文章jvm系列(一):java类的加载机制
类加载器
Extension和Application加载器,其实是Launcher类的内部类,他们对应加载的文件都是在Launcher定义的。
- 启动类加载器:Bootstrap ClassLoader,由C++实现,加载jdk的核心类库,比如String。 负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
-
- String类是由Bootstrap类加载器加载的,返回的是个null,是因为Bootstrap类加载器是由C++来实现的,java里面并没有一个class和它直接对应,所以返回null,所以当我们看到null值的时候,代表类加载器已经到头了
- 扩展类加载器:Extension ClassLoader,加载ext包下的类。 该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,加载自己写的类。该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
\
双亲委派机制
- 当我们通过自定义类加载器加一个类的时候,会先去自定义类加载器的缓存当中找(如果已经加载过一遍了就会存到缓存当中),如果从缓存中找到了就直接返回,没找到就委托父加载器找。
- 应用类加载器收到委托后去它的缓存当中找,找到就返回,没找到就委托它的父加载器
- 扩展类加载器收到委托后去它的缓存当中找,找到就返回,没找到就委托它的父加载器
- 启动类加载器收到委托后去它的缓存当中找,找到就返回,没找到就委派它的子加载器去寻找class文件并加载
- 扩展类加载器收到委派命令后尝试去加载,找到就返回,没找到就委派它的子加载器去寻找class文件并加载
- 应用类加载器收到委派命令后尝试去加载,找到就返回,没找到就委派它的子加载器去寻找class文件并加载
- 自定义加载器收到委派命令后尝试去加载,找到就返回,没找到就报错(classnotfound)
这些加载器并不是实际意义上的继承关系,也就是父加载器并不是它的父类,只是它自己的上一层加载器的意思
双亲委派的意义
出于安全考虑,加载器只能加载自己对应的类,假如你自定义了一个包并且建了一个java.lang.String类,如果没有双亲委派机制,那么我把这个自定义String交给自定义的类加载器,加载到内存直接把JDK的String给替换掉,然后将整个的这些打包成一个类库,交给客户使用,客户的密码肯定都是String类型的,这时候因为替换了使用的就是我们自定义的String,我们可以通过自定义String获取到用户的密码。但是使用了双亲委派机制后,当我自定义了一个String,并且准备用自定义类加载器去加载的时候,就会先去父加载器去加载,父加载器加载到String这个类后就会直接返回,这样我们自定义的String类就无法使用了。
自定义类加载器
继承ClassLoader,重写findClass方法。再将自己需要加载的类转换为二进制数组,通过defineClass方法生成Class对象
根据双亲委派机制,当父加载器都加载不到这个对象的时候,就会轮到自己去加载,也就是执行执行findClass方法
如何打破双亲委派机制
重写loadClass方法
类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效