图例
类加载子系统作用
- 类加载子系统负责从文件系统中或者网络中加载class文件,class文件在文件的开头有特定的文件标识
- ClassLoader只负责class文件的加载,至于它是否可以运行则有ExecutionEngine决定
- 加载完成的类的信息存放在一块叫方法区的内存空间。除了类的信息外,方法区还会存放运行时常量池信息,可能还会存放字符串字面量和数字常量(这部分常量信息是class文件常量池部分的内存映射)
类的加载过程
1. 加载(Loading)
1.1 通过一个全限定名获取一个定义此类的二进制字节流
1.2 将这个字节流所代表的静态存储结构转化为方法区(方法区是一个抽象概念,具体看JDK版本。JDK7及以前叫永久代,JDK8之后叫元空间。)的运行时数据结构
1.3 在内存中生成一个代表这个类的java.long.Class对象(用过反射机制这类都会有印象),作为方法区这个类的各种数据的访问入口
1.4 补充:加载.class文件的方法
1.4.1 从本地系统中直接加载
1.4.2 通过网络获取,典型场景:Web Applet
1.4.3 从zip包中读取:jar、war等格式
1.4.4 运行时动态生成:jdk原生支持的动态代理、cglib技术
1.4.5 有其他文件生成:JSP应用
1.4.6 从转悠数据库中读取.class文件
1.4.7 从加密文件中获取,典型的防class文件被反编译的保护措施
2. 连接(Linking)
2.1 验证(Verify)
2.1.1 目的在于确保class文件的字节流中保函信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身的安全
2.1.2 主要包含四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
2.2 准备(PrePare)
2.2.1 为类变量分配内存并设置该类变量的默认初始值,即零值(就是java中8种基本数类型的初始值,见下面代码)
申明java中的8中基本数类型:
public class HelloWord {
int i;
byte b;
short s;
long l;
float f;
double d;
boolean boo;
char c;
@Override
public String toString() {
return "HelloWord{" +
"i=" + i +
", b=" + b +
", s=" + s +
", l=" + l +
", f=" + f +
", d=" + d +
", boo=" + boo +
", c=" + c +
'}';
}
}
调用main方法:
public static void main(String[] args) {
HelloWord helloWord=new HelloWord();
System.out.println(helloWord.toString());
}
打印结果:
HelloWord{i=0, b=0, s=0, l=0, f=0.0, d=0.0, boo=false, c= }
2.2.2 这里不包含用final修饰的static,因为final在编译的时候就会分配了
2.2.3 这里不会为实例变量初始化值,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中
2.3 解析(Resolve)
2.3.1 将常量池内的符号引用转换为直接引用的过程
2.3.2 事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行
3. 初始化
3.1 初始化阶段就是执行类构造器方法<clinit>()的过程(jdk环境为1.8,IDEA安装了jclasslib插件后查看)
3.2 此方法不需要定义,是java编译器自动收集类中的所有的变量的赋值动作和静态代码块中的语句合并而来的
3.3 构造器方法中的指令按照语句在源文件中出现的顺序执行
3.4 <clinit>()不同于类的构造器
3.5 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
3.6 虚拟机保证一个类的<clinit>()方法在多线程下被同步加锁