当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行加载。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象(类的类)(字节码对象),也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象(一个类只可能有一个字节码对象)。
现在 : 对象.getClass() == 类名.class --> 前面的对象是否属于后面的类型
以前的写法: 对象 instanceof 类
类的加载由类加载器(ClassLoader)完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类(类加载器的基类)来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据(字节码文件),通常有如下几种来源。
-
从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
-
从JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
-
通过网络加载class文件。 把一个Java源文件动态编译,并执行加载。
链接类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。 Object
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据(字节码对象)合并到JRE中。类连接又可分为如下3个阶段。
1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如Java有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
四种验证做进一步说明:
文件格式验证:主要验证字节流(加载的字节码文件)是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值(系统默认值)。
3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
初始化初始化是为类的静态变量赋予正确的初始值(程序员给静态成员变量赋的值),准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,
如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,,然后到解析即a=0;到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
类何时加载总结:
加载: 生成类的字节码对象
链接:
验证: 校验字节码对象中的成员是否符合Java类规范
准备: 加载类中静态成员并赋予系统默认值
解析: 把类中的符号引用替换成直接引用
初始化: 把类中静态成员的真实值替换默认值;
-
创建类的实例,也就是new一个对象
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(Class.forName("全类名"))
-
初始化一个类的子类(会首先初始化子类的父类)
-
JVM启动时标明的启动类,即文件名和类名相同的那个类 (main方法所在的类)
类加载器类何时被加载: 类第一次被使用的时候加载
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载进入JVM中,同一个类就不会被再次载入了。(一个类只会被加载一次!)
-
根类加载器(bootstrap classloader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。(无法获取到根类加载器对象)
-
扩展类加载器(extensions classloader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
-
系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。
-
自定义加载器(custom class loader): 我要指定一些类由某个类加载器加载 那么就可以自定义类记载器
类加载器使用场景
类加载器的功能: 快速获取当前类所在模块src文件夹根目录下的文件 (新的相对路径)
ClassLoader: 类加载器的类(类加载器的基类)
static ClassLoader getSystemClassLoader() : 获取到系统类加载器对象
InputStream getResourceAsStream(String fileName): 快速的获取传入文件路径指向的文件
String fileName: 文件路径 --> 相对路径写: 相对路径的起点是src文件夹
static InputStream getSystemResourceAsStream(String name):直接获取输入流对象 -> 推荐方法
单元测试 : 通过单独启动一个方法,测试方法内的逻辑或者结果是否正确 (白盒测试)
使用步骤:
导入jar包,并注入到当前模块 (模块的根建libs文件夹,复制jar包,Add as Lib..)
测试方法的规则: 不加static
1. 方法名 : testXxxx
2. 形参列表 : 必须无参 ()
3. 返回值类型 : 必须 void
4. 方法体内 : 逻辑代码
断言操作 : 预估结果
Assert.assertEquals(期望值, 真实值);
@Before : 以前 --> 被修饰的方法一定会在测试方法之前执行一次
@After : 以后 --> 被修饰的方法一定会在测试方法之后执行一次
反射 : 看懂后期的框架代码 //面向解决需求的人
正射 : stu.study();
反射 : study.invoke(stu);
1. 类名.class : 任何类型都有class属性,通过class属性可以拿到此类型的字节码对象 (引用,基本,数组)
Student.class , String.class , int[].class , double.class
2. 对象.getClass() : 通过对象获取该对象所在类的字节码对象 (引用数据类型[基本数据类型使用包装类对象获取])
3. Class类(类的类)中 -> static Class forName("类型的全类名") : 传入一个类的全类名可以获取这个类的字节码对象
Class类中的成员方法 :
//只能拿public修饰的构造方法,可以看到父类的public修饰的构造方法对象
Constructor>[] getConstructors() : 获取字节码对象中所有public修饰的构造方法对象
Constructor
//Class>... parameterTypes : 形参类型的字节码对象
//不看权限修饰符的,只能拿到本类的拿不到父类的
Constructor>[] getDeclaredConstructors() : 获取字节码对象中所有的构造方法对象
Constructor
Class类中的成员方法 :
//只能拿public修饰的构造方法,可以看到父类的public修饰的成员变量对象
Field[] getFields() : 获取字节码对象中所有public修饰的成员变量对象
Field getField(String name) : 获取字节码对象中指定的public修饰的成员变量对象
//String name : 要获取的成员变量对象的变量名
//不看权限修饰符的,只能拿到本类的拿不到父类的
Field[] getDeclaredField() : 获取字节码对象中所有的成员变量对象
Field getDeclaredField(String name) : 获取字节码对象中指定的成员变量对象
Class类中的成员方法 :
//只能拿public修饰的构造方法,可以看到父类的public修饰的成员方法对象
Method[] getMethods() : 获取字节码对象中所有public修饰的成员方法对象
Method getMethod(String methodName,Class>... parameterTypes) : 获取字节码对象中指定的public修饰的成员方法对象
//String methodName : 方法名 Class>... parameterTypes : 形参的字节码对象
//不看权限修饰符的,只能拿到本类的拿不到父类的
Method[] getDeclaredMethods() : 获取字节码对象中所有的成员方法对象
Method getDeclaredMethod(String methodName,Class>... parameterTypes) :: 获取字节码对象中指定的成员方法对象
构造方法暴力反射开关 : 屏蔽权限 void setAccessible(boolean flag)
只要成员没有访问权限,需要强行访问请开启暴力反射开关!!
以前 : new 构造方法(实参)
现在 : 反射
1. 拿到了构造方法对象
2. 构造方法对象自启动
Constructor类中的方法:
T newInstance(Object...args)
//Class类 : T newInstance() 前提 : Class对象所指向的类中必须有无参构造方法
Student student = clazz.newInstance();
System.out.println("student = " + student);
以前 : stu.属性名 / stu.属性名 = 值;
现在 : 反射
1. 拿到属性的对象
2. 属性对象设置值和获取值
Field类中的方法:
Object get(Object obj) : 获取调用方法的该属性的值
Object : 接收的就是该属性的具体值
Object obj : 哪个对象的该属性值
void set(Object obj, Object value)
Object obj : 哪个对象的该属性值
Object value : 设置的值
以前 : 返回值类型 返回值 = stu.方法名(实参);
-> stu.study();
现在 : 反射
1. 拿到方法的对象
2. 方法对象自启动,传入调用方法的对象 study.invoke(stu);
Method类中的方法 : 启动方法对象指向的那个方法
Object invoke(Object obj, Object... args)
返回值 Object : 被调用的方法的返回值
// 如果被启动的方法对象返回值是void,那么invoke方法的返回值是null
形参列表 Object obj : 调用方法的对象
形参列表 Object... args : 被启动的方法启动时需要的实际参数



