目录
1.类文件结构
2.类加载器
3.类加载的三个阶段
3.1加载
3.2链接
3.2.1验证
3.2.2准备阶段
3.2.3解析阶段
3.3初始化
4.拓展:反射
4.1获取类对象
4.2创建实例
4.3获取方法
4.4方法调用
1.类文件结构
2.类加载器
类加载器用来将类文件的二进制字节码加载到JVM的方法区中。有四种类加载器:
连接数据库驱动调用的类加载器:在连接数据库时会调用DriverManager类进行驱动,而DriverManager是核心类,所以会使用到启动类加载器;但数据库连接的驱动包并不在核心类库中,所以DriverManager类中有一个loadInitialDrivers()方法,内部使用了两种方式加载驱动:
第一种是SPI机制加载驱动,约定是在jar包中添加一个META-INF/services目录,在其中添加一个配置文件,文件的名称就是接口的全限定名称,数据库连接驱动就是java.sql.Driver,文件内容就是接口的实现类 ;通过ServiceLoader.load()方法根据约定的路径找到实现类;这个load方法的内部调用的是线程上下文类加载器,由于在创建线程时默认分配的是应用类加载器,所以这种机制实际上调用的是应用类加载器。第二种是使用系统变量jdbc.drivers定义的驱动类的类名加载驱动,调用Class.forName()方法加载和初始化驱动类,使用的是系统类加载器,也就是应用类加载器。
数据库连接驱动是先调用的系统类加载器再调用的应用类加载器,所以在某些情况下会打破双亲委派机制。
3.类加载的三个阶段
类加载分为三个阶段:加载、链接、初始化。链接又分为三个阶段:验证、准备、解析。
3.1加载
要注意的是,一个类的.class文件加载到方法区后变成了C++的instanceKlass文件,这个文件中包含了这个类的各种信息,然后再在堆中生成一个Class类型的对象(区分class,class是定义一个类用的;区分.class文件,这是编译生成的一个类的二进制字节码,还没有被加载),因为java不能直接访问方法区的instanceKlass,所以需要这个Class副本来供我们使用,通过反射拿到的一个类的Class对象就是这么来的,这也就是为什么这个Class对象被叫做类镜像了,下图的Person.class应该是Person的Class对象,InstanceKlass中的_java_mirror存的是Class对象的指针。至于Class对象里有静态成员变量是因为在JDK8和以上版本中静态成员变量就被放在了Class对象的末尾,也就是放到堆中了。
3.2链接
3.2.1验证
验证这个类转换的字节码是否符合JVM规范,并进行安全性检查,比如检查字节码中的魔数是否是Java文件的魔数。
3.2.2准备阶段
给静态变量分配空间,并设置默认值:
- 静态变量在JDK7和之前的版本中是放在instanceKlass的末尾也就是方法区中,而从JDK8开始则是放在了类镜像末尾,也就是堆内存中。
- 静态变量的空间分配在准备阶段完成,而赋值则是在初始化阶段,但是final类型的静态变量比较特殊:如果是final的基本类型或字符串类型的静态变量,则分配空间和赋值都在准备阶段完成,因为对于这些类型的变量而言final说明值不会改变,已经确定了静态变量的值,所以在准备阶段会直接赋值;而如果是final的引用类型的静态变量则赋值会放在初始化阶段,因为new一个对象需要类先初始化完成后才能创建。
3.2.3解析阶段
将常量池的符号引用解析为直接引用。符号引用就是这个类虽然被加载了,但由于还没有进行解析,也就不知道这个类在内存中的位置,相当于只是一个符号;而直接引用就是经过了解析之后,知道了其在内存中的具体位置,就可以访问这个类了。
3.3初始化
类的初始化是为了确保类的结构正确并且所有的数据都已初始化为预期的状态,只有在类的初始化完成后才能在系统中正常使用这个类及其方法和属性。初始化过程主要包括给静态变量赋值、静态代码块的执行等,只有首次主动使用时才会触发初始化,初始化是懒惰的,且只进行一次。
初始化发生的时机:
- main方法所在的类会先进行初始化。
- 首次访问这个类的静态变量或静态方法时。
- 子类进行初始化时,若父类还没有初始化,则会先进行父类的初始化再进行子类的初始化。
- 当子类访问父类的静态变量时,只会触发父类的初始化。
- 当执行Class.forName方法时,会执行类加载并默认进行初始化;当然也可以给参数initialize设置为false表示不执行初始化。
- 通过new创建实例化对象时会触发初始化。
以下情况不会触发初始化:
- 访问静态常量时,因为静态常量的空间分配和赋值均在链接时的准备阶段完成。
- 使用类加载器的loadClass方法时,loadClass方法只进行加载阶段。
- 访问类对象的.class文件时不会触发初始化,因为Class对象在class文件加载到方法区后就会生成,所以在加载阶段时就已经生成。
- 创建该类的数组不会触发初始化,因为在JVM中会生成一个其他的类来表示数组类型,与原本的类无关,所以不会触发原来的类的初始化。
4.拓展:反射
反射:通过使用类对象(即堆中的Class对象)来创建实例、调用方法等。
4.1获取类对象
Class c=类名.forName();
或
Class<?> c=类名.class;
或
Class<?> c=类名.getClass();
以下操作都建立在获取了Class对象的基础上
4.2创建实例
Object o=c.newInstance();
也可以通过指定的构造器来创建实例,比如使用String的构造器来创建实例:
//先获取String类的带一个String类型参数的构造器
Constructor cst=c.getConstructor(String.class);
//再通过调用构造器的newInstance方法来创建实例
Object o=cst.newInstance("abc");
4.3获取方法
//获取这个类的除继承父类的方法外的其他所有方法
Method[] m1=c.getDeclaredMethods();
//获取这个类的所有公有方法
Method[] m2=c.getMethod();
//获取指定方法
Method m=c.getMethod("方法名");
4.4方法调用
//先获取指定的方法
Method m=c.getMethod("方法名");
//调用方法:
//当所调用的方法既有参数也有返回值时
Object result=m.invoke(参数集);
//当所调用的方法没有返回值且无参数时
m.invoke(null);