- 类加载器
- 启动类加载器:非java语言实现,负责将lib目录中的可被虚拟机识别的类库加载到虚拟机内存中(如rt.jar)
- 扩展类加载器:负责将JAVA_HOME\lib\ext 目录中的可被虚拟机识别的类库加载到虚拟机内存中
- 应用程序类加载器:可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。
public static void main(String[] args) {
// 我们自己定义的类
Test test = new Test();
// 获取当前线程的类加载器对象
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("自定义类的类加载器:"+test.getClass().getClassLoader());
System.out.println("当前线程的类加载器:"+loader);
System.out.println("父类加载器:"+loader.getParent());
System.out.println("父类加载器:"+loader.getParent().getParent());
}
输出:
自定义类的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
当前线程的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
父类加载器:sun.misc.Launcher$ExtClassLoader@4554617c
父类加载器:null
可以看到,通常系统默认使用的类加载是AppClassLoader
AppClassLoader的父类加载器是ExtClassLoader
ExtClassLoader的父类加载器是BootstrapClassLoader,但是输出null,因为这个类加载器是用C语言实现的
- 双亲委派模型
- 如果一个类加载器收到类加载请求,它不会自己先去加载这个请求类,而是委派给父类加载器去加载,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader);只有当父类加载器无法加载这个类时,子加载器自己才会尝试去加载这个类。
- 双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题
- 类加载方式
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
// 测试类
public class Test {
// 静态快
static {
System.out.println("类的静态初始化块执行了");
}
// 非静态快
{
System.out.println("类的初始化块执行了");
}
private String a;
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
}
// ClassLoader.loadClass()方法加载
// 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
public static void main(String[] args) {
try {
// 获取类加载器对象
ClassLoader loader = Test.class.getClassLoader();
// 加载类
loader.loadClass("classloader.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出:
// Class.forName()方法加载
// 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
public static void main(String[] args) {
try {
// 加载类
Class c = Class.forName("classloader.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出:
类的静态初始化块执行了
// Class.forName()方法加载
public static void main(String[] args) {
try {
// 加载类
Class c = Class.forName("classloader.Test");
// 创建类的实例
Test t = (Test) c.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
输出:
类的静态初始化块执行了
类的初始化块执行了
- 类加载过程 、类的生命周期
- 加载:通过一个类的全限定名来获取定义此类的二进制字节流并加载到内存;将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构);在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。
- 验证:校验加载的类是否符合JVM规范与安全
- 准备:为静态变量分配空间(在方法区中)、并将其初始化为默认值(如 static int a=3,a会被初始化为0,其他数据类型参考成员变量声明;注:如果变量同时被static和final修饰,则变量a会被直接初始化为声明的值,如static final int a=3,a会被直接初始化为3 )
- 解析:将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。
- 初始化:初始化类,执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程,或者说,初始化阶段就是执行类构造器<clinit>()方法的过程。(主要是根据程序中的赋值语句主动为类变量赋值;在准备阶段,变量a被初始化为0,在初始化阶段变量a将被赋值为3)
- 使用:使用
- 卸载:垃圾回收
- 类加载时机
- 当应用程序启动的时候,所有的类并不会被一次性加载,而是加载保证程序正常运行的基础核心类
- 一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类
- 类初始化时机
- 使用new创建对象时
- 调用类的静态变量(除了final常量)和静态方法时(注:通过子类引用父类的静态变量,不会导致子类初始化)
- 通过反射对类进行调用时
- 虚拟机启动,main方法所在类被提前初始化。
- 初始化一个类,如果其父类没有初始化,则先初始化父类。
- 类构造器
- 类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,
- 虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。
- 类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。
- 在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象。
-
JVM类加载机制
-
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
-
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
-
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
参考:
https://www.cnblogs.com/qiuyong/p/6407418.html?utm_source=itdadao&utm_medium=referral