前言
从本篇开始我们进入到初始化阶段,这是类加载器的最后一个阶段,如果前面步骤都没有问题, 那么类就可以顺利的装在到系统到中。
具体描述
初始化阶段,简言之,为类的静态变量赋予正确的初始值
,到了初始化阶段,才真正开始执行类中定义的Java程序代码
初始化阶段的重要工作是执行类的初始化方法:<clinit>()方法
- 该方法仅能由Java编译器生成并由JVM调用,
程序开发者无法自定义一个同名的方法
- 更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成
- 它是由
类静态成员的赋值语句以及static语句块合并产生的
接下来我们使用示例代码来体会一下这个<clinit>方法在字节码中是怎么体现的?
public class InitializationTest {
public static int id = 1;
public static int number;
static {
number = 2;
System.out.println("father static{}");
}
}
这时我们编译代码,使用插件来看看具体的字节码是怎么回事?
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类cclinit>之前被调用。也就是说,父类的static块优先级高于子类
。口诀:由父及子,静态先行
接下来我们使用示例代码来体会这个父子关系的情况
public class InitializationTest {
public static int id = 1;
public static int number;
static {
number = 2;
System.out.println("father static{}");
}
}
public class subInitialization extends InitializationTest {
static{
number = 4;
System.out.println( "son static{}");
}
public static void main( string[] args) {
system.out.println( number);
}
}
//输出结果如下:
father static{}
son static{}
4
由于子类使用static静态代码块给number赋值,所以需要number属性必须已经提前加载
那么什么时候Java编译器并不会为所有的类都产生<clinit>()初始化方法
。哪些类在编译为字节码后,字节码文件中将不会包含<clinit>()方法?
- 类中并
没有声明任何的类变量,也没有静态代码块时
- 类中
有声明类变量,但没有使用类变量的初始化语句以及静态代码块来初始化时
- 类中
有static final修饰的基本数据类型的字段
,这些类字段初始化语句采用编译时常量表达式
接下来我们使用示例代码来体会上面的三种情况
public class InitializationTest {
//场景1:对于非静态的字段,不管是否进行显示赋值都不会展示<clinit>()方法
public int num = 1;
//场景2:静态字段,没有显示赋值 不会生成<clinit>()方法
public static int num1;
}
这时我们编译一下上面的示例代码,就可以看到并没有生成对应的方法
接下来说说场景三,针对于静态字段进行显示赋值看看是什么情况?
public class InitializationTest {
//场景1:对于非静态的字段,不管是否进行显示赋值都不会展示<clinit>()方法
public int num = 1;
//场景2:静态字段,没有显示赋值 不会生成<clinit>()方法
public static int num1;
//场景3:静态字段进行显示赋值
public static int num2 = 1;
}
这时我们再来看看使用了static与final修饰
的变量又是什么情况呢?
public class InitializationTest2{
public static int a = 1;
public static final int INT_CONSTANT = 10;
}
这时我们编译代码,看看这两行的代码针对于初始化<clinit>()方法
是怎么回事?
这时我们看更复杂一点情况又是怎么样呢?又与之前的有和不同
public class InitializationTest2{
public static int a = 1;
public static final int INT_CONSTANT = 10;
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);
}
这时我们编译代码,看看这两行的代码针对于初始化<clinit>()方法
是怎么回事?
接下来我们再看看如果是new对象的方式又有和不同?
public class InitializationTest2{
public static int a = 1;
public static final int INT_CONSTANT = 10;
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);
public static final String s0 = "helloworld0";
public static final String s1 = new String("helloworld1");
}
这时我们编译代码,看看这两行的代码针对于初始化<clinit>()方法
是怎么回事?
通过上面的示例代码,我们可以做出以下的几种情况小结,对于基本数据类型的字段来说,如果使用static final修饰,并显式赋值(直接赋值常量,而非调用方法赋值的话)通常是链接阶段的准备环节进行
。
对于String类型来说,如果使用字面量的方式赋值,使用satie final修饰的话,则显式赋值通常是在准备环节进行
,在排除上面这两种情况之外的话的属于在初始化阶段<clinit>()中赋值的情况
最终的结论:
使用static、final修饰,且显示赋值中不涉及到方法或者构造器调用的节本数据类型,或者String类型的显示赋值
,则属于是在链接阶段的准备环节进行中。
<clinit>()
的线程安全性
对于<clinit>()
方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步
,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕
正是因为函数<clinit>()
带锁线程安全的,因此如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁
。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行<clinit>()
方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息
这时我们可以通过示例代码来体会,当一个类加载另外一个类的时候,出现的死锁问题
class staticA {
static {
try {
Thread.sleep(1000) ;
}catch ( InterruptedException e) {
}
try {
Class.forName( "com.atguigu.java1.staticB");
}catch (classNotFoundException e) {
e.printStackTrace();
}
system.out.print1n( "staticA init OK");
}
}
class staticB {
static {
try {
Thread.sleep(1000) ;
}catch ( InterruptedException e) {
}
try {
Class.forName( "com.atguigu.java1.staticA");
}catch (classNotFoundException e) {
e.printStackTrace();
}
system.out.print1n( "staticB init OK");
}
}
这时我们就会造成死锁问题
类的初始化情况:主动使用
Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。
Java虚拟机规定:一个类或接口在初次使用前,必须要进行初始化
这里指的“使用”,是指主动使用,主动使用只有下列几种情况:
如果出现如上的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成
public class ActiveUse1{
public static void main( String[] args) {
order order = new order();
}
}
//当创建一个类的实例时(比如使用new关键字,或者通过反射、克隆、反序列化)
class order{
static{
System.out.println("order类的初始化过程");
}
}
//运行结果如下:
Order类的初始化过程
public class ActiveUse1{
public static void main( String[] args) {
order order = new order();
}
//序列化过程
@Test
public void test1(){
ObjectOutputStream oos = null;
try{
oos = new ObjectOutputStream(new FileoutputStream("order.dat"));
oos.writeobject(new Order());
}catch (IOException e){
e.printstackTrace();
}finally{
try{
if(oos != null){
obs.close();
}
}catch (IOException e){
e.printstackTrace();
}
}
}
//反序列过程
@Test
public void test2(){
ObjectInputStream ois = null;
try{
ois = new ObjectInputStream(new FileInputstream("order.dat"));
order order = (order) ois.readobject();
}catch ( IOException e){
e.printstackTrace();
}catch (classNotFoundException e){
e.printstackTrace();
}finally{
try {
if (ois != null)
ois.close( );
}catch ( IOException e){
e.printstackTrace();
}
}
}
}
//当创建一个类的实例时(比如使用new关键字,或者通过反射、克隆、反序列化)
class order implements Serializable {
static{
System.out.println("order类的初始化过程");
}
}
//test1运行结果如下:
Order类的初始化过程
//test2运行结果如下:
Order类的初始化过程
public class ActiveUse1{
public static void main( String[] args) {
order order = new order();
}
@Test
public void test3(){
Order.method();
}
}
//当调用类的静态方法时(比如使用了字节码invokestatic指令)
class order implements Serializable {
static{
System.out.println("order类的初始化过程");
}
public static void method(){
System.out.println("order method()......");
}
}
//test3运行结果如下:
Order类的初始化过程
order method()......
public class ActiveUse2 {
@Test
public void test1(){
System.out.println(User.num);
}
}
//当使用类、接口的静态字段时(final修饰特殊考虑)(比如使用getstatic或者putstatic指令)(对应访问变量、赋值变量操作)
class User{
static{
System.out.println("User类的初始化过程");
}
public static int num = 1;
}
//test1运行结果如下:
User类的初始化过程
1
此时我们可以结合之前我们的准备阶段和初始化阶段来再看看下一个示例代码是怎么样的?
public class ActiveUse2 {
@Test
public void test1(){
System.out.println(User.num1);
}
}
//当使用类、接口的静态字段时(final修饰特殊考虑)(比如使用getstatic或者putstatic指令)(对应访问变量、赋值变量操作)
class User{
static{
System.out.println("User类的初始化过程");
}
public static int num = 1;
public static final int num1 = 1;
}
//test1运行结果如下:
1
这时就没有触发初始化阶段,因为num1 已经在准备阶段进行了,所以num 还是 0
public class ActiveUse2 {
@Test
public void test1(){
System.out.println(User.num2);
}
}
//当使用类、接口的静态字段时(final修饰特殊考虑)(比如使用getstatic或者putstatic指令)(对应访问变量、赋值变量操作)
class User{
static{
System.out.println("User类的初始化过程");
}
public static int num = 1;
public static final int num1 = 1;
public static final int num2 = new Random().nextInt(10);
}
//test1运行结果如下:
User类的初始化过程
8
这时我们就可以看到,对应的初始化就出来了,接下来我们看看接口是怎么用的?
public class ActiveUse2 {
@Test
public void test2(){
System.out.println(compareA.NUM1);
}
}
interface CompareA{
public static final Thread t = new Thread() {
{
System.out.println("CompareA的初始化");
}
};
public static final int NUM1 = 1;
}
//test2运行结果如下:
1
这说明我们也并没有执行初始化,这时候我们稍作修改重新体会体会
public class ActiveUse2 {
@Test
public void test2(){
System.out.println(compareA.NUM2);
}
}
interface CompareA {
public static final Thread t = new Thread() {
{
System.out.println("CompareA的初始化");
}
};
public static final int NUM1 = 1;
public static final int NUM2 = new Random().nextInt(10);
}
//test2运行结果如下:
CompareA的初始化
7
这时我们就执行到初始化了,并且发现也并不是final修饰就无法执行初始化了
public class ActiveUse3{
@Test
public void test1(){
try{
Class clazz = Class.forName("com.atguigu.java1.order");
}catch(classNotFoundException e){
e.printstackTrace();
}
}
}
//当使用java.Lang.reflect包中的方法反射类的方法时。
//比如: Class.forName("com.atguigu.java.Test")
class order {
static{
System.out.println("order类的初始化过程");
}
}
//test1运行结果如下:
order类的初始化过程
public class ActiveUse3{
@Test
public void test2(){
System.out.println(Son.num);
}
}
//当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
class Father{
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father{
static {
System.out.println("son类的初始化过程");
}
public static int num = 1;
}
//test2运行结果如下:
Father类的初始化过程
son类的初始化过程
1
那么在针对于初始化类的时候,我们有说过要求它的父类都初始化,但不试用于接口
public class ActiveUse3{
@Test
public void test2(){
System.out.println(Son.num);
}
}
class Father{
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father implements CompareB{
static {
System.out.println("son类的初始化过程");
}
public static int num = 1;
}
interface CompareB{
public static final Thread t = new Thread() {
{
System.out.println("compareB的初始化");
}
};
}
//test2运行结果如下:
Father类的初始化过程
son类的初始化过程
1
我们可以发现并没有compareB的初始化
说明,这就是我们说的这个不会初始化接口
public class ActiveUse3{
@Test
public void test2(){
System.out.println(Son.num);
}
}
class Father{
static {
System.out.println("Father类的初始化过程");
}
}
class Son extends Father implements CompareB{
static {
System.out.println("son类的初始化过程");
}
public static int num = 1;
}
//如果一个接口定义了default方法
//那么直接实现或者同接实现该接口的类的初始化,该接口要在其之前被初始化。
interface CompareB{
public static final Thread t = new Thread() {
{
System.out.println("compareB的初始化");
}
};
public default void method1(){
System.out.println("你好!");
}
}
//test2运行结果如下:
Father类的初始化过程
compareB的初始化
son类的初始化过程
1
//当虚拟机启动时,用户需要指定一个要执行的主类〈包含main()方法的那个类),虚拟机会先初始化这个主类
public class ActiveUse3{
static {
System.out.println("ActiveUse3的初始化过程");
}
public static void main(String[] args) {
System.out.println("hello");
}
}
//运行结果如下:
ActiveUse3的初始化过程
hello
类的初始化情况:被动使用
除了上面的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化
被动使用有下列的几种情况说明:
接下来使用代码示例来体会以上几种情况以及与没有初始化的类,但不意味着没有加载是什么情况
public class PassiveUse1{
@Test
public void test1(){
System.out.println(child.num) ;
}
}
//当访问一个静态字段时,只有真正声明这个字段的类才会被初始化
//注意:当通过子好用父类的静态变量,不会导致子类初始化
class Parent{
static {
System.out.println("Parent的初始化过程");
}
public static int num = 1;
}
class Child extends Parent
static {
System.out.println("child类的初始化过程");
}
}
//执行test1运行结果如下:
Parent的初始化过程
1
public class PassiveUse1{
@Test
public void test1(){
Parent[] parents = new Parent[10];
}
}
//通过数组定义类引用,不会触发此类的初始化
class Parent{
static {
System.out.println("Parent的初始化过程");
}
public static int num = 1;
}
//运行结果如下:
public class PassiveUse2{
@Test
public void test1(){
System.out.println(Parent.num);
}
}
//引用常量不会触发此类或接口的初始化(常量在链接阶段被显式赋值)
class Parent{
static {
System.out.println("Parent的初始化过程");
}
public static final int num = 1;
}
//运行结果如下:
public class PassiveUse2{
@Test
public void test1(){
try{
Class clazz = classLoader.getSystemClassLoader().loadClass("com.atguigu.java1.Person");
}catch (classNotFoundException e){
e.printstackTrace( );
}
}
}
//调用ClassLoader类的loadClass()方法加载一个类
//并不是对类的主动使用,不会导致类的初始化
class Parent{
static {
System.out.println("Parent的初始化过程");
}
public static final int num = 1;
}
//运行结果如下: