类的加载、连接和初始化
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。正常情况下,JVM将会连续完成这三个步骤,所以一般将这三个步骤统称为类加载或类初始化。
JVM和类
当调用java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。同一个JVM的所以线程、所有变量都处于同一个进程中,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止。
- 程序运行到最后正常结束。
- 程序运行到使用System.exit()或Runtime.getRuntime.exit()代码处结束程序。
- 程序执行过程中遇到未捕获的异常或错误而结束。
- 程序所在平台强制结束了JVM进程。
类的加载
类加载是将类的class文件读入内存,并为之创建一个java.lang.Class对象。
该过程由类加载器完成,类加载器通常由JVM提供,JVM提供的类加载器通常被称为系统类加载器。除此之外,我们还可以通过继承java.class.ClassLoader超类来创建自己的类加载器对其进行扩展。
通过使用不同的类加载器,可以加载不同来源的类,通常来源如下:
- 从本地文件系统加载class文件
- 从jar包加载class文件
- 通过网络加载class文件
- 将一个Java源文件动态编译,并执行加载。
类加载器通常无需等到“首次使用”该类时才加载该类,Java虚拟机规范运行系统预先加载某些类。
类的连接
当类被加载之后,系统为之生成一个对象的Class对象,接着将会进入到连接阶段,该阶段复杂把类的二进制数据合并到JRE中。类连接又可分为以下三个阶段:
- 验证:验证阶段用于检验被加载的类是否有正确的内存结构,并和其他类协调一致。
- 准备:类准备阶段负责为类变量分配内存,并设置默认初始值。
- 解析:将类的二进制数据中的符号引用替换为直接引用。
类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在Java类中对类变量指定初始值有两种方式:
- 声明类变量时指定初始值。
- 使用静态初始化块为类变量指定初始值。
public class Test01{
private static int a ;//默认初始值为0
private static int b = 2;
private static int c ;
static {
//静态初始化块为其指定初始值
c = 3;
}
}
/*
* a = 0
* b = 2
* c = 3
*/
public class Test02{
static {
//静态初始化块为其指定初始值
b = 9;
}
private static int a ;//默认初始值为0
private static int b = 10;
private static int c ;
}
/*
* a = 0
* b = 10
* c = 0
* 静态初始化块都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行
* b = 9 之后再次被赋值为 b = 10
*/
JVM初始化一个类包含如下步骤:
- 1、假如这个类没有被加载和连接,则程序线加载并连接该类。
- 2、假如该类的直接父类还没有被初始化,则线初始化其直接父类。
- 3、假如类中有初始化语句,则系统依次执行这些初始化语句。
当执行步骤2中,系统对其直接父类的初始化步骤也遵循1-3,如果该直接父类又有直接父类,则以此类推,所以JVM总是最先初始化java.lang.Object类。当程序主动使用一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都被初始化。
类的初始化时机
当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口
- 创建类的实例。包括new操作符、发射创建、反序列化方式创建
- 调用某个类的静态方法
- 访问某个类或接口的类变量,或为该类变量赋值
- 使用反射方式来强制创建某个类或接口对于的java.lang.Class对象
- 初始化某个类的子类
- 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序将先初始化该主类
值得指出的是,对于一个使用final关键字修饰的类变量,如果该类变量的值在编译期间就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的具体值,因此即使程序使用该类变量也不会导致该类的初始化,因为实际上程序并没有使用该类的类变量,而是相当于使用了一个常量。
public class Test03{
public static void main(String[] args) {
//本行代码不会导致类的初始化
System.out.println(StaticTest.helloWorld);
}
}
class StaticTest{
static{
System.out.println("*类被初始化了*");
}
public static final String helloWorld = "hello world";
}
反之,如果final修饰符修饰的类变量不能在编译时确定,则必须等到运行时才可以确定该类变量的值,如果访问该类变量,会导致该类被初始化。
public class Test04{
public static void main(String[] args) {
//本行代码不会导致类的初始化
System.out.println(StaticTest.helloWorld);
//本行代码会导致该类被初始化
System.out.println(StaticTest.currentTimeMillis);
}
}
class StaticTest{
static{
System.out.println("*类被初始化了*");
}
public static final String helloWorld = "hello world";
public static final Long currentTimeMillis= System.currentTimeMillis();
}
当使用java.class.ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class.forName静态方法才会导致强制初始化该类。
public class Test05{
public static void main(String[] args) throws ClassNotFoundException{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//只是加载StaticTest类
classLoader.loadClass("edu.yunyu.StaticTest");
System.out.println("*****准备加载StaticTest类*****");
//初始化StaticTest类
Class.forName("edu.yunyu.StaticTest");
}
}
class StaticTest{
static{
System.out.println("*类被初始化了*");
}
public static final String helloWorld = "hello world";
public static final Long currentTimeMillis= System.currentTimeMillis();
}
类加载器
类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象。
类加载器介绍
类加载器负责加载所有的类,系统为所以被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了(包名.类名确定同一个类)。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。
- Bootstrap ClassLoader:(根/引导/原始)类加载器
- Extension ClassLoader:扩展类加载器
- System ClassLoader:(系统/应用)类加载器
Bootstrap ClassLoader被称为引导类加载器,它负责加载Java的核心类。
根类加载器非常特殊,它并非java.class.ClassLoader的子类,而是由JVM自身实现的。通过下面代码可以获取到根类加载器所加载的核心类库信息:
import sun.misc.Launcher;
import java.net.URL;
public class Test06{
public static void main(String[] args) throws ClassNotFoundException{
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
}
}
Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%\jre\lib\ext或者java.ext.dirs系统属性指定的目录)中jar包的类。通过这种方式,我们可以将自身开发的类打包为jar文件,放入该目录下,就可以为Java扩展新的功能。
System ClassLoader被称之为应用加载器,它负责在JVM启动时加载来自java命令的-classpath选项,java.class.path系统属性。或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过ClassLoader的静态方法getSystemCLassLoader()来获取系统类加载器,如果没有特别指定,则用户自定义的类加载器都以应用类加载器作为父加载器。
类加载器机制
JVM的类加载机制主要有以下三种:
- 全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入 - 父类委托
先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。 - 缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
import sun.misc.Launcher;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
public class Test07 {
public static void main(String[] args) throws Exception {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器=systemClassLoader = " + systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
/*
* 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定
* 如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
*/
Enumeration<URL> resources = systemClassLoader.getResources("");
while ((resources.hasMoreElements())){
System.out.println("resources.hasMoreElements() = " + resources.nextElement());//file:/D:/develop_tools/idea_projects/JavaAppCase/target/classes/
}
System.out.println("***********************************");
//获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("扩展类加载器=extClassLoader = " + extClassLoader);//sun.misc.Launcher$ExtClassLoader@7f31245a
System.out.println("扩展类加载器加载路径:"+System.getProperty("java.ext.dirs"));
ClassLoader classLoader = extClassLoader.getParent();
System.out.println("扩展类加载器的父类为:"+classLoader);//null
//为什么不是根类加载器?
//因为根类加载器并没有继承ClassLoader类,所有扩展类加载器的getParent()方法返回null,
//但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的
}
}
类加载器加载Class步骤:
- 1、检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则执行第2步
- 2、如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则执行第3步
- 3、请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步
- 4、请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步
- 5、当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步
- 6、从文件中载入Class,成功载入后跳到第8步。
- 7、抛出CLassNotFoundException异常
- 8、返回对应的java.lang.Class对象。
自定义类加载器
JVM中除了根类加载器之外的所有类加载器都是ClassLoader子类的实例。所以我们可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
应用场景:
- 执行代码前自动验证数字签名
- 根据用户提供的密码解析代码,从而可以实现代码混淆器来避免反编译*.class字节码文件
- 根据用户需要来动态的加载类
- 根据应用需求把其他数据以字节码的形式加载到应用中
//TODO