简介
在介绍jvm的时候,我们了解了jvm 运行时数据区,在运行时数据区里,我们知道对象是放在堆中的,类是放在方法区(元空间)的,对象是我们程序运行过程中产生的,那么类又是如何产生和加载到方法区呢?这就是我们接下来需要讲述的类加载器,类加载器是JVM中的一个独立模块,我们称之为类加载子系统。
还记得我们刚开始学习java的时候,老师在介绍java相比较于其他语言有个很好的优势,就是它可移植(跨平台),在不同的操作系统中,都可以运行我们的java程序,而实现这个功能的就是不同操作系统下的JVM。我们要运行java程序,首先要将java文件编译成class文件,类加载器通过加载class文件,将类的信息存储到方法区。类的生命周期为:加载,验证,准备,解析,初始化,调用,卸载这个七个。
类加载步骤
类的加载步骤主要是类的生命周期的前五步。
1、加载
首先要明确一点,程序中的所有类信息不是程序启动后就立马进行加载的,只有当我们需要使用的时候,才会进行加载!
加载流程:
首先获得类的名称,去判断这个类有没有被加载过
如果加载了进入下一步一验证;如果没有加载,通过ClassLoader (默认类加载器)或者自定义类加载器装载
如果类加载器装载成功,进入验证阶段;如果装载失败抛出异常
在类加载器装载过程中,虚拟机需要完成下面三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
那么对于何时进行类加载,这里可以归纳下述几个时机:
遇到new、getstatic、putstatic或invokestatic这4条字节码指令时;
使用java.lang.reflect包的方法对类进行反射调用的时候;
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
用户指定程序启动时需要初始的类
2、验证
验证、准备和解析,三步可以统称为连接。验证主要是从文件的格式,元数据,字节码,符号引用等方面,保证class文件内容符合规定并且确保不会危害虚拟机自身的安全。
3、准备
简单来将就是为类变量分配内存并设置初始值。这里只要搞清楚什么是类变量,类变量就是由static修饰
的变量。
举个例子:
public static inti = 100;
public intj= 50;
public static final intk = 10;
上述的三个变量: i属于类变量,准备阶段会设置初始值为0; j属于实例变量,不会分配内存和设置初始值(它将会在对象实例化时随着对象一起分配在Java堆中) ;而k是final修饰的,所以他再这个阶段不会设置初始值,而是直接设置值为10.
4、解析
这一步很好理解,解析就是将常量池内的符号引用替换为直接引用的过程。在准备阶段,我们已经给类变量在方法区的常量池里分配了内存地址,解析就是将之前的符号引用(意思就是class文件中那些用文字
来代替描述实际引用的部分)替换为直接引用。
符号引用: java文件编译成class文件后,对于一些引用地址并不能在编译过程中知道,所以它要用符号来代替。
直接引用:就是指向内存地址的指针。
5、初始化
初始化阶段是执行类构造器 () 方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
这里需要注意几个点:
1、static修饰变量的顺序会影响源文件中出现的顺序
package com.example.demo.jvm;
/**
* @author: sunzhinan
* @create: 2020-08-04 20:25
* @description:
*/
public class Test01 {
public static int i = 10;
static {
i = 20;
j = 30;
}
public static int j = 40;
public static void main(String[] args) {
System.out.println(Test01.i);
System.out.println(Test01.j);
}
}
结果:
20
40
2、若该类具有父类,JVM会先去执行父类的< clinit> ()方法,再去执行子类的< clinit >()。
package com.example.demo.jvm;
/**
* @author: sunzhinan
* @create: 2020-08-04 20:36
* @description:
*/
public class Test02 {
public static void main(String[] args) {
System.out.println(B.k);
}
}
class A {
public static int i = 2;
static{
i = 3;
}
}
class B extends A{
public static int k = i;
}
结果:
3
3.、< clinit> () 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 () 方法。
4、虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit> () 方法,其他线程都需要阻塞等待,直到活动线程执行< clinit> ()方法完毕。
package com.example.demo.jvm;
/**
* @author: sunzhinan
* @create: 2020-08-04 20:39
* @description:
*/
public class Test03 {
static {
System.out.println(Thread.currentThread().getName() + " : 这个线程初始化了 Test03");
}
public static void main(String[] args) {
new Thread(()->{
new Test03();
new C();
},"Thread-01").start();
new Thread(()->{
new Test03();
new C();
},"Thread-02").start();
}
}
class C {
static{
System.out.println(Thread.currentThread().getName() + " : 这个线程初始化了 C");
}
}
结果:
main : 这个线程初始化了 Test03
Thread-01 : 这个线程初始化了 C
使用和卸载
类的使用,就是去创建这个类的实例。类的加粗样式卸载,两个概念:
- 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
- 由用户自定义的类加载器加载的类是可以被卸载的。
加载该类的ClassLoader已经被回收
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
类加载器
JVM自带三个类加载器,它们分别是:
- 引导类加载器(Bootstrap ClassLoader) : 最顶层的加载类,主要加载核心类库,也就是我们环境变量下
面%JAVA_ HOME%\ib’下的rt.jar、resources.jar、charsets.jar和class等,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类
加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。 - 扩展类加载器(Extention ClassL oader) : 扩展的类加载器,加载目录%JAVA_ HOME%\lib\ext目录; 下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
- 应用程序类加载器/系统类加载器(Appclass Loader) :加载当前应用的classpath的所有类,这也是我们经常接触到的,它主要就是加载我们项目中的class。
除了上面三个JVM自带的类加载器,我们还可以通过继承抽象类java.lang.ClassL oader类,重写findclass () 方法(findclass方法中是编写的类加载逻辑)
类加载机制
双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载
器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassL oader。双亲委派模型更多的是一种流程或者说是规则,它规定:当加载一个类的时候,会先将这个类交给其父类加载器加载。但是需要注意的是,是一直向上委派加载任务的, 直到顶层的启动类加载器。当父类加载器无法完成加载任务时,才会尝试执行加载任务。
优点:
- 具备优先级的层次关系,避免重复加载。
- 沙箱安全机制,保证对java核心源代码的保护,防止被恶意篡改