一、类加载概述
在JVM执行我们写好的代码的过程中,具体是在代码中用到这个类的时候将“.class”文件加载进JVM内存里,类的加载到使用具体经过下面这几个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。
各个将阶段的任务描述:
二、类的初始化顺序
Java类加载机制中最重要的就是程序初始化过程,其中包含了静态资源,非静态资源,父类子类,构造方法之间的执行顺序。
首先通过一个例子来分析java代码的执行顺序:
public class CodeBlockForJava extends BaseCodeBlock {
{
System.out.println("这里是子类的普通代码块");
}
public CodeBlockForJava() {
System.out.println("这里是子类的构造方法");
}
@Override
public void msg() {
System.out.println("这里是子类的普通方法");
}
public static void msg2() {
System.out.println("这里是子类的静态方法");
}
static {
System.out.println("这里是子类的静态代码块");
}
public static void main(String[] args) {
BaseCodeBlock bcb = new CodeBlockForJava();
bcb.msg();
}
Other o = new Other();
}
class BaseCodeBlock {
public BaseCodeBlock() {
System.out.println("这里是父类的构造方法");
}
public void msg() {
System.out.println("这里是父类的普通方法");
}
public static void msg2() {
System.out.println("这里是父类的静态方法");
}
static {
System.out.println("这里是父类的静态代码块");
}
Other2 o2 = new Other2();
{
System.out.println("这里是父类的普通代码块");
}
}
class Other {
Other() {
System.out.println("初始化子类的属性值");
}
}
class Other2 {
Other2() {
System.out.println("初始化父类的属性值");
}
}
这个例子比较简单,在运行代码之前分析一下:带有static关键字的代码块应该是最先执行,其次是非static关键字的代码块以及类的属性(Fields),最后是构造方法。带上父子类的关系后,上面的运行结果为:
这里是父类的静态代码块
这里是子类的静态代码块
初始化父类的属性值
这里是父类的普通代码块
这里是父类的构造方法
这里是子类的普通代码块
初始化子类的属性值
这里是子类的构造方法
这里是子类的普通方法
注意的是类的属性与非静态代码块的执行级别是一样的,谁先执行取决于书写的先后顺序。
结论1:
父类的静态代码块 -->
子类的静态代码块 -->
初始化父类的属性值/父类的普通代码块(自上而下的顺序排列) -->
父类的构造方法 -->
初始化子类的属性值/子类的普通代码块(自上而下的顺序排列) -->
子类的构造方法
注:构造函数最后执行。
接下来再看一个比较复杂的例子:
public class ClassloadSort1 {
public static void main(String[] args) {
Singleton.getInstance();
System.out.println("Singleton value1:" + Singleton.value1);
System.out.println("Singleton value2:" + Singleton.value2);
Singleton2.getInstance2();
System.out.println("Singleton2 value1:" + Singleton2.value1);
System.out.println("Singleton2 value2:" + Singleton2.value2);
}
}
class Singleton {
static {
System.out.println(Singleton.value1 + "\t" + Singleton.value2 + "\t" + Singleton.singleton);
//System.out.println(Singleton.value1 + "\t" + Singleton.value2);
}
private static Singleton singleton = new Singleton();
public static int value1 = 5;
public static int value2 = 3;
private Singleton() {
value1++;
value2++;
}
public static Singleton getInstance() {
return singleton;
}
int count = 10;
{
System.out.println("count = " + count);
}
}
class Singleton2 {
static {
System.out.println(Singleton2.value1 + "\t" + Singleton2.value2 + "\t" + Singleton2.singleton2);
}
public static int value1 = 5;
public static int value2 = 3;
private static Singleton2 singleton2 = new Singleton2();
private String sign;
int count = 20;
{
System.out.println("count = " + count);
}
private Singleton2() {
value1++;
value2++;
}
public static Singleton2 getInstance2() {
return singleton2;
}
}
这个用例相比第一个,知识点更深了一层。如果你用结论1是没法分析出正确答案的,但这并不代表结论1就是错误的。
运行结果:
Singleton value1:5
Singleton value2:3
Singleton2 value1:6
Singleton2 value2:4
Singleton中的value1,value2并没有受到构造方法中自加操作的影响。然而Singleton2中的代码也相同,为什么执行出来的效果就不一样呢?
要想知道原因,必须先搞清楚Java类加载中具体做了些什么。
我们来看Singleton2.getInstance()的执行分析:
(1) 类的加载。运行Singleton2.getInstance(),JVM在首次并没有发现Singleton类的相关信息。所以通过classloader将Singleton.class文件加载到内存中。
(2) 类的验证。
(3) 类的准备。将Singleton2中的静态资源转化到方法区。value1,value2,singleton在方法区被声明分别初始为0,0,null。
(4) 类的解析。将常量池内的符号引用替换为直接引用的过程。
(5) 类的初始化。执行静态属性的赋值操作。按照顺序先是value1 = 5,value2 = 3,接下来是private static Singleton2 singleton2 = new Singleton2();
这是个创建对象操作,根据 结论1 在执行Singleton2的构造方法之前,先去执行static资源和非static资源。但由于value1,value2已经被初始化过,所以接下来执行的是非static的资源,最后是Singleton2的构造方法:value1++;value2++。
所以Singleton2结果是6和4。
以上除了搞清楚执行顺序外,还有一个重点->结论2:静态资源在类的初始化中只会执行一次。
有了以上的这个结论,再来看Singleton.getInstance()的执行分析:
(1) 类的加载。将Singleton类加载到内存中。
(2) 类的验证。
(3) 类的准备。将Singleton的静态资源转化到方法区。
(4) 类的解析。将常量池内的符号引用替换为直接引用的过程。
(5) 类的初始化。执行静态属性的赋值操作。按照顺序先是private static Singleton singleton = new Singleton(),根据 结论1 和结论2,value1和value2不会在此层执行赋值操作。所以singleton对象中的value1,value2只是在0的基础上进行了++操作。此时singleton对象中的value1=1,value2=1。
然后, public static int value1 = 5; public static int value2 = 3; 这两行代码才是真的执行了赋值操作。所以最后的结果:5和3。
如果执行的是public static int value1; public static int value2;结果又会是多少?结果: 1和1。
因为static变量的赋值在类的初始化中只会做一次。程序在执行private static Singleton singleton = new Singleton()时,已经是对Singleton类的static变量进行赋值操作了。这里new Singleton()是一个特殊的赋值。会自动过滤static变量的赋值操作。但非static的变量依然会被赋值。
结论3:在结论2的基础上,非静态资源会随对象的创建而执行初始化。每创建一个对象,执行一次初始化。
三、类加载器
JVM提供了以下3种系统的类加载器?
- 启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。
用户自定义的类加载器步骤?
- 继承ClassLoader。
- 重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象。
为什么要自定义类加载器?
- 可以从指定位置加载class文件,比如说从数据库、云端加载class文件。
- 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。
Java程序对类的执行有几种方式?
JVM必须在每个类“首次 主动使用”的时候,才会初始化这些类。
- 创建类的实例。
- 读写某个类或者接口的静态变量。
- 调用类的静态方法。
- 同过反射的API(Class.forName())获取类。
- 初始化一个类的子类。
- JVM启动的时候,被标明启动类的类(包含Main方法的类)。
只有当程序使用的静态变量或者静态方法确实在该类中定义时,该可以认为是对该类或者接口的主动使用。
JVM规范允许类加载器在预料某个类将要被使用的时候,就预先加载它。如果该class文件缺失或者存在错误,则在程序“首次 主动使用”的时候,才报告这个错误。(Linkage Error错误)。如果这个类一直没有被程序“主动使用”,就不会报错。
类加载机制与接口?
- 当Java虚拟机初始化一个类时,不会初始化该类实现的接口。
- 在初始化一个接口时,不会初始化这个接口父接口。
- 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。
ClassLoader:调用Classloader的loadClass方法去加载一个类,不是主动使用,因此不会进行类的初始化。
类的卸载:
- JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的。
- 由用户自定义类加载器加载的类,是可以被卸载的。