类在内存中的加载过程
Java类加载内存分析
(方法区在堆里,为了区别于Java堆有个别名叫非堆Non-Heap)
类的加载过程
-
加载
类加载器将类的.class文件(字节码)内容加载到内存中,(out目录里可以看到每个类对应一个.class文件)
并将这些静态数据转换成方法区的运行时数据结构,
然后(在栈里)生成一个代表这个类的java.lang.Class对象
-
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程。(我理解:java代码适配JVM运行状态的过程)
-
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
-
准备:正式为类变量(static)分配内存并设置类变量默认初始值(指0、0.0、null)的阶段,这些内存都将在方法区中进行分配。
-
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
“准备”一步,不同JVM/不同运行情况根据相同的符号引用会分配到不同的内存地址,也就是得到不同的直接引用;
“解析”一步,用JVM得到的直接引用覆盖虚拟机常量池中的符号引用。
-
-
初始化(JVM执行)
-
产生并执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集所有类变量的赋值动作(声明并赋初值)和static静态代码块中的语句合并(合并考虑语句顺序)产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
/* 不同于方法中的“变量要先声明才能使用”,类加载过程在链接-准备阶段就扫描到类似"static int m……"的语句,给类变量分配了内存,在初始化阶段只是考虑顺序地将静态代码块里的赋值语句和声明并赋初值的语句合并在一起。 */ //1 static {m = 300;}//静态代码块 static int m = 100;//“声明并赋初值” //2 static int m = 100; static {m = 300;} //上述1和2都是正确的,1的m值最后是100,2的m值最后是300. //3 static {m += 300;}//非法前向引用 static int m = 100; //4 static int m = 100; static {m += 300;} //由于合并产生<clinit>()方法考虑顺序,3中的"m+=300"是非法前向引用,4则合法。 //相当于把"static int m = 100;"拆成"static int m;"和"static{m=100};"再把"static int m;"提到最前面执行、所有static静态代码块中赋值语句顺序执行
-
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
class A{ static int m = 100;//声明并赋初值 static { System.out.println("A类初始化"); m += 1100; }//静态代码块 } class B extends A{ static { m += 300; System.out.println("B类初始化"); } } public class Test05 { public static void main(String[] args) throws ClassNotFoundException { System.out.println(B.m); B.m += 10; System.out.println(Class.forName("A")); System.out.println(B.m); System.out.println(Class.forName("B")); System.out.println(B.m); } }
输出结果:
A类初始化 1200 class A 1210 B类初始化 class B 1510
表现:(类初始化的时机)
- 使用、修改继承来的变量(以及没有重写的方法)会触发父类初始化,但不会触发自己初始化
- 声明对象数组只是开辟空间,也不会引起所属类初始化
- 使用Class对象或new一个本类对象会触发初始化
- 已经初始化过一次就不会再触发
-
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
-