class文件的格式
- 魔数:class文件的前4字节为魔数,用于确定当前文件是否能够被虚拟机识别。
- 版本号:魔数后面4字节用于描述class文件的版本号。高版本jvm可以兼容低版本class文件,反之不可以。
- 常量池:版本号之后紧随数据是常量池,常量池主要存放字面量(字符串,final数字)和符号引用(类名 方法名)。符号引用会在类加载时通过动态链接与具体内存地址相对应。
- 类的访问标志:用于标明一个类是否是public,是否是abstract,是否是接口等。
- 类索引,父类索引,接口索引:用于描述类的继承关系
- 字段表:描述类中的成员变量。变量类型,是否static,是否final等等。
- 方法表:描述类中的方法。方法修饰符,参数,返回值等等。
- 属性表:保存方法的字节码内容,final关键字定义的常量值,方法的局部变量描述,Java源码行号与字节码指令对应关系等等。字段表 方法表都可以引用属性表中的信息。
类加载的整体流程
- 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。包括:加载、连接、初始化几个阶段,其中加载阶段用户可以参与,连接和初始化完全由虚拟机主导。
- 加载:
读取class文件数据,获得二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区中的运行时数据结构。
在堆内存空间创建一个代表该类的 java.lang.Class对象,作为类方法区信息的访问入口。
此阶段通过类加载器完成,可以是系统提供的类加载器,也可能是自定义类加载器。 - 连接:
验证: 包括类文件合法性检查,因为class文件是可以被任意篡改的,因此需要验证class字节流信息是否符合虚拟机要求。
准备:为静态变量分配内存并设置默认值。注意是默认值而不是初始值。如果是final类型的静态基本类型或字符串,会在编译期确定它的值。
解析:将常量池符号引用替换为直接引用,把类名,方法名等符号引用直接与实际内存地址向关联。
对于类的解析:在类a中解析类b时,如果类b还没有加载,那么会使用当前类加载器先加载类b,加载成功后得到类b的直接引用。
对于方法的解析,如果是静态方法私有方法,在编译期方法入口确定,直接把方法符号引用转化为直接引用。如果是普通方法,可能被子类重写,那么不进行解析,保留符号引用,在方法真正调用时再动态连接得到方法实际入口。 - 初始化:
执行静态变量初始化语句,执行静态代码块 - 类的卸载:
- 当一个类没有用时就会卸载,避免方法区内存溢出
- 当 1 一个类对应类加载器对象不可达 2 类的所有实例对象不可达 3 类的Class对象不可达 这三个条件满足时就会触发类的卸载
- 由于系统类加载器,扩展类加载器,应用程序类加载器这三个类加载器始终被jvm引用,因此这三个类加载器加载的类一般不会卸载。自定义类加载器加载的类可能被卸载。
类的class对象
- class对象用来封装一个类在方法区的信息,是为方法区信息的访问提供的入口类。
- 需要注意的是类的静态变量,方法相关信息存放在方法区,而Class对象存放在堆内存空间。
- 创建class对象的三种方式,并且这三种方式得到的class对象是同一个。
public class Test {
public static void main(String[] args) throws Exception {
//通过类名创建
Class cls1 = Dog.class;
//通过类的实例化对象创建
Class cls2 = new Dog().getClass();
//通过类路径名创建
Class cls3 = Class.forName("Dog");
//通过类加载器的loadclass方法加载
Class cls = ClassLoader.getSystemClassLoader().loadClass("Dog");
}
}
class Dog{
}
class.forname方法与类加载器loadclass方法的区别
- 调用class.forname加载类时,还会对类进行连接和初始化,会执行静态代码块的代码。
- 调用loadclass方法仅仅只是加载,不会触发连接和初始化。
- 但是如果class.forname方法第二个参数为false时不会触发初始化
类初始化的时机
-
总的来说,类的初始化是惰性的,就是只有不得不初始化时才会初始化,否则不会初始化。
-
初始化的时机有
main方法所在的类会被初始化
首次访问一个类的静态变量或静态方法
一个类的子类初始化时
子类访问父类的静态成员只会触发父类的初始化
调用Class.forname时
new实例化一个类对象时 -
不会导致类初始化的情况
访问类的static final静态常量(基本类型和字符串)
类对象.class不会触发初始化
创建该类的数组不会触发初始化
使用类加载器的loadClass方法加载类
Class.forName的参数2为false时 -
实例
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//main方法所在的类 会被初始化
//初始化子类时 父类会初始化
int a = Dog.a; //访问静态变量 会初始化
Dog.eat(); //调用静态方法 会初始化
new Dog(); //实例化对象 会初始化
Class.forName("Dog"); //手动加载一个类 会初始化
Dog.class.newInstance(); //反射实例化对象 会触发初始化
//--------------------------------------
int b = Dog.b; //访问静态常量 不会初始化
Class cls = Dog.class; //访问类的class对象 不会初始化
Dog[] arr = new Dog[3]; //创建类的数组 不会初始化
Class.forName("Dog",false,ClassLoader.getSystemClassLoader()); //加载类时传false参数表示只加载不初始化
ClassLoader.getSystemClassLoader().loadClass("Dog"); //使用类加载器加载类 不会初始化
}
}
class Dog{
static {
System.out.println("init dog");
}
public static int a = 10;
public static final int b = 20;
public static void eat(){ }
}
几种类加载器
- 1、启动类加载器, 加载核心类库,lib目录的jar包。
- 2、扩展类加载器,加载核心类库,lib/ext目录的jar包。
- 3、应用程序类加载器,加载classpath目录的代码。
比如在idea中运行Java代码,idea默认会把当前工程的class文件目录放入到classpath路径中,会把maven本地仓库中我们依赖的第三方jar包放入classpath。这样我们自己写的代码以及第三方库代码就可以被应用程序类加载器加载了。 - 4、自定义类加载器,通过继承ClassLoader实现,比如自定义类加载器加载任意指定目录的class文件。
- 当jvm进程启动时,并不是lib目录中的所有jar包都会加载,而是使用到了哪个类才加载哪个类。比如使用到了 java.lang.String类,那么才会在lib目录的rt.jar文件中加载String类。
- 可以通过类对应class对象的getClassLoader来查看该类是由哪个类加载器加载
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//object类是由启动类加载器加载
ClassLoader loader1 = Object.class.getClassLoader();
System.out.println(loader1); //null
//syting类也是由启动类加载器加载
ClassLoader loader2 = String.class.getClassLoader();
System.out.println(loader2); //null
//dog类是由应用程序类加载器加载
ClassLoader loader3 = Dog.class.getClassLoader();
System.out.println(loader3); //sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
class Dog{
}
- ClassLoader.getSystemClassLoader()方法返回的是应用程序类加载器。
类加载机制的特点
- Java中的每个类由加载它的类加载器进行托管,一个类在jvm进程中的唯一性由类的全限定名和对应类加载器对象共同确定。
- 双亲委派:当一个类加载器加载一个类时,首先会委托其父类加载器来完成,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
- 全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。比如 类a由类加载器1加载,当类a内部调用类b时,类b也会由类加载器1进行加载。
- 缓存机制:每个类加载器都会对自己加载过的类进行缓存,如果当前类加载器已经加载过类a,那么当再次加载类a时会直接从缓存中查找返回上次缓存的类a。
双亲委派模型的作用
- 保证了Java中的类拥有有序的层次关系。避免不同层次的代码拥有相同类全限定名带来的冲突问题,保证了核心类库的代码不会被用户编写的代码覆盖。
- 我们知道String这个类是在java.lang包下的,它的全路径名是 java.lang.String,默认我们使用的String类就是通过启动类加载器加载的。
- 现在我们手动在用户工程目录编写一个java.lang.String类。然后使用应用程序类加载器加载我们编写的java.lang.String类,最终发现加载到的其实是核心类库的String类而不是自己编写的String类。
import java.lang.String;
public class Test {
public static void main(String[] args) {
Class cls = Class.forName("java.lang.String",true,ClassLoader.getSystemClassLoader());
System.out.println(cls.getClassLoader()); //null
}
}
- 这就是双亲委派模型在起作用,它保证了我们加载的string类是唯一的。当我们用应用程序类加载器加载自定义的string类时,应用程序类加载器会向上委派,然后发现启动类加载器可以加载,那么启动类加载器就会加载lib目录下的那个string类,我们自定义的string类压根不会加载。
- 如果我们加载自定义的一个user类,应用程序类加载器同样会向上委派,但是启动类加载器和扩展类加载器发现都加载不了user类,所以才会交给应用程序类加载器进行加载。
双亲委派模型的实现
- ClassLoader抽象类的loadclass方法实现了双亲委派的逻辑。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 从缓存中查看当前类是否已经被加载过了
Class<?> c = findLoadedClass(name);
//如果没有加载过,委托父类加载器进行加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
}
//如果父类加载器没有加载到,那么调用findClass方法进行加载
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
}
}
//是否执行连接(验证 准备 解析)操作
if (resolve) {
resolveClass(c);
}
return c;
}
}
- ClassLoader抽象类的findClass方法实现具体类加载逻辑,该方法需要被子类重写。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
类加载器对象的继承关系
- 类加载器对象的继承关系与类加载器的委派关系是不一样的,要区分开。类加载器委派关系是通过对象的parent属性确定的,而继承关系是通过super引用确定的。
自定义类加载器
- 我们可以继承 URLClassLoader 重写 findClass或 loadClass方法进行自定义类加载器。
- 如果不需要破坏双亲委派模型,只需要自定义类加载行为,只需要重写findClass,如果需要破坏双亲委派模型,还要重写loadClass。
- 需要自定义类加载器的应用场景有:从网络数据库等指定位置加载class文件,class文件加密,tomcat类加载体系实现,OSGI标准实现,代码热替换等。
其实每个jar包也有自己独立的命名空间
- 比如在classpath目录有一个 a.jar 它里面可能有一个类是 com.test.stu.class。
- 而classpath目录完全可能还有一个 b.jar 它里面也有一个类是 com.test.stu.class。
- 当a.jar和b.jar两个jar包都在classpath目录时,使用com.test.stu.class,加载的究竟是哪个jar包类取决于哪个jar包放在前面。