最近肝代码肝的有点头疼,随便写点简单的东西吧。
类的加载
要想去理解类的加载的过程,我们就需要关注。JVM数据分配的设计、类加载过过程。它们觉得了类不同操作下加载不同的内容,以及谁先加载谁后加载的内容。
类加载的内容
要想去理解类的加载机制,就要先了解JVM数据分配的设计。在之前JVM运行时数据区域中可以知道。
JVM为类分配了(Java堆,方法区(1.8之后使用元空间))。两者都保存了类的信息。区别在于
区域 | 描述 |
---|---|
Java堆 | JAVA堆唯一目的就是存放对象实例 |
方法区 | (1.8之后被元空间替代)用来储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码 |
关于JVM数据分配的问题这里就不展开了。但是上面可以看到根据类创建出来的对象和类中静态信息和常量信息是不同的保存区域。我们访问类的静态内容时是不需要创建一个对象实例的。
类的加载顺序
讲到类的加载,就要说之前在JVM上的类加载内容。这个时候我又要把这种图掏出来了。
根据上面流程我们可以知道,类加载大致可以分为加载、连接、初始化、使用、卸载。
阶段 | 描述 |
---|---|
加载 | 此阶段的作用是获得定义此类的二进制流,将这个字节流所代表的静态结构转化为方法区运行时的数据结构,内存中生成这个类的java.lang.Class对象 |
连接 | 这一阶段的作用主要是验证类数据和合法性、分配内存、将符号引用转换成内存中的直接引用,此时内存分配的仅仅是类的变量(static),不包括实例变量,实例变量会在对象实例化时候一起被分配到java堆中。此时实例变量值为初始值。 |
初始化 | 通过执行类构造器方法的过程,如果一个类父类还没有初始化,则先进行初始化。 |
使用 | 具体对类的操作 |
卸载 | 一系列清除处理 |
类加载
从上面可以整理下面内容:
- 类的实例和类的静态常量、静态变量不是关联的内容。访问类的静态常量和静态变量不需要创建类实例。
- 类的静态变量和静态常量保存于方法区中,只可能被加载一次。
- 静态变量和静态常量的操作会始终会优先于实例变量的操作。父类的操作会优先于子类。
- 运行时变量会在编译期就加载进内存(final),访问final数的操作不会触发类的加载,或者类实例的创建——具体可以搜索final字段的解释
- 类的构造代码块会优先于构造器——具体可以搜索类构造代码块的内容
后面两个是final字段的极少以及代码块内容的介绍,可以搜索相关知识点。
有了上面的了解我们可以尝试去理解下面测试中输出的内容。
基础类创建
现在先创建下面几个类用来测试
@Data
public class Parent {
private String name;
public static String tel = "1";
public final static String tel_final = "1";
static {
System.out.println("Parent static");
}
{
System.out.println("Parent init");
}
public Parent() {
tel = "2";
System.out.println("Parent Constructor");
}
public static int getNum() {
return 1;
}
}
@Data
public class Son extends Parent {
private String sonName;
public static String sonTel;
public final static String sonAdd = "add";
static {
System.out.println("Son static");
}
{
System.out.println("Son class");
}
public Son() {
System.out.println("Son Constructor");
}
}
public class ParentUtils {
/**
* 维护了一个静态的parent
*/
public static Parent parent = new Parent();
public static String name = "test";
}
@Data
public class ParentV2 {
/**
* 维护了一个的parent
*/
public Parent parent = new Parent();
public String name = "test";
}
下面尝试模拟了一些类的操作,然后可以得到注释上面的输出内容。
导致类实例创建的操作
new指令
/**
* new指令
* 顺序触发
* 静态代码块
* 触发代码块
* 触发构造函数
*/
@Test
public void testParent() {
Parent parent = new Parent();
}
newInstance方法
/**
* newInstance
* 顺序触发
* 静态代码块
* 触发代码块
* 触发构造函数
*/
@Test
public void testParentInstance() throws IllegalAccessException, InstantiationException {
Parent parent = Parent.class.newInstance();
}
new指令创建子类
/**
* 子类初始化
* 顺序触发
* 父类-静态代码块
* 子类-静态代码块
* 父类-代码块
* 父类-构造函数
* 子类-代码块
* 子类-构造函数
*/
@Test
public void testSon() {
Son son = new Son();
}
作为某对象的静态变量,当访问对象静态内容的时候
/**
* 访问其静态参数
* 顺序触发
* 静态代码块
* 触发代码块
* 触发构造函数
*/
@Test
public void testStatic() {
String str = ParentUtils.name;
System.out.println(str);
}
作为某对象的变量,当创建对象的时候
/**
* 访问其静态参数
* 顺序触发
* 静态代码块
* 触发代码块
* 触发构造函数
*/
@Test
public void testStatic() {
String str = ParentUtils.name;
System.out.println(str);
}
不会创建实例,只会触发类信息被加载
通过反射获得类信息
/**
* 反射获取类信息
* 顺序触发
* 静态代码块
*/
@Test
public void testClass() throws ClassNotFoundException {
Class <Parent> parent = (Class <Parent>) Class.forName("learn.clazz.entity.Parent");
}
访问类的静态信息
/**
* 调用其静态字段
* 顺序触发
* 父类-静态代码块
*/
@Test
public void testTestField() {
System.out.println(Parent.tel);
}
访问子类静态信息
/**
* 子类调用子类静态字段
* 顺序触发
* 父类-静态代码块
* 子类-静态代码块
*/
@Test
public void testSonStatic() {
System.out.println(Son.sonTel);
}
访问静态方法
/**
* 调用静态方法
* 静态代码块
*/
@Test
public void testTestMethod() {
int num = Parent.getNum();
}
动态语言调用静态方法
/**
* 调用1.7的动态语言
* @throws Throwable
* 静态代码块
*/
@Test
public void testLookup() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle testMethodHandle =
lookup.findStatic(ParentUtils.class,
"getMethod",
MethodType.methodType(int.class));
Object invoke = testMethodHandle.invoke();
}
无事发生
声明一个数组对象
/**
* 定义数组并不会导致初始化
*/
@Test
public void testArray() {
Parent[] parent = new Parent[5];
}
访问一个常量
/**
* Final 编译期间就会被保存起来,所以不会导致初始化
*/
@Test
public void testFinal() {
System.out.println(Parent.tel_final);
}
总结
假如之前了解过类加载和JVM的数据分布,然后去理解上面内容会非常简单。我这边尝试使用简单的方式去描述这一些列的关系。
类加载顺序
根据上面信息我们可以整理出下面的流程
静态常量在静态变量之前加载(访问静态常量不会触发静态代码块),静态变量在实例变量之前加载(一个类存在静态内容,其实例化的时候静态内容的加载会优先于父类实例化操作)
类加载内容
操作/触发行为 | 静态代码块 | 代码块 | 构造函数 | 子类静态代码块 | 子类代码块 | 子类构造函数 |
---|---|---|---|---|---|---|
new指令父类 | 触发(唯一) | 触发 | 触发 | — | — | — |
new指令子类 | 触发(唯一) | 触发 | 触发 | 触发(唯一) | 触发 | 触发 |
访问父类静态变量 | 触发(唯一) | — | — | — | — | — |
从子类访问父类静态变量 | 触发(唯一) | — | — | — | — | — |
访问子类静态变量 | 触发(唯一) | — | — | 触发(唯一) | — | — |
访问类静态常量 | — | — | — | — | — | — |
访问子类静态常量 | — | — | — | — | — | — |
这里需要注意,使用final static修饰的静态常量仅仅是不会触发当前类的加载和实例操作。
假如使用final static修饰对象,依旧会触发对象实例的相关行为。
所有代码内容都包含在下面地址中:https://gitee.com/daifylearn/class-learn
毕竟这个只是临时想到的内容随手写了写。可能有考虑不周的地方,假如存在麻烦给予指正。我会尽快修改。