类加载
类加载子系统
- 类加载器子系统负责从文件系统或者网络中加载class文件。classLoadr只负责class文件的加载,至于它是否可以运行,则由Exception Engine(执行引擎)决定。
- 加载类的信息存放于一块成为方法区的内存空间
- class file 存在于硬盘上,可以理解为设计师画在纸上的模板,最终这个模板在执行的时候要加载JVM当中来,根据这个模板实例化出若干个实例。
- class file 加载到JVM中,被称为DNA元数据模板
- 此过程需要有一个运输工具(类加载器 ClassLoader),扮演一个快递员的角色。
类加载的过程
加载
- 通过类名(地址)获取此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的各种数据访问接口。
链接
验证:对字节码文件格式进行验证,看文件是否被污染,对基本语法格式进行验证
准备: 准备阶段负责对类的静态属性分配内存,并设置默认初始值;
不包含使用final修饰的static变量,在编译时进行初始化
public static int val = 123;
//value 在准备阶段值为0,而不是123
静态常量在编译期间就被初始化
解析:将类的二进制中符号引用替换为直接引用
将字节码中的表现形式,转为内存中表现(内存地址)
初始化:类的初始化,为类中定义的静态变量进行赋值
public static int val = 123;
//在初始化阶段后值为123
类什么时候会被加载(初始化)?
- 在类中运行main方法
- 通过new关键字创建对象
- 使用类中的静态变量/方法
- 反射 Class.forName(“类的地址”)
- 子类被加载
以下两种情况类不会被初始化:
static final int b = 20;//编译期间赋值的静态常量
Class[] class = new Class[10]; // 作为数组类型
类初始化顺序
- 对static修饰的变量或语句进行赋值
- 如果包含多个静态变量和静态代码块,则按照自上而下的顺序依次进行。
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化父类
- 顺序是 父类static --> 子类static
public class classInit{
static {
num = 20;
}
static int num = 10;
public static void main(String[] args){
//num从准备到初始化值变化过程 num = 0 --> num = 20 --> num = 10
System.out.println(num);//10
}
}
public class classInit{
static int num = 10;
static {
num = 20;
}
public static void main(String[] args){
//num从准备到初始化值变化过程 num = 0 --> num = 10 --> num = 20
System.out.println(num);//10
}
}
类加载器分类
- 站在 JVM 的角度看,类加载器可以分为两种:
- 引导类加载器(启动类加载器 Bootstrap ClassLoader).
- 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader.
- 站在 java 开发人员的角度来看,类加载器就应当划分得更细致一些,自 JDK1.2 以来 java 一直保持者三层类加载器
- 引导类加载器:
用C/C++语言开发的,JVM底层的开发语言,负责加载java核心类库,与java语言无关。 - 扩展类加载器
java语言编写的,由sun.misc.Launcher$ExtClassLoader 实现,继承ClassLoader类,
从JDK系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库 - 应用程序类加载器
java语言编写的,由sun.misc.Launcher$AppClassLoader实现,派生于ClassLoader类。
加载程序中自己开发的类 - 自定义类加载器
public class ClassLoaderDemo{
public static void main(String[] args){
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);//null 因为是由引导类加载的 不是java代码
ClassLoader classLoader1 = ClassLoaderDemo.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@184aac2
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);//sun.misc.Launcher$ExtClassLoader @1b6d3586
System.out.println(classLoader2.getParent());//null classLoader2的父类是由引导类加载器加载的
}
}
双亲委派机制
- Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
- 工作原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
如果均加载失败,就会抛出 ClassNotFoundException 异常 - 如果我们自己创建一个java.lang.String类当我们new String()时,仍会加载创建核心类库创建核心类库中的String对象
package java.lang;
public class String{
static{
System.out.println("自己的String");
}
}
new java.lang.String();
逐级向上委托,最终引导类加载器找到了系统中真正的String
如何打破双亲委派机制
- 双亲委派机制是java提供的类加载的规范,但不是强制不能改变的
- 我们可以通过自定义类的加载器,改变加载方式
在 ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被 final 修饰,也就表示其他子类可以重写.- 重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制, 不推荐)
- 重写 findClass 方法 (推荐)
我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如 tomcat 等都有自己定义的类加载