[color=red]【本文整合多篇网文+验证扩展而成】[/color]
[b]1. 静态变量(块)初始化[/b]
[b]1.1 大概了解Java虚拟机初始化的原理[/b]
JVM通过加装、连接和初始化一个Java类,使该类可以被正在运行的Java程序所使用。装载和连接必须在初始化之前就要完成。Java类型的生命周期:
[img]http://dl2.iteye.com/upload/attachment/0093/7849/838a8b68-41d8-307a-93e4-34b42484a987.bmp[/img]
1):[b]类加载load[/b]:从字节码二进制文件*.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的[color=red]方法区[/color]内,之后在[color=red]堆区[/color]建立一个java.lang.Class对象,用来封装方法区的数据结构。这个时候就体现出了万事万物皆对象了,干什么事情都得有个对象。就是到了最底层究竟是鸡生蛋,还是蛋生鸡呢?[color=red][b]类加载的最终产物就是堆中的一个java.lang.Class对象。
[/b][/color]
2):[b]连接[/b]:连接又分为以下小步骤
[b]验证[/b]:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。
[b]准备[/b]:将类的[color=red]静态变量[/color]进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情,只是初始化默认值,而不是赋值)
[b]解析[/b]:把类的符号引用转为直接引用(保留)
3):[b]类的初始化(不是对象实例的初始化)[/b]: 将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值([color=red]这是跟准备阶段的区别[/color])。
类初始化阶段,主要是为类变量[color=red](注意是静态变量,如果静态变量初始化时需要间接初始化其他类实例则转而去初始化依赖的变量,也就是<cinit>阶段遇到<init>就先执行完<init>再继续执行<cinit>)[/color]赋予正确的初始值。这里的“正确”初始值指的是程序员希望这个类变量所具备的起始值。一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。初始化一个类包含两个步骤:
1) 如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
2) 如果类存在一个类初始化方法,就执行此方法。
那什么时候类会进行初始化呢?Java 虚拟机规范为类的初始化时机做了严格定义:在首次[color=red][b]主动[/b][/color]使用时初始化。
那哪些情形才符合首次主动使用的标准呢?Java虚拟机规范对此作出了说明,他们分别是:
1) 创建类的新实例;
2) 调用类的静态方法;
3) 操作类或接口的静态字段(final字段除外);
4) 调用Java的特定的反射方法;
5) 初始化一个类的子类,初始化一个类的[b]子类[/b]的时候,[b]父类[/b]也相当于被程序主动调用了([color=red]如果调用子类的静态变量是从父类继承过来并没有复写的,那么也就相当于只用到了父类的东东,和子类无关,所以这个时候子类不需要进行类初始化)[/color];
6) 指定一个类作为Java虚拟机启动时的初始化类,也就是直接运行一个main函数入口的类。
除了以上六种情形以外,所有其它的方式都是被动使用的,不会导致类的初始化。
[color=red][b]注意[/b][/color]:这里是[color=violet]类的初始化[/color],不是[color=violet]对象实例初始化[/color],只涉及静态变量的初始化,所以不会主动调用构造函数,除非<cinit>中调用了<init>。请参看本文示例【练习2】。
[i]<init>介绍:
Java编译器为它编译的每个类都至少生成一个实例初始化方法,即<init>()方法。一个<init>()方法内包括的代码内容可能有三种:调用另一个<init>() 方法;对实例变量初始化;构造方法体的代码。(注意,实例变量初始化和非静态初始化块的代码优先构造体先执行)。[/i]
[b]1.2 静态成员变量会被jvm自动转成静态块[/b]
当java源代码转换成一个class文件后,其转换成类似下面的代码:
进一步说,static变量的声明和初始化是分开的,即使是写成一行,编译时也会分成两步。如果先初始化,后声明也没关系,比如:
class **
{
static
{
_aa = 2;
}
static int _aa = 3;
}
编译后就是:
class **
{
static int _aa
static
{
_aa = 2;
_aa = 3;
}
}
[b]1.3 静态变量(静态块)初始化线程安全[/b]
前面说过,如果静态变量声明时就初始化,则编译后会自动添加一个static块。
JVM 保证同一个 ClassLoader 加载的类中的静态块只被执行一次,如果有其他线程也会触发静态初始化,就会block,等待正在进行的初始化。[color=red][b]因此在static块中初始化是线程安全的[/b][/color]。也正因为这一点,很多时候我们可以利用 static initialization block 执行一些初始化(write)操作,而无需对该 block 使用任何同步机制。 但这里只是说的初始化过程,初始化之后的使用,还有通过static method做的懒加载就不保证线程安全了,需要自己保证。
下面的例子不推荐:
class Employee{
private Employee(){}
private static Employee emp;
}
public static Employee getEmployee(){
return emp;
}
}
这里的if (emp==null)是多余的,而且造成误解,会让人考虑是否需要同步。这里的emp一定是null的。而且跟下面的写法是一样的:
class Employee{
private Employee(){}
public static Employee getEmployee(){
return emp;
}
}
[b]1.3.1 多线程写静态变量实例[/b]
包含静态块:
public enum Test
{
public static ConcurrentHashMap<String, Integer> messageTypeMap = null;
static {
System.out.println("static init begin");
messageTypeMap = new ConcurrentHashMap<String, Integer>();
messageTypeMap.put("a",1);
try
{
Thread.sleep(1000 * 10);
}
catch (Exception e)
{
}
System.out.println("static init end");
}
}
多线程:
public class TT implements Runnable
{
int index;
public TT(int index)
{
this.index = index;
}
@Override
public void run()
{
System.out.println("TT run:" + Thread.currentThread().getName());
Test.messageTypeMap.put("TT" + index, 1);
//Test.messageTypeMap.get("aa"); // get也是一样
System.out.println("TT run:" + Thread.currentThread().getName() + "mapSize=" + Test.messageTypeMap.size());
}
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
(new Thread(new TT(i))).start();
}
}
}
[color=red][b]结果的关键是static init begin和static init end之间不会有任何Test.messageTypeMap.put("TT" + index, 1);执行,一直等待static block执行完才开始执行。[/b][/color]
还可以参考:
[url]http://stackoverflow.com/questions/878577/are-java-static-initializers-thread-safe?rq=1[/url]
关于final变量的初始化安全性可以参考:
[url]http://zoroeye.iteye.com/blog/2058889[/url] 1.3节
[url]http://zoroeye.iteye.com/blog/2026866[/url] 2.4.1节
[b]2. 类实例初始化(对象的生成)[/b]
对于对象的生成其初始化过程与类的初始化过程类似,但会增加构造函数阶段,源代码如下:
编译器转换成class文件后,会转换成类似下面的代码:
可以看到,对于类中对成员变量的初始化和代码块中的代码全部都挪到了构造函数中,并且是按照java源文件的初始化顺序依次对成员变量进行初始化的,而原构造函数中的代码则移到了构造函数的最后执行。
[b]3.总结[/b]
[b]3.1 无继承关系的类初始化[/b]
对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是[color=red](静态变量、静态初始化块)[/color] >[color=green](变量、初始化块)[/color] > [color=blue]构造器[/color]。
[b]3.2 有继承关系类初始化[/b]
(1)加载父类(以下序号相同,表明初始化是按代码从上到下的顺序来的)
1.为父类的静态属性分配空间并赋于初值
1.执行父类静态初始化块;
(2)加载子类
2.为子类的静态属性分配空间并赋于初值
2.执行子类的静态的内容;
(3)加载父类构造器
3.初始化父类的非静态属性并赋于初值
3.执行父类的非静态代码块;
4.执行父类的构造方法;
(4)加载子类构造器
5.初始化子类的非静态属性并赋于初值
5.执行子类的非静态代码块;
6.执行子类的构造方法.
总之一句话,静态代码块内容先执行(父先后子),接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。
静态变量和静态初始化块谁在前面谁先执行。
练习1:
[b]分析:[/b]
(1) Jvm先要找到包含main的类ClassInit,然后初始化静态成员或静态块,输出第一行(测试一下,如果把static块变成非static块,则不会执行)。
(2) 然后执行main方法,先创建SuperInitField实例,还是按照先链接阶段初始化静态成员或静态块,再初始化实例变量的顺序再构造函数的顺序。
(3) 创建SubInitField实例时先加载父类,父类静态变量已经初始化所以不再加载,接着加载子类,先加载静态变量。然后运行父类的构造函数,再执行子类构造函数。所有的类都加载链接完再按特定顺序初始化。
(4) 如果main方法改为:
public static void main(String[] args) {
//SuperInitField p = new SuperInitField();
SuperInitField c = new SubInitField();
SuperInitField p = new SuperInitField();
}
则输出:
ClassInit static field.
static parent
static child
parent
child
parent
练习2:
分析:因为jvm要执行main方法,先要加载InitTest类,首先初始化静态变量或静态块,然后执行main方法。因为没有显式或隐式创建InitTest实例,所以构造函数不需要执行。
练习3:
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧在任何方法([b]包括构造器[/b])被调用[color=red][b]之前[/b][/color]得到初始化。可以参考:[url]http://zhangjunhd.blog.51cto.com/113473/20927/[/url]
[b][color=red]【补充:】[/color][/b]
[b]1.对于final修饰的static字段处理会跟单独的static修饰不一样,final修饰的会被jvm当做常量在编译阶段就初始化了,类加载时直接能看到分配的值,但这仅限于String和原始类型,Object类型仍然不会编译时就初始化。可以通过javap -verbose classFile 查看到带final和不带final的区别。[/b]
[b]以上参考[/b]:
1.[url]http://developer.51cto.com/art/201205/337932.htm[/url]
2.[url]http://wenku.baidu.com/link?url=NdUKiyOQJimng03U13ZafTDnGGjPETbWaK7CUw-Lzsi7SepCbrDxJVIkwFPSwFqB1I2zie3DXyG15ykfR00GMQIJ70lcKYPJez5LiJOalZS[/url]
3.[url]http://developer.51cto.com/art/201103/249613.htm[/url]
4.[url]http://www.cnblogs.com/lmtoo/archive/2012/04/08/2437918.html[/url]
[b]1. 静态变量(块)初始化[/b]
[b]1.1 大概了解Java虚拟机初始化的原理[/b]
JVM通过加装、连接和初始化一个Java类,使该类可以被正在运行的Java程序所使用。装载和连接必须在初始化之前就要完成。Java类型的生命周期:
[img]http://dl2.iteye.com/upload/attachment/0093/7849/838a8b68-41d8-307a-93e4-34b42484a987.bmp[/img]
1):[b]类加载load[/b]:从字节码二进制文件*.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的[color=red]方法区[/color]内,之后在[color=red]堆区[/color]建立一个java.lang.Class对象,用来封装方法区的数据结构。这个时候就体现出了万事万物皆对象了,干什么事情都得有个对象。就是到了最底层究竟是鸡生蛋,还是蛋生鸡呢?[color=red][b]类加载的最终产物就是堆中的一个java.lang.Class对象。
[/b][/color]
2):[b]连接[/b]:连接又分为以下小步骤
[b]验证[/b]:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。
[b]准备[/b]:将类的[color=red]静态变量[/color]进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情,只是初始化默认值,而不是赋值)
[b]解析[/b]:把类的符号引用转为直接引用(保留)
3):[b]类的初始化(不是对象实例的初始化)[/b]: 将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值([color=red]这是跟准备阶段的区别[/color])。
类初始化阶段,主要是为类变量[color=red](注意是静态变量,如果静态变量初始化时需要间接初始化其他类实例则转而去初始化依赖的变量,也就是<cinit>阶段遇到<init>就先执行完<init>再继续执行<cinit>)[/color]赋予正确的初始值。这里的“正确”初始值指的是程序员希望这个类变量所具备的起始值。一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。初始化一个类包含两个步骤:
1) 如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
2) 如果类存在一个类初始化方法,就执行此方法。
那什么时候类会进行初始化呢?Java 虚拟机规范为类的初始化时机做了严格定义:在首次[color=red][b]主动[/b][/color]使用时初始化。
那哪些情形才符合首次主动使用的标准呢?Java虚拟机规范对此作出了说明,他们分别是:
1) 创建类的新实例;
2) 调用类的静态方法;
3) 操作类或接口的静态字段(final字段除外);
4) 调用Java的特定的反射方法;
5) 初始化一个类的子类,初始化一个类的[b]子类[/b]的时候,[b]父类[/b]也相当于被程序主动调用了([color=red]如果调用子类的静态变量是从父类继承过来并没有复写的,那么也就相当于只用到了父类的东东,和子类无关,所以这个时候子类不需要进行类初始化)[/color];
6) 指定一个类作为Java虚拟机启动时的初始化类,也就是直接运行一个main函数入口的类。
除了以上六种情形以外,所有其它的方式都是被动使用的,不会导致类的初始化。
[color=red][b]注意[/b][/color]:这里是[color=violet]类的初始化[/color],不是[color=violet]对象实例初始化[/color],只涉及静态变量的初始化,所以不会主动调用构造函数,除非<cinit>中调用了<init>。请参看本文示例【练习2】。
[i]<init>介绍:
Java编译器为它编译的每个类都至少生成一个实例初始化方法,即<init>()方法。一个<init>()方法内包括的代码内容可能有三种:调用另一个<init>() 方法;对实例变量初始化;构造方法体的代码。(注意,实例变量初始化和非静态初始化块的代码优先构造体先执行)。[/i]
[b]1.2 静态成员变量会被jvm自动转成静态块[/b]
public class Person{
public static String name="张三";
public static int age;
static{
age=20;
System.out.println("初始化age");
}
public static String address;
static{
address="北京市";
age=34;
}
public static void main(String[] args) {
System.out.println(name);
System.out.println(age);
System.out.println(address);
}
}
当java源代码转换成一个class文件后,其转换成类似下面的代码:
public class Person{
public static String name;
public static int age;
public static String address;
static{
name="张三";
age=20;
System.out.println("初始化age");
address="北京市";
age=34;
}
public static void main(String[] args) {
System.out.println(name);
System.out.println(age);
System.out.println(address);
}
}
进一步说,static变量的声明和初始化是分开的,即使是写成一行,编译时也会分成两步。如果先初始化,后声明也没关系,比如:
class **
{
static
{
_aa = 2;
}
static int _aa = 3;
}
编译后就是:
class **
{
static int _aa
static
{
_aa = 2;
_aa = 3;
}
}
[b]1.3 静态变量(静态块)初始化线程安全[/b]
前面说过,如果静态变量声明时就初始化,则编译后会自动添加一个static块。
JVM 保证同一个 ClassLoader 加载的类中的静态块只被执行一次,如果有其他线程也会触发静态初始化,就会block,等待正在进行的初始化。[color=red][b]因此在static块中初始化是线程安全的[/b][/color]。也正因为这一点,很多时候我们可以利用 static initialization block 执行一些初始化(write)操作,而无需对该 block 使用任何同步机制。 但这里只是说的初始化过程,初始化之后的使用,还有通过static method做的懒加载就不保证线程安全了,需要自己保证。
下面的例子不推荐:
class Employee{
private Employee(){}
private static Employee emp;
static {
if (emp==null){
emp=new Employee();
}
}
public static Employee getEmployee(){
return emp;
}
}
这里的if (emp==null)是多余的,而且造成误解,会让人考虑是否需要同步。这里的emp一定是null的。而且跟下面的写法是一样的:
class Employee{
private Employee(){}
private static Employee emp = new Employee();
public static Employee getEmployee(){
return emp;
}
}
[b]1.3.1 多线程写静态变量实例[/b]
包含静态块:
public enum Test
{
public static ConcurrentHashMap<String, Integer> messageTypeMap = null;
static {
System.out.println("static init begin");
messageTypeMap = new ConcurrentHashMap<String, Integer>();
messageTypeMap.put("a",1);
try
{
Thread.sleep(1000 * 10);
}
catch (Exception e)
{
}
System.out.println("static init end");
}
}
多线程:
public class TT implements Runnable
{
int index;
public TT(int index)
{
this.index = index;
}
@Override
public void run()
{
System.out.println("TT run:" + Thread.currentThread().getName());
Test.messageTypeMap.put("TT" + index, 1);
//Test.messageTypeMap.get("aa"); // get也是一样
System.out.println("TT run:" + Thread.currentThread().getName() + "mapSize=" + Test.messageTypeMap.size());
}
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
(new Thread(new TT(i))).start();
}
}
}
[color=red][b]结果的关键是static init begin和static init end之间不会有任何Test.messageTypeMap.put("TT" + index, 1);执行,一直等待static block执行完才开始执行。[/b][/color]
还可以参考:
[url]http://stackoverflow.com/questions/878577/are-java-static-initializers-thread-safe?rq=1[/url]
关于final变量的初始化安全性可以参考:
[url]http://zoroeye.iteye.com/blog/2058889[/url] 1.3节
[url]http://zoroeye.iteye.com/blog/2026866[/url] 2.4.1节
[b]2. 类实例初始化(对象的生成)[/b]
对于对象的生成其初始化过程与类的初始化过程类似,但会增加构造函数阶段,源代码如下:
public class Person{
{
name="李四";
age=56;
System.out.println("初始化age");
address="上海";
}
public String name="张三";
public int age=29;
public String address="北京市";
public Person(){
name="赵六";
age=23;
address="上海市";
}
}
编译器转换成class文件后,会转换成类似下面的代码:
public class Person{
public String name;
public int age;
public String address;
public Person(){
name="李四";
age=56;
System.out.println("初始化age");
address="上海";
name="张三";
age=29;
address="北京市";
name="赵六";
age=23;
address="上海市";
}
}
可以看到,对于类中对成员变量的初始化和代码块中的代码全部都挪到了构造函数中,并且是按照java源文件的初始化顺序依次对成员变量进行初始化的,而原构造函数中的代码则移到了构造函数的最后执行。
[b]3.总结[/b]
[b]3.1 无继承关系的类初始化[/b]
对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是[color=red](静态变量、静态初始化块)[/color] >[color=green](变量、初始化块)[/color] > [color=blue]构造器[/color]。
[b]3.2 有继承关系类初始化[/b]
(1)加载父类(以下序号相同,表明初始化是按代码从上到下的顺序来的)
1.为父类的静态属性分配空间并赋于初值
1.执行父类静态初始化块;
(2)加载子类
2.为子类的静态属性分配空间并赋于初值
2.执行子类的静态的内容;
(3)加载父类构造器
3.初始化父类的非静态属性并赋于初值
3.执行父类的非静态代码块;
4.执行父类的构造方法;
(4)加载子类构造器
5.初始化子类的非静态属性并赋于初值
5.执行子类的非静态代码块;
6.执行子类的构造方法.
总之一句话,静态代码块内容先执行(父先后子),接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。
静态变量和静态初始化块谁在前面谁先执行。
练习1:
public class ClassInit {
public static String dd = "ClassInit static field.";
static
{
System.out.println(dd);
}
public static void main(String[] args) {
SuperInitField p = new SuperInitField();
SuperInitField c = new SubInitField();
//SuperInitField p = new SuperInitField();
}
}
class SuperInitField {
public SuperInitField() {
System.out.println("parent");
}
static {
System.out.println("static parent");
}
}
class SubInitField extends SuperInitField {
public SubInitField() {
System.out.println("child");
}
static {
System.out.println("static child");
}
}
输出:
ClassInit static field.
static parent
parent
static child
parent
child
[b]分析:[/b]
(1) Jvm先要找到包含main的类ClassInit,然后初始化静态成员或静态块,输出第一行(测试一下,如果把static块变成非static块,则不会执行)。
(2) 然后执行main方法,先创建SuperInitField实例,还是按照先链接阶段初始化静态成员或静态块,再初始化实例变量的顺序再构造函数的顺序。
(3) 创建SubInitField实例时先加载父类,父类静态变量已经初始化所以不再加载,接着加载子类,先加载静态变量。然后运行父类的构造函数,再执行子类构造函数。所有的类都加载链接完再按特定顺序初始化。
(4) 如果main方法改为:
public static void main(String[] args) {
//SuperInitField p = new SuperInitField();
SuperInitField c = new SubInitField();
SuperInitField p = new SuperInitField();
}
则输出:
ClassInit static field.
static parent
static child
parent
child
parent
练习2:
public class InitTest
{
public InitTest(){
System.out.println("parent");
}
static{
System.out.println("static parent");
}
public static void main(String[] args) {
System.out.println("main");
}
}
输出:
static parent
main
分析:因为jvm要执行main方法,先要加载InitTest类,首先初始化静态变量或静态块,然后执行main方法。因为没有显式或隐式创建InitTest实例,所以构造函数不需要执行。
练习3:
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧在任何方法([b]包括构造器[/b])被调用[color=red][b]之前[/b][/color]得到初始化。可以参考:[url]http://zhangjunhd.blog.51cto.com/113473/20927/[/url]
[b][color=red]【补充:】[/color][/b]
[b]1.对于final修饰的static字段处理会跟单独的static修饰不一样,final修饰的会被jvm当做常量在编译阶段就初始化了,类加载时直接能看到分配的值,但这仅限于String和原始类型,Object类型仍然不会编译时就初始化。可以通过javap -verbose classFile 查看到带final和不带final的区别。[/b]
[b]以上参考[/b]:
1.[url]http://developer.51cto.com/art/201205/337932.htm[/url]
2.[url]http://wenku.baidu.com/link?url=NdUKiyOQJimng03U13ZafTDnGGjPETbWaK7CUw-Lzsi7SepCbrDxJVIkwFPSwFqB1I2zie3DXyG15ykfR00GMQIJ70lcKYPJez5LiJOalZS[/url]
3.[url]http://developer.51cto.com/art/201103/249613.htm[/url]
4.[url]http://www.cnblogs.com/lmtoo/archive/2012/04/08/2437918.html[/url]