JVM类加载机制
关于JVM的类加载机制,可以理解为:把编译完成的 .class文件,从文件硬盘加载到内存中(元数据区)这样的一个过程,最终要获取到类对象。这个过程,可以大致理解为以下几个步骤:
加载:把 .class 文件找到,打开文件,读文件,把文件内容读到内存中;
验证:检查 .class 文件格式对不对,.class文件是一个二进制文件,此处的检查是根据官方提供的 JVM 虚拟机规范,它会说明描述 .class 的规范;
准备:给类对象分配内存空间(在元数据区占用一个空间) ,内存初始化成全0,为类中定义的静态变量分配内存,并设置初始值也为0;
解析: 初始化字符串常量,把符号引用转成直接引用;
符号引用转成直接引用:对于字符串常量来说,是需要有一块内存空间,来存这个字符的实际内容的,还需要有一个引用,来保存这个内存空间的起始地址,而在类加载过程中,字符串常量是存储在 .class 文件中的,而不是内存上的,此时的这个 "引用" 记录的不是字符串常量的真正地址,而是它在文件中的一个偏移量,这个偏移量就理解为 "符号引用";
在类加载之后,才真正的把这个字符串常量放在内存中,此时才是真正拥有内存地址,此时的内存地址就理解为 "直接引用" ;
初始化: 针对类对象里面的内容进行初始化,加载父类,执行父类静态代码块,父类静态变量赋值,子类静态代码块,子类静态变量赋值,父类初始化模块,父类普通变量,父类构造器,子类初始化模块,子类普通变量,子类构造器...
一个类什么时候会被加载呢?
并不是Java程序一运行,就把所有的类都加载了,而是等到了真正用到的时候,才会去加载,属于是懒汉模式,一般会有以下三种情况:
- 构造类的实例;
- 调用了这个类的静态方法,或者使用到了这个类的静态属性;
- 加载子类的时候,通常会先加载其父类;
一般是用到了才会进行类加载,并且加载过一次后,后续再使用也不需要进行重复加载了;
双亲委派模型
双亲委派模型,描述的是JVM类加载机制中的 加载 过程,也就是找 .class 文件的过程;
JVM 默认提供了三个类加载器:
- BootstrapClassLoader:负责加载标准库中的类;
- ExtensionClassLoader:负责加载 JVM 扩展库中的类;
- ApplicationClassLoader: 负责加载用户提供的,也就是用户代码中的类;
在这三个类加载器中,存在一个 "父子关系" --- 每个 ClassLoader 类中会有一个 parent 属性来标识自己的父加载器:
ApplicationClassLoader 的 parent 是 ExtensionClassLoader;
ExtensionClassLoader 的 parent 是 BootstrapClassLoader;
类加载器的加载过程
当加载一个类的时候,是从 ApplicationClassLoader 开始的,但它会把当前的加载任务交给父亲去做,也就是让 ExtensionClassLoader 去加载,然后 ExtensionClassLoader 也把任务交给父加载器,也就是 BootstrapClassLoader 去加载,此时 BootstrapClassLoader 的 parent 是 null 了,也就只能自己去加载,此时 BootstrapClassLoader 就要搜索自己负责的标准库中的类,如果能找到,就加载,如果找不到,就再返回交给 ExtensionClassLoader 去加载,同样的道理,如果 ExtensionClassLoader 在自己负责的 JVM 扩展库中找到了对应的类,就加载,如果找不到,就再返回给 ApplicationClassLoader 去加载,如果找到了就加载,如果到最后都没有找到,那么就会抛出 类找不到 这样的一个异常;
就大致是一个这样的执行过程:
之所以以这样的一个加载顺序,让标准库先加载,然后再扩展库,最后再是用户写的类,是为了防止用户可能会写出一些标准库或者扩展库已经存在的类的,就比如 java.lang.String,而这个是标准库中已经存在的类的,按照上述的加载顺序,JVM 最后加载出来的就是标准库中的类,而不是用户自己写的;
另一方面,类加载器也是可以自定义的,以上三个是 JVM 自带的,用户自定义的类加载器,也可以加入到上述的流程中使用,自己写的类加载器,可以遵守双亲委派模型,也可以选择不遵守,例如 Tomcat,去加载 webapp 这里就是单独的类加载器,不遵守双亲委派模型;