先看一个令人dan teng的面试题
public class Singleton
{
public static Singleton s=new Singleton();
public static int k1;
public static int k2=0;
private Singleton(){
k1++;
k2++;
}
public static void main(String[] args)
{
System.out.println(Singleton.k1);
System.out.println(Singleton.k2);
}
}
请问输出什么?
如果是下面这样呢?
public class Singleton
{
public static int k1;
public static int k2=0;
public static Singleton s=new Singleton();
private Singleton(){
k1++;
k2++;
}
public static void main(String[] args)
{
System.out.println(Singleton.k1);
System.out.println(Singleton.k2);
}
}
诸位看官莫急,咱们慢慢来从虚拟机的层次来看这个问题。
什么是虚拟机?虚拟机是干什么的?
这些资料大家百度,我就不赘述了。
虚拟机的生命周期
在以下几种情况下,java虚拟机将结束生命周期。1 执行了System.exit()方法
通过查看api文档,我们exit的参数为int,当参数为0就是正常结束,否则就是非正常结束。
2 程序正常结束
3 程序执行过程中遇到异常或错误
4 操作系统出现错误
类的加载,连接与初始化
大体的图如下
加载: 查找并加载类的二进制数据
连接:
-验证 确保被加载的类的正确性
-准备 为类的静态变量分配内存,并将其初始化为默认值(int就是0 boolean就是 false 引用类型就是null)
-解析 把类中的符号引用变为直接引用
初始化: 为类的静态变量赋予正确的初始值
不知道诸位看官是否有什么疑问?
如果大家没有,我倒是有个问题,在我们最开始编写java的时候总是javac命令编译成class字节码,java命令运行。如果java代码有什么问题,在javac的时候就会抛出问题,换句话说等我们连接class文件的时候它肯定是没问题的,那还验证什么呀?
答案就是:
如果class的产生只能通过javac命令的话,那就没有任何问题了,可关键就是人们也可以手动产生class文件,所以验证这一步还是有用的。
第二个问题 在连接的准备阶段有把静态变量初始为默认值,但是在初始化的阶段也有为类的静态变量赋予正确的初始值。这两个步骤有什么关系?
看如下代码片
public class test{
public static int a=5;
public static int b;
public static int c;
static{
c=10;
}
public static Person p;
}
在准备阶段,a与b的值都是0(因为int型默认都是0) p为null(Person 就是简单的一个类)
到初始化阶段,a就是5了,b仍然是0,c是10,p还是null。
换句话说,只有在初始化阶段,赋值的=才发挥作用。(对a的赋值与下面的static代码块是一回事)
(准备阶段与初始化阶段对静态变量的赋值顺序,都是按照变量的书写顺序来的。上面的例子就是先给a再给b,然后c,最后p)
一步一步来
加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。如下图
类的加载的最终产品是位于堆区中的Class对象
这里就有另一个名词了Class对象。
就像每一本书有作者,价格等描述自身一些熟悉的信息,每个类也都有一个描述自己信息的东西,它就是class对象。
例如
public class Person{
public String name;
public String getName(){
return name;
}
}
这个类的Class对象里就包含了这个类里面有几个属性,每个属性是什么类型,有什么方法,每个方法的参数都是什么,返回值都是什么等等。
具体信息,参考api。
谈到类的加载,就得说说类的加载器了。
java中有两种类的加载器
– Java虚拟机自带的加载器
根类加载器(Bootstrap)
(用c++实现 大家都看不到)
扩展类加载器(Extension)
系统类加载器(System)
(扩展类加载器与系统类加载器是用java写的)
-用户自定义的类加载器
java.lang.ClassLoader的子类
用户可以定制类的加载方式
看如下代码片
Class c1=Class.forName("java.lang.String");
Class c2=Class.forName("Singleton"); //就是上面的那个Singleton
System.out.println(c1.getClassLoader());
System.out.println(c2.getClassLoader());
运行结果就是
null
sun.misc.Launcher$AppClassLoader@fb56b1
String是Bootstrap加载的,用c++写的我们看不到,所以是null
AppClassLoader就是系统加载器
连接之验证
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。如下图
连接之准备
上文已经解释。连接之解析
如下图至于什么是符号引用,大家在eclipse中,打开一个class文件
上面红框里面的就是符号引用
初始化
主动使用
被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“ 首次主动使用”时才初始化他们。
主动使用包含以下六种情况
创建类的实例 (如在代码中new Person())
访问某个类或接口的静态变量,或者对该静态变量赋值 (Singlean.a=8)
调用类的静态方法 (Singleton.getInstance();)
反射(如Class.forName(“com.shengsiyuan.Test”)
初始化一个类的子类 (有father类,有child类,且child继承或实现father类。 )
Java虚拟机启动时被标明为启动类的类(调用java启动命令 如Java Test)
除了以上六种,别的调用方法都是被动使用。
再说说最开始的那个题目
public static Singleton s=new Singleton();
public static int k1;
public static int k2=0;
private Singleton(){
k1++;
k2++;
}
在连接的准备阶段 k1,k2都是0,s是null
到了初始化阶段, s的初始化调用了构造函数
k1,k2都从0变成了1
继续往下阅读
public static int k1;
public static int k2=0;
k1没有被赋值还是1
但是k2却被赋值为0了
所以最后的结果就是
1
0
至于把
public static Singleton s=new Singleton();
放到后面,大家自己分析吧.