Java类的加载
类的加载、连接、初始化
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这个三个步骤,所以有时也把这三个步骤统称为类的加载或者类的初始化
类的加载
系统可能在第一次使用某个类时加载该类,可也可能采用预先加载机制来预加载某个类,不管怎样,类的加载必须由类加载器来完成,类加载器通常是由JVM提供,由JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有一下几种来源,
- 从本地系统直接读取.class文件,这是绝大部分类的加载方法
- 从zip、jar等归档文件中加载.class文件,这种方式也是很常见的
- 通过网络下载.class文件或数据
但是不管怎类的字节码内容从哪里加载,加载的结果都是一样,这些字节码内容加载到内存后,都会将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(引用地址),所有需要访问和使用类数据只能通过这个Class对象
类的连接
当类被加载后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段会负责把类的二进制数据合并到JVM的运行状态之中,类连接又可以分为如下三个阶段
- 验证:确保加载的类信息符合JVM规范,
- 准备:正式为类变量(static)分配内存并设置类变量的默认初始值的阶段,这些内存都将在方法区中进行分配
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
类的初始化
类的初始化主要就是对静态的类变量进行初始化
- 1、执行类构造器()方法的过程,类构造器()方法是由编译期自动收集类中所有类变量的显示赋值动作和静态代码块中的语句合并产生的,(类构造器是构造类信息的,不是构造该类对象的构造器)
- 2、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
- 3、虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步
class TestFather{
private static int a= getNum();
static{
++a;
System.out.println("(2)a="+a);
}
static {
++a;
System.out.println("(3)a="+a);
}
public static int getNum(){
System.out.println("(1)a="+a);
return 1;
}
}
public class Test1 extends TestFather{
private static int b= getNum();
static{
++b;
System.out.println("(5)b="+b);
}
static {
++b;
System.out.println("(6)b="+b);
}
public static int getNum(){
System.out.println("(4)b="+b);
return 1;
}
public static void main(String[] args) {
}
}
虽然类的加载大多数时候和类初始化是一气呵成的,但其实类的加载不一定就会触发类的初始化,当Java程序首次通过下面6中方式来使用某个类时,系统就会初始化该类:
会发生初始化:
- 当虚拟机启动,先初始化main方法所在的类
- new一个类的对象
- 调用该类的静态变量(final的常量除外)和静态方法
- 使用java.lang.reflect包中的方法对类进行反射调用
- 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类
不会发生初始化:
- 引用静态常量不会触发此类的初始化(常量在连接阶段就存入了调用类的常量池中了)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化
- 当通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
类加载器
将java类加载到JVM虚拟机中。Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。
四种类加载器
- 引导类加载器(Bootstrap ClassLoader)
- 又称为根类加载器。他负责加载Java核心类库(JAVA_HOME/jre/lib/rt.jar )
- 此类加载器并不继承于java.lang.ClassLoader
- 不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分
- 扩展类加载器(Extension ClassLoader)
- 这个类加载器负责加载JAVA_HOME\lib\ext目录下的类库,用来加载java的扩展库
- 应用程序类加载器(Application ClassLoader)
- 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载
- 这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器
- 自定义类加载器
- 通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求
经典委托模式
类加载器负责加载所有的类,系统为所有被加载入内存中的类生成了一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了
- 那么,怎么样算是“同一个类”呢?
- 在JVM中,一个类用其全限定类名(包名.类名)和其类加载器作为唯一标识。所以一个类使用不同的类加载器分别加载JVM将视为两个不同的类,它们互不兼容
- 那么,我们的类加载器在执行类加载任务时,如何确保一个类的全局唯一性呢?
- Java虚拟机的设计者们通过一种称之为“双亲委派模型(Parent Delegation Model)”的委派机制来约定类加载器的加载机制。
按照双亲委派模型的规则,除了引导类加载器之外,程序中的每一个类加载器都应该拥有一个超类加载器,比如,ExtensionClassLoader的超类加载器是引导类加载器,而AppClassLoader的超类加载器是ExtensionClassLoader,而自定义类加载器的超类就是AppClassLoader。
- 当一个类加载器接收到一个类加载任务的时候,他自己并不会加载,先检测此类是否加载过,即在方法区寻找该类对应的Class对象是否存在
- 如果存在就是已经加载过了,直接返回Class对象,否则会将加载任务委派给它的超类加载器去执行
- 每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止,
- 如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器,
- 这时子类加载器才会尝试去加载,如果无法加载,再退回给下一级
- 如果所有的类加载器都加载失败,就会报java.lang.ClassNotFoundException 或 java.lang.NoClassDefFoundError
双亲委派模式是在Java1.2引入的
双亲委派模式的优势:
- 采用双亲委派模式的好处是java类随它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类已经加载该类时,就没有必要子类再加载一次
- 其次考虑安全因素,java核心api中定义的类型不会被随意替换
注意:
由于java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委托模式,只是建议采用这种方式而已,比如在Tomcat中,类加载所采用的加载机制和传统的双亲委派模型有一定的区别,当缺省的类加载器只接到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给他的超类加载器去执行,这同时也是Servlet规范推荐的一种做法
数组类型本身并不是由类加载器去负责创建,而是由JVM在运行时根据需要而直接创建,但数组的元素类型仍然需要依靠类加载器去创建,因此,JVM会把数组元素类型的类加载器记录为数组类型的类加载器
ClassLoader
抽象类,
- 根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例
- ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。
为了完成加载类的这个职责,ClassLoader提供了一系列的方法 :
返回值 | 方法名 | 说明 |
---|---|---|
ClassLoader | getParent() | 返回委托的父类加载器 |
static ClassLoader | getSystemClassLoader() | 返回委托的系统类加载器 |
Class<?> | loadClass(String name) | 使用指定的全限定名来加载类 |
protected Class<?> | findClass(String name) | 使用二进制名称(类的全限定名)来查找类。 |
protected final Class<?> | findLoadedClass(String name) | 使用指定的全限定名来查找类,并返回,否则返回null |
protected final Class<?> | defineClass(String name,byte[] b,int off,int len) | 将一个byte数组转换为Class类的实例 |
-
全限定名:
- 包名.类名
- 内部类有所不同:
- 匿名内部类(外部类的全限定名$编号)
- 局部内部类(外部类的全限定名$编号+类名)
- 成员/静态内部类(外部类的全限定名$类名)
-
findClass(String name)方法应该被类加载器的实现重写,该实现按照委托模式来加载类,在通过父类加载器检查所请求的类后,此方法将被loadClass方法调用
很多开发过程中,都遇到过java.lang.ClassNotFoundException 或java.lang.NoClassDefError,想要更好的解决这类问题,或者一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其类加载机制也就成了每一个Java开发人员的必备技能之一
package com.zxy;
import java.io.*;
/**
* 自定义类加载器示例
*/
public class Test {
public static void main(String[] args) {
ClassLoader fileClassLoader=new FileClassLoader("F:\\FileTest");
Class<?> cc= null;
try {
cc = fileClassLoader.loadClass("HelloWorld");
} catch (ClassNotFoundException e) {
// e.printStackTrace();
}
System.out.println(cc.getClassLoader());
System.out.println(cc);
}
}
/**
* 自定义一个文件加载器
*/
class FileClassLoader extends ClassLoader{
private String rootDir;//指定加载路径
public FileClassLoader(String rootDir){
this.rootDir=rootDir;
}
/**
* 检查类是否已经加载过,如果加载过则返回类的class实例
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//首先检查请求的类型是否已经被这个类加载器装载到命名空间中,如果已经被装载,直接返回
Class<?> c=findLoadedClass(name);
if(c==null){
//委派类加载器请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例
//获取当前类加载器的父类
ClassLoader parent=this.getParent();
try {
c = parent.loadClass(name);
//如果加载不成功,最终需要有自己加载
}catch (Exception ce) {
// ce.printStackTrace();
}
//调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取到,则调用defindClass()导入类型到方法去
if(c==null){
byte[]classData=getClassData(name);
if(classData==null){
throw new ClassNotFoundException("没有找到指定的类");
}else{
c=defineClass(name,classData,0,classData.length);
}
}
}
return c;
}
/**
* 把.class文件的内容读取到一个字节数组中,为什么读取到字节数组中?
* 为defineClass(String name, byte[] b, int off, int len)
* 获取Class实例做数据的准备
* File.separator可以根据操作系统来生成相应的路径分隔符
* @param name 是包名.类名
* @return
*/
private byte[]getClassData(String name){
//获取字节码的路径
String path=rootDir+ File.separator+name.replace(".",File.separator)+".class";
InputStream is=null;
ByteArrayOutputStream baos=null;
try {
is=new FileInputStream(path);
//创建一个写入字节到内存的流对象
baos=new ByteArrayOutputStream();
byte[]buffer=new byte[1024];
int len;
while ((len=is.read(buffer))!=-1) {
baos.write(buffer,0,len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(baos!=null){
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}