当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过 加载,连接,初始化,这三个步骤对类进行初始化,如果没有意外,JVM 将会连续完成这三个步骤,所以有时也称为类初始化。
虽然我们并无序过分关心类加载机制,但是 基本 工作机制我们还是要知道的,这样对我们来说也能更好的去理解。先用一张思维导图来概括类加载机制。不过需要注意的是,jdk1.9对类加载器进行了改变,并废除了一些方法,本篇博客并没有对 jdk1.9进行概述。
类加载 (划重点)
类加载指的是将类的 class 文件读入内存,并为之创建一个 Java.lang.class 对象,也就是说,当程序中使用任何类时,系统都会为之创建 一个 java.lang.Class 对象。
类的加载由加载器完成,类加载器通常由 jvm 提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,还可以通过继承 ClassLoader 基类来创建自己的类加载器。一般有如下几种类加载来源:
- 从本地文件系统加载class 文件
- 从jar包记载 class 文件
- 通过网络加载class文件
- 把一个java源文件动态编译,并执行加载。
类加载器通常无序等到 首次使用该类时才加载,Java 虚拟机允许系统预加载某些类。
当 JVM 启动时(也就是你点击运行程序之后),会形成有三个类 加载器组成的初始类加载器层次结构:
- Bootstrop ClassLoader ,被称为引导(也成为原始或根) 类加载器。他负责加载 Java的核心类。 我们一般使用的 Math,String,System类就是由此加载。
- Extension Classloader ,被称为扩展类加载器,他负责加载 jre 的扩展目录中 jar 包的类。通过这种方式,我们就可以为 java 扩展核心类 以外的新功能。
- System Classloader,系统(也称应用) 类加载器,该加载器继承自扩展类加载器,主要功能是加载我们在日常编译器(idea,eclipse等)里面编写的类,这些类的加载,都是由该类加载器完成的。程序可以通过 ClassLoader 的静态方法 getSysemClassLoader() 获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以类加载器作为父加载器。
类加载机制主要有以下三种
- 全盘负责:当一个类加载器加载某个 Clas时,该Class 所依赖和引用的其他 Class 也将于该类加载器负责载入,除非显示的使用另一个类加载器来载入。
- 双亲委派: 当一个类加载器收到了类加载请求,它会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径加载该类,如果直至引用程序类加载器还没有发现该类同名的类,则会抛出异常。举个简单例子,你可以自定义一个 java.lang.Math类,但你永远永不了该类,因为该类存于 java 核心类,会由引导加载器去加载,所以不论你怎么调用,它加载的永远是 java 核心类库的 Math类。
- 缓存机制: 缓存机制会保证所有加载过的 Class 都会被缓存,当程序中使用某个Class时,类加载器首先从缓存区寻找 该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class对象。这也就是为什么修改了 Class之后,必须重新运行程序才会生效的原因。
但需要注意,类加载器之间并不是继承上的父子关系,而是类加载器实例之间的关系。
连接
当类被加载之后,系统为之生成一个对应的 Class 文件,接着将会进入连续阶段,链接阶段负责把 类的二进制数据合并到 jre中,类链接又分为如下3个阶段。
- 验证: 验证阶段同于检验被加载的类是否有正确的内部结构,就是检查符不符合Java语言规范,并和其他类协调一致。
- 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
- 八种基本类型默认的初始值为0
- 引用类型默认的初始值是 null
- 有 static final 修饰的会直接赋值,例如 static final a=10,则默认就是10
- 解析:将类的二进制数据中的符号引用替换成直接引用。就是jvm 会把所有的类或接口名,字段名,方法名转换为具体的内存地址。
初始化
虚拟机负责对类进行初始化,主要就是对类 变量 进行初始化,有如下两种初始化方式:
- 声明类变量时初始化
- 使用静态初始化为类变量指定初始值
一般初始化一个类包含如下几个步骤:
- 假如这个类没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如该类中有初始化语句,则系统一次执行这些初始化语句。
一个类什么时候会被初始化呢?
- 创建类的实例。为某个类创建实例的方法会包括:使用new 操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
-
调用某个类的类方法(静态方法)。
-
访问某个类或接口的类变量,或为该类变量赋值。
-
使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
-
Class.forName("Person") //如果系统还未初始化 一个 类,则这行代码将会导致该类被初始化,并返回该 类对应的 java.lang.Class对象。
-
初始化某个类的子类。当初始化某个类的子类时,盖子类的所有父类就会被初始化。
-
直接使用 java.exe 命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
-
不过也需要注意以下几种情形:
-
对于一个 final 型的变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于 “宏变量” 。java 编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。
-
public class MyClass { //使用静态初始化块打印的语句 static { System.out.println("123"); } //声明变量a时指定初始值 static final int a = 5; } class Demo{ public static void main(String[] args) { //并不会初始化Myclass System.out.println(MyClass.a); } }
反之,如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,通过该类来访问它的类变量,则会导致该类被初始化。
public class MyClass { static { System.out.println("静态方法块"); } //运行时才可以确定的变量,就算加了final也不行 static final String com=System.currentTimeMillis()+""; } class Demo{ public static void main(String[] args) { System.out.println( MyClass.com); } }
-
当某个变量使用了 final 修饰,而且他的值可以在编译时就确定下来,那么程序其他地方使用该变量时,实际上并没有使用该变量的值,而是相当于使用常量。