1.JDK/JRE/JVM的关系:JDK 8是JRE 8的超集,包含了JRE 8中的所有内容,编译器和调试器等开发applet和应用程序。JRE 8提供了库、Java虚拟机(JVM)和运行用Java编程编写的applet和应用程序的其他组件语言。注意,JRE包含了Java SE不需要的组件,规范,包括标准和非标准Java组件。 目前工作中常用的JDK版本为1.8,所以定位到官网:https://docs.oracle.com/javase/8/docs/index.html
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。 在这之前,我们会通过Javac命令将.java文件编译成.class文件。比如如下源码文件: public class Person { private String name; private int age; private static String address; private final static String hobby = "Programming"; public void say() { System.out.println("Hello,Person"); } public int calc(int op1, int op2) { return op1 + op2; } } 2.编译过程:编译的过程大致可以分为以下几个步骤:Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器-> 注解抽象语法树 -> 字节码生成器 -> Person.class文件 类文件(Class文件):oracle官网对于类文件结构的描述页面:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html 通过javac编译得到的.class文件我们用文件编辑器打开会发现:
Class文件是一组以8位字节为基础单位的二进制流。每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Calss文件。也就是 CA FE BA BE 这四个字节。类似于身份标识。 0000 0034 对应10进制的52,代表JDK 8中的一个版本。 0027 对应十进制27,代表常量池中27个常量。 ClassFile {
u4 magic; 魔数
u2 minor_version; class文件的次要版本号
u2 major_version; class文件的主要版本号
u2 constant_pool_count; 常量个数
cp_info constant_pool[constant_pool_count-1]; 代表各种串常量,类和接口名,字段名
u2 access_flags; 访问标志
u2 this_class; 类索引
u2 super_class; 父类索引
u2 interfaces_count; 接口个数
u2 interfaces[interfaces_count]; 接口名
u2 fields_count; 属性个数
field_info fields[fields_count]; 属性名表数组
u2 methods_count; 方法个数
method_info methods[methods_count]; 方法表数组
u2 attributes_count; 附加属性个数
attribute_info attributes[attributes_count]; 附加属性的表数组
}
对class文件有了一个简单的了解之后进入正题,他是怎么被JVM进行处理的。 3.类文件到虚拟机(类加载机制):JVM 将类的加载过程分为三个大的步骤:加载(loading),链接(link),初始化(initialize)。其中链接又分为三个步骤:验证,准备,解析。
加载(loading): 加载是类加载过程中的第一个阶段,加载过程虚拟机需要完成以下三件事情:
这个过程主要就是类加载器完成。 链接(link):链接分为3个小部分,验证、准备、解析。 验证:验证的目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证; 准备:给静态方法和静态变量赋予初值,比如static int a;给其中的a赋予初值为0,但是这里不会给final修饰的静态变量赋予初值,因为被final修饰的静态变量在编译期间就已经被赋予初值了;内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。例如下面的代码在准备阶段之后,num 的值将是 7,而不是 0。public static final int num = 7; 解析:主要将常量池中的符号引用替换为直接引用的过程。 初始化(initialize): 到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
4.类装载器ClassLoader:JVM 类加载器作用,将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。类加载器是通过ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
分类:
加载原则: 检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 双亲委派机制:定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己去加载的话,那么系统中会存在多种不同的Object类。 我们可以通过JDK1.8的源码 ClassLoader 的 loadClass(String name, boolean resolve) 来一探究竟 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded //首先,检查这个class是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) {// 等于null,没有被加载过 long t0 = System.nanoTime(); try { if (parent != null) {//父加载器是否为空 c = parent.loadClass(name, false);//存在父加载器,使用父加载器加载 } else {//若其父类加载器为null,则说明本类加载器为扩展类加载器,父类加载器为启动类加载器,尝试使用bootstrap classloader进行类的加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) {若c为空,则父类加载器加载失败 // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name);//尝试使用自定义类加载器进行加载 // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {//通过传入的标识来控制是否要对该类进行初始化操作 resolveClass(c); } return c; } } 源码中将双亲委派机制体现的淋漓尽致,详细看了这个源码的小伙伴对双亲委派机制不会再陌生了。 实战分析:了解了类加载的基本概念即流程后,我们需要通过具体的例子来加深印象: 例子1: class School {
static {
System.out.println("School 静态代码块");
}
}
class Teacher extends School {
static {
System.out.println("Teacher 静态代码块");
}
public static String name = "Tony";
public Teacher() {
System.out.println("I'm Teacher");
}
}
class Student extends Teacher {
static {
System.out.println("Student 静态代码块");
}
public Student() {
System.out.println("I'm Student");
}
}
class InitializationDemo {
public static void main(String[] args) {
System.out.println("Teacher's name: " + Student.name); //入口
}
}
以上这个例子的输出会是什么样得呢?我们一步一步来分析,也许会有人问为什么没有输出"Student 静态代码块"?这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 对面上面的这个例子,我们可以从入口开始分析一路分析下去:
例子2: class School {
static {
System.out.println("School 静态代码块");
}
public School() {
System.out.println("I'm School");
}
}
class Teacher extends School {
static {
System.out.println("Teacher 静态代码块");
}
public Teacher() {
System.out.println("I'm Teacher");
}
}
class Student extends Teacher {
static {
System.out.println("Student 静态代码块");
}
public Student() {
System.out.println("I'm Student");
}
}
class InitializationDemo {
public static void main(String[] args) {
new Student();
}
}
首先在入口这里我们实例化一个 Student 对象,因此会触发 Student 类的初始化,而 Student 类的初始化又会带动 Teacher、School 类的初始化,从而执行对应类中的静态代码块。因此会输出:School 静态代码块、Teacher 静态代码块、Student 静态代码块。当 Student 类完成初始化之后,便会调用 Student 类的构造方法,而 Student 类构造方法的调用同样会带动 Teacher、School 类构造方法的调用,最后会输出:I'm School、I'm Teacher、I'm Student。 例子3: class Teacher {
public static void main(String[] args) {
staticFunction();
}
static Teacher teacher = new Teacher();
static {
System.out.println("teacher 静态代码块");
}
{
System.out.println("teacher普通代码块");
}
Teacher() {
System.out.println("teacher 构造方法");
System.out.println("age= " + age + ",name= " + name);
}
public static void staticFunction() {
System.out.println("teacher 静态方法");
}
int age = 24;
static String name = "Tony";
}
这个例子中,main 主方法所在类有许多代码,这个点我们需要关注到。
首先按代码顺序收集所有静态代码块和类变量进行赋值,即执行以下代码并且将 name 初始化为 null static Teacher teacher = new Teacher();
static {
System.out.println("teacher 静态代码块");
}
static String name = "Tony";
而这里触发了对象的构造器(先收集成员变量赋值,后收集普通代码块,最后收集对象构造器,最终组成对象构造器),从而执行: int age = 24; //将age赋值为24
{
System.out.println("teacher普通代码块");
}
Teacher() {
System.out.println("teacher 构造方法");
//此时 name 还没赋值,所以是null
System.out.println("age= " + age + ",name= " + name);
}
最后执行以下语句: static {
System.out.println("teacher 静态代码块");
}
//此刻执行name赋值为 Tony 的操作
public static void staticFunction() {
System.out.println("teacher 静态方法");
}
通过以上例子我们可以得出以下结论:
|
|
来自: 昵称15242507 > 《Java》