一、 编译
??我们知道Java代码在运行时,首先编译器会把我们的代码编译成一个.Class文件,Java中有句话“一次编译,到处运行”,能到处运行的原因和这个Class文件有很大的关系。
二、类加载
??我们知道每一个类都会生成一个.Class文件,这是一个字节码文件,其实就是一串二进制的比特流;一个类的生命周期从被类加载开始,到从内存中卸载结束,一共被分为加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中从加载到初始化这五个过程被称为类加载过程。系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行的java程序处于两个不同的JVM进程中,两个jvm之间并不会共享数据。
加载
??这个阶段首先会获取.Class文件中的二进制字节流,然后会把这个字节流的静态存储结构转化为方法区的运行时数据结构,也就是在方法区中储存这个类的静态成员便变量(这里只是有了一个结构,但是还没有初始化,就是还没有赋值),最后会在内存中生成一个代表整个类的java.lang.Class 对象,这个Class对象就相当于一个模板一样,后续如果我们要new一个类对象时,就会参照这个Class对象,并且还会为访问方法区的静态变量提供一个入口,我们访问静态变量时是通过类名去访问的,其实就是通过这个Class对象去访问的静态变量。
??注意获取二进制字节流时不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类),还可以由程序员自己决定。
验证
??这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。就比如,在Java中不允许发生像访问数组的边界值、将一个类型转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果有这种情况首先就会编译报错,但是如果出现了bug,通过了编译,或者是通过其他的途径生成了Class文件,如果说虚拟机不检查输入的字节流,很可能因为载入了有害的字节流而导致系统崩溃。因此,验证是虚拟机对自身保护的一项重要工作。验证主要分一下几个方面:
- 文件格式验证:主要目的是保证输入的字节流能正确的解析并存储于方法区内,格式上符合描述一个java类型信息的要求。
- 元素数据验证:主要目的是对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
- 字节码验证:这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:符号引用验证可以看做是对类自身以外(常量池中各种符号引用)的信息进行匹配校验,符号引用验证的目的是确保解析动作能够正常执行。
准备
??这个阶段会正式为静态变量分配内存空间并为静态变量赋初值,这个内存分配是发生在方法区中。这个阶段要注意以下几个点:
- 这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。
- 赋初值的意思并不是初始化,赋初值是把变量的值设置为对应类型的默认值,我们知道每一个基本类型的变量都有一个自己的默认值,像int的默认值是0,boolean的默认值为false。
- 如果这个静态变量前加了final字段,这个变量在这个阶段就会直接被赋值,举个例子,public static final int v = 8080;这个语句如果不加final字段,准备阶段结束v的值应该为0,但是加了final之后,准备阶段结束v的值就为8080了,因为在编译阶段会为 v 生成一个 ConstantValue 属性,在准备阶段虚拟机就会会根据 ConstantValue 属性将 v赋值为 8080。
解析
??这个阶段JVM会将常量池中的符号引用替换为直接引用,上面我们介绍了在验证阶段验证符号引用就是为这个阶段做铺垫,那么什么是符号引用什么是直接引用呢?
- 符号引用:在写代码时,我们对引用这个概念肯定不会陌生,引用引的是一段地址,但是在编译时,JVM却不知道这个地址的具体含义,所以就把这些地址转换成了一些符号地址,就叫符号引用,符号引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
- 直接引用:这就相当于把地址再转换回来,直接引用可以是直接指向目标的指针(这个可以是Class对象中指向静态成员的引用,可以是指向静态方法的引用,也可以是直接指向Class对象的引用),也可以是相对偏移量(比如指向实例变量、实例方法的直接引用都是偏移量),如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
??初始化是类加载机制的最后一步,这个时候才正真开始执行类中定义的Java程序代码。在前面准备阶段,类变量已经赋过一次系统要求的初始值,在初始化阶段最重要的事情就是对类变量进行初始化,对类变量的赋值有两种方式,一种是声明类变量时指定初始值,第二种是在静态代码块中对类变量赋值,这个阶段主要关注的重点是父子类之间各类资源初始化的顺序。
??在为静态成员赋值时会执行我们的构造器cilent()方法,cilend()方法只会在第一次初始化时执行,client()方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子client()方法执行之前,父类的client()方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成client()方法。
public class A{
public static int a = 123;
}
public class B{
public static int b;
static{
b = 123;
}
}
??首先我们要知道类加载的前四个过程是由JVM完成的,最后这个过程是通过代码实现的,那么什么时候会触发初始化这个过程我们就一定要知道:
- 在使用new关键字实例化对象时。
- 通过反射创建类的实例化对象时。
- 调用某个类的静态方法时。
- 访问某个类的类变量或为类变量赋值时。
- 当初始化子类时,它的所有父类都会被初始化。
??除了上述几种情况会发生初始化外,还有几种不会发生初始化的情况,我们也需要知道。
1、子类引用父类的静态变量,不会导致子类初始化。
public class A{
public static int a = 123;
static{
System.out.println("A init");
}
}
public class B extends S{
static{
System.out.println("B init");
}
}
public class Test{
public static void main(String[] args){
System.out.println(B.a);
}
}
-----------------------------------------------------------------------------------
结果:B init
123
2、 定义类数组的时候,不会初始化这个类
public class A{
static{
System.out.println("A init");
}
}
public class Test{
public static void main(String[] args){
SupClass[] spc = new SupClass[10];
}
}
-----------------------------------------------------------------------------------
结果:
3、引用被final修饰的常量时,不会初始化这个类
public class A{
public static final int a = 123;
static{
System.out.println("A init");
}
}
public class Test{
public static void main(String[] args){
System.out.println(A.a);
}
}
-----------------------------------------------------------------------------------
结果:123
父子类之间各类资源初始化的顺序
public class A{
public A{
System.out.print("A.构造");
}
static{
System.out.print("A.static");
}
{
System.out.print("A.{}");
}
}
public class B extends A{
public B{
System.out.print("B.构造");
}
static{
System.out.print("B.static");
}
{
System.out.print("B.{}");
}
}
public class Test extends B{
public static void main(String[] args){
System.out.print("开始");
new B;
new B;
System.out.print("结束");
}
}
-----------------------------------------------------------------------------------
结果:A.static B.static 开始 A.{} A.构造 B.{} B.构造 A.{} A.构造 B.{} B.构造 结束
??首先要调用main()方法,就需要先加载Test这个类,因为Test继承自B,所有就要加载B,B又继承自A,所以就还要加载A(最终要加载Object类),然后就会先执行A的静态代码块再执行B的静态代码块。在之后的顺序是实例代码块,然后才是构造方法。
??本地代码块的执行时机是调用一个类的构造函数之前,本地代码块一般的作用是为实例成员赋值。我们的实例成员在new一个新对象时会先被创建出来,然后被赋予一个初值(也就是基本变量的默认值),然后这时如果实例代码块中有赋值操作,就会直接给实例成员赋值。
??static代码块只在类加载时执行,类是用类加载器来读取的,类加载器是带有一个缓存区的,它会把读取到的类缓存起来,所以在一次虚拟机运行期间,一个类只会被加载一次,这样的话静态代码块只会运行一次。
三、双亲委派模型
??当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 双亲委派模型的好处:
- 避免有些类被重复加载。
- 避免程序员自己定义的类名和库中的类名重复。
|