深入理解Java中JVM对类的加载机制

有如下经典代码:

  1. package test;
  2. class Singleton {     
  3. public static Singleton singleton = new Singleton();     
  4. public static int a;     
  5. public static int b = 0;     
  6. private Singleton() {     
  7. super();     
  8.         a++;     
  9.         b++;     
  10.     }     
  11. public static Singleton GetInstence() {     
  12. return singleton;     
  13.     }     
  14. }     
  15. public class MyTest {    
  16. public static void main(String[] args) {     
  17.         Singleton mysingleton = Singleton.GetInstence();     
  18.         System.out.println("a="+mysingleton.a);     
  19.         System.out.println("b="+mysingleton.b);     
  20.     }     
  21. }    

运行结果为:a=1,b=0;

如果将代码加以修改,将原来的3-5行代码如下:

public static Singleton singleton = new Singleton();     

public static int a;     

public static int b = 0;

修改为

public static int a;     

public static int b = 0;     

public static Singleton singleton = new Singleton();

运行程序,结果为a=1,b=1

为什么改变了成员变量的顺序,运行结果会有所不同?为了弄清这个问题,就必须先理解Java中JVM加载类的机制。

java程序运行需要使用某个类时,如果该类还没有加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如下图所示:

 

 

1.类加载
当我们运行java.exe命令执行某个java程序时,由于java程序本身以.class字节码的形式存在,它不是一个可执行文件,所以需要JVM将类文件加载到内存中。
JVM中有三个类加载器:根类加载器、扩展类加载器和系统类加载器(也叫应用加载器)。根类加载器是C++实现的,扩展类加载器和应用加载器都是java语言实现的。
当运行java.exe命令执行一个java程序时,程序最基本的加载流程如下:
1.java.exe程序搜索jre目录,寻找JVM.dll,启动JVM
2.JVM运行根类加载器,加载java核心类(所有java.*开头的类)
3,根类加载器运行后,它会自动加载扩展类加载器和系统类加载器,并将扩展类加载器的父类设置为根类加载器,将应用加载器的父类设置为扩展累加载器。
4.扩展类加载器搜索jre/lib/ext目录,加载扩展API。
5.应用加载器搜索CLASSPATH目录,加载我们要运行的类。
6.类的class文件读入内存后,就会创建一个java.lang.Class对象,一旦某个类被载入JVM中,同一个类就不会再次被载入
一个类加载后,对应的Class对象,可以通过该类的实例的getClass()方法获得,Class对象有一个getClassLoader()方法,可以得到加载该类所用的类加载器。
2.连接
当类被加载后,系统就会位置创建一个对应的Class对象,接着进入连接阶段,连接又分为以下三个阶段:
1.验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致。
2.准备:负责为类的静态属性分配内存,并设置默认初始值。注意:必须是静态属性!因为此时并不存在实例对象,设置值也是默认值初始值,而不是人为给定的值
3.解析:将类的二进制数据中的符号引用替换成直接引用。
3.初始化
JVM负责对类进行初始化,也就是对静态属性进行初始化。java中对静态属性指定初始值的方式有两种:①声明静态属性时指定初始值;②使用静态初始化快为静态属性指定初始值。
需要注意的是JVM对一个类初始化时如果该类的父类没有被初始化,则会先初始化其父类,如果直接父类还有父类,那么会先初始化父类的父类,以此类推。所以,JVM最先初始化的总是java.lang.Object类,
当程序主动使用任何一个类时,系统会保证该类以及它的所有父类都会被初始化。
当java程序首次通过下面的六种方式来使用某个类或者接口时,系统就会初始化该类或者接口:
♦创建类的实例
调用某个类的静态方法
访问某个类或接口的静态属性,或者为静态属性赋值
使用反射方式强制创建某个类或接口对应的java.lang.Class对象
初始化某个类的子类
直接用java.exe命令运行某个主类。

回顾那个诡异的代码


从入口开始看

Singleton mysingleton = Singleton.GetInstence();

是根据内部类的静态方法要一个Singleton实例。这个时候就属于主动调用Singleton类了。之后内存开始加载Singleton类

1):对Singleton的所有的静态变量分配空间,赋默认的值,所以在这个时候,singleton=null、a=0、b=0。注意b=0是默认值,并不是我们手工为其赋予的的那个0值。(这一步对应连接阶段)

2):之后对静态变量赋初始值,这个时候的赋值就是我们在程序里手工初始化的那个值了。(这一步对应初始化阶段)

按照顺序,首先初始化静态变量singleton = new Singleton();调用了构造方法。构造方法里面a=1、b=1。之后接着顺序往下执行。

接下来public static int a;为a赋初始值,因为没有指定初始值,所以a保持不变,即a=1,

再之后public static int b = 0;为b赋初始值,代码中指定的初始值为0,此时b之前的1被0覆盖,因而b=0;  所以最终结果为a=1,b=0。

而如果将代码中静态属性的声明顺序改为如下:

public static int a;    

public static int b = 0;     

public static Singleton singleton = new Singleton();

那么先初始化a,a没有指定初始值,保持不变,a仍为默认值0,再初始化b,b的指定初始值为0,所以b=0,

最后初始化Singleton singleton = new Singleton();调用构造器,其中a++,b++,所以最终结果为a=1,b=1


 

posted @ 2016-05-24 00:06  skip_2_my_lou  阅读(1286)  评论(0编辑  收藏  举报