类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题说明类可以被顺利装载到系统中,此时jvm才会执行类中的字节码(即到了初始化阶段才真正开始执行类中定义的java程序代码)。
javac编译器并不是所有的类编译后都会产生方法,下面几种类在编译后字节码文件中就不会包含方法。
对于方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境下的安全性。虚拟机会保证一个类的方法在多线程环境下被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行完方法完毕。正是因为方法是带锁且线程安全的,如果一个类中的方法中存在很多耗时较长的操作,就可能造成多个线程阻塞,引发死锁。方法的锁一个隐式锁不同于直接用synchronized关键字标记的方法,这种死锁是很难被发现的,因为看起来它们并没有可用的锁信息。
如果之前线程成功的加载了该类则等待在队列中的线程就没有机会再执行方法了,当使用这个类是虚拟机会直接返回已经初始化好的类信息。
下面通过一段代码,看下方法和显式synchronized标记的方法在字节码层面的不同。
package jvm.memory.clinit; public class ClinitDemo { static String aa="123"; static int bb=12; static { aa="234"; } static synchronized void locakDemo(){ System.out.println("这个方法加了同步锁"); } }
显式synchronized标记的locakDemo
ClinitDemo的方法
可以看到的访问标志上没有锁标记,属于隐性锁实现,又jvm控制,所以通常的死锁排查工具不易发现导致的死锁问题。
代码如下,staticA的中初始化staticB,staticB的中初始化staticA,启动线程1初始化staticA,线程2初始化staticB
package jvm.memory.clinit; public class ClinitLockDemo { public static void main(String[] args) { StaticDeadLockMain staticDeadLockMain1=new StaticDeadLockMain("A"); staticDeadLockMain1.setName("线程1"); StaticDeadLockMain staticDeadLockMain2 = new StaticDeadLockMain("B"); staticDeadLockMain2.setName("线程2"); staticDeadLockMain1.start(); //注释 staticDeadLockMain2.start(); } } class staticA{ static { try { System.out.println("线程"+Thread.currentThread().getName()+"正在初始化staticA"); Thread.sleep(1000); System.out.println("线程"+Thread.currentThread().getName()+"》staticA的<clinit>方法正在执行,需要初始化staticB"); // 在staticA的<clinit>方法里显示加载并初始化staticB Class.forName("jvm.memory.clinit.staticB",true,ClinitLockDemo.class.getClassLoader()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println("staticA加载完毕"); } } class staticB{ static { try { System.out.println("线程"+Thread.currentThread().getName()+"正在初始化staticB"); Thread.sleep(1000); System.out.println("线程"+Thread.currentThread().getName()+"》staticB的<clinit>方法正在执行,需要初始化staticA"); // 在staticB的<clinit>方法里显示加载并初始化staticA Class.forName("jvm.memory.clinit.staticA",true,ClinitLockDemo.class.getClassLoader()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println("staticB加载完毕"); } } class StaticDeadLockMain extends Thread{ private String flag; public StaticDeadLockMain(String flag){ this.flag=flag; } @Override public void run() { try { Class.forName("jvm.memory.clinit.static"+flag,true,ClinitLockDemo.class.getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
打印结果如下,此时程序并未结束,处于死锁中。
线程线程2正在初始化staticB线程线程1正在初始化staticA 线程线程2》staticB的<clinit>方法正在执行,需要初始化staticA 线程线程1》staticA的<clinit>方法正在执行,需要初始化staticB
当注释掉第12行代码staticDeadLockMain2.start();后再此执行打印结果如下,此时程序结束
线程线程1正在初始化staticA 线程线程1》staticA的<clinit>方法正在执行,需要初始化staticB 线程线程1正在初始化staticB 线程线程1》staticB的<clinit>方法正在执行,需要初始化staticA staticB加载完毕 staticA加载完毕
Class只有在必须首次使用时才会初始化,Java虚拟机规定一个类或接口在初次使用之前必须要进行初始化,这里的“使用”指的是首次使用,而初始化操作之前的加载、验证、准备阶段都已经完成了。主动使用的情况如下:
针对5.补充说明:
当java虚拟机初始化一个类时,要求它的所有父类都已经初始化,但是这条规则并不适用于接口。
针对6.补充说明:
JVM启动的时候加载一个启动类(定义main方法的类),这个类调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需要的类的加载、链接和初始化。
除了以上的情况属于主动使用,其他情况均数据被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会初始化。