首先引入一道面试题java
class Single {
private static Single single = new Single();
public static int count1;
public static int count2 = 0;
private Single() {
count1++;
count2++;
}
public static Single getInstance() {
return single;
}
}
public class Test {
public static void main(String[] args) {
Single single = Single.getInstance();
System.out.println("count1=" + single.count1);
System.out.println("count2=" + single.count2);
}
}
复制代码
错误答案:
count1=1;count2=1
正确答案:count1=1;count2=0
面试
为神马?为神马?这要从java的类加载时机提及。数据库
原本是准备把分析结果写在最下面的可是怕你们没有耐心看到最后我这边先大概分析下,若是看不懂下面的分析。建议你们能看到最后,文章不算长。数组
Single single = Single.getInstance();
调用了类的Single
调用了类的静态方法,触发类的初始化single=null count1=0,count2=0
single
赋值为new Single()
调用类的构造方法count=1;count2=1
count1
与count2
赋值,此时count1
没有赋值操做,全部count1
为1,可是count2
执行赋值操做就变为0类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为链接(linking)。 安全
其中加载、验证、准备、初始化和卸载五个步骤的顺序都是肯定的,解析阶段在某些状况下有可能发生在初始化以后,这是为了支持 Java 语言的运行期绑定的特性。 ![]()
什么状况下须要开始类加载过程的第一个阶段:"加载"。虚拟机规范中并没强行约束,这点能够交给虚拟机的的具体实现自由把握,可是对于初始化阶段虚拟机规范是严格规定了以下几种状况,若是类未初始化会对类进行初始化。网络
constant variable
),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种颇有用的优化,可是若是你须要改变final域的值那么每一块用到那个域的代码都须要从新编译。Class.forName("my.xyz.Test")
)接口的加载过程与类的加载过程稍有不一样。接口中不能使用
static{}
块。当一个接口在初始化时,并不要求其父接口所有都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。数据结构
对于静态字段,只有直接定义这个字段的类会被初始化,若是是经过子类引用父类的字段,父类会被初始化,子类不必定会被初始化,子类会不会被初始化 JVM 虚拟机规范并无明确规定,取决于虚拟机的具体实现多线程
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 1;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
System.out.println("The value is " + Subclass.value);
}
}
复制代码
上面代码运行以后输出结果以下所示编辑器
SuperClass init!
The value is 24
复制代码
public class SubClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
SubClass[] subClassArray = new SubClass[10];
}
}
复制代码
上面代码运行以后,并不会输出 "SubClass init!
",由于在上面Demo#main()
方法中,并无初始化SubClass
类,而是初始化了一个SubClass[]
数组类,SubClass[]
数组类表明了一个元素类型为SubClass
的一维数组,继承自Object
类,由newarray
字节码建立。布局
public class Constant {
static {
System.out.println("Constant init!");
}
public static final String VALUE = "Hello World!";
}
public class Demo {
public static void main(String[] args){
System.out.println(Constant.VALUE);
}
}
复制代码
上面代码运行以后也并不会输出"Constant init!
",由于这涉及到一个概念 ---- “常量传播优化”。虽然在代码中Demo
类引用了Constant
类中的常量VALUE
,可是在编译阶段,会将VALUE
的实际值"Hello World!
"放到Demo
类中的常量池中,Demo
类每次使用"Hello World!
"常量的时候都会从本身的常量池中去找。Demo
类不会持有Constant
类的符号引用,因此Constant
类也并不会被初始化。
在加载阶段有三个步骤:
java.lang.Class
的对象,做为方法区这些数据的访问入口 在这个阶段,有两点须要注意:.class
静态存储文件中获取,也能够从zip、jar
等包中读取,能够从数据库中读取,也能够从网络中获取,甚至咱们本身能够在运行时自动生成。java.lang.Class
对象以后,并无规定此Class
对象是方法Java
堆中的,有些虚拟机就会将Class
对象放到方法区中,好比HotSpot
。验证是链接阶段的第一个步骤,验证的目的是为了确保.class
文件中的字节流所包含的信息是符合当前虚拟机的要求,而且不会危害到虚拟机自身的安全的。
Java
语言自己是相对安全的语言,使用Java编码是没法作到如访问数组边界之外的数据、将一个对象转型为它并未实现的类型等,若是这样作了,编译器将拒绝编译。可是,Class
文件并不必定是由Java
源码编译而来,可使用任何途径,包括用十六进制编辑器(如UltraEdit
)直接编写。若是直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。
不一样的虚拟机,对类验证的实现可能有所不一样,但大体都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
private、protected、public、default
)是否可被当前类访问验证阶段对于虚拟机的类加载机制来讲,不必定是必要的阶段。若是所运行的所有代码确认是安全的,可使用-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一块儿分配在Java堆中。
有三点须要注意:
static
修饰的变量),而不包括实例变量,实例变量将会跟随着对象在 Java 堆中为其分配内存0
值,好比有以下类变量,在准备阶段完成以后val
的值是0
而不是 123
,为 val
复制为123
,是在后面要讲的初始化阶段以后public static int val=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。
复制代码
ConstantValue
属性当中,因此在准备阶段结束以后,常量的值就是ConstantValue
所指定的值了,好比以下,在准备阶段结束以后,val
的值就是123
了。public static final int val = 123;
复制代码
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到内存中。 直接引用(Direct Reference):直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,若是有了直接引用,那么引用的目标一定已经在内存中存在。
类的初始化阶段才是真正开始执行类中定义的 Java 程序代码。初始化说白了就是调用类构造器<clinit>()
的过程,在类的构造器中会为类变量初始化定义的值,会执行静态代码块中的内容。下面将介绍几点和开发者关系较为紧密的注意点
<clinit>()
是由编译器自动收集类中出现的类变量、静态代码块中的语句合并产生的,收集的顺序是在源文件中出现的顺序决定的,静态代码块能够访问出如今静态代码块以前的类变量,出现的静态代码块以后的类变量,只能够赋值,可是不能访问,好比以下代码public class Demo {
private static String before = "before";
static {
after = "after"; // 赋值合法
System.out.println(before); // 访问合法,由于出如今 static{} 以前
System.out.println(after); // 访问不合法,由于出如今 static{} 以后
}
private static String after;
}
复制代码
<clinit>()
类构造器和<init>()
实例构造器不一样,类构造器不须要显示的父类的类构造,在子类的类构造器调用以前,会自动的调用父类的类构造器。所以虚拟机中第一个被调用的<clinit>()
方法是 java.lang.Object
的类构造器static{}
代码块也优先于子类的static{}
执行<clinit>()
对于类来讲并非必需的,若是一个类中没有类变量,也没有static{}
,那这个类不会有类构造器<clinit>()
static{}
,可是接口中也能够有类变量,因此接口中也能够有类构造器 <clinit>{}
,可是接口的类构造器和类的类构造器有所不一样,接口在调用类构造器的时候,若是不须要,不用调用父接口的类构造器,除非用到了父接口中的类变量,接口的实现类在初始化的时候也不会调用接口的类构造器<clinit>()
方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只有一个线程去执行这个类的类构造器<clinit>()
,其余线程会被阻塞,直到活动线程执行完类构造器<clinit>()
方法看到这里不容易了,你们应该都理解类加载的流程了吧,但愿之后遇到这样的面试题能想起这篇文章
能够和博主一块儿交流: