线程一1.0 线程 类的加载过程详解
看一下运行结果是什么,如果将2换到1 的位置。输出是什么?
一: 类的加载阶段
类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的Java.lang.class对象,作为访问方法区数据结构的入口:
类加载的最终产物就是堆内存中的class对象,对于同一个class loader来说,不管某个类被加载啦多少次,对应堆内存中的class对象始终是同一个,虚拟机规范中指出了类的加载是通过一个全限类名(包名+类名)来获取二进制数据流,但是并没有限定必须通过某种方式去获得,比如常见的class二进制文件的形式,还有以下几种形式:
(1)运行时动态生成,比如通过开源的asm 包也可以生成一些class,或者通过动态代理 java.lang.proxy也可以生成代理类的二进制字节流,
(2)通过网络获取,比如很早之前的applet小程序,以及rmi动态发布等
(3)通过读取zip文件获得类的二进制字节流。比如 jar。war
(4)将类的二进制数据存储在数据库的blob字段类型中、
(5)运行时生成class文件,并且动态加载,比如使用thrift,avro 等都是可以在运行 时将某个schema文件生成的对应若干个class文件,然后进行加载
我们现在说的是类加载过程中的第一个阶段,不代表整个类已经加载完成了。在某个类完成加载阶段之后,虚拟机会将这些二进制字节流按照虚拟机所需的格式存储在方法区中,然后形成特定的数据结构,随之又在堆内存中实例化一个java.lang.class类对象,在类加载的整个生命周期内,加载过程还没有结束,链接阶段是可以交叉工作的,比如链接阶段验证字节流信息的合法性,但是总体来说记载阶段肯定是出现在链接阶段之前的,
二: 类的连接阶段
类的链接阶段分为三个小过程:验证。准备和解析,
(1)验证: 验证在链接阶段目的是确保class文件的字节流所包含 的内容符合当前jvm的规范要求,兵器不会出现危害jvm自身安全的代码,当字节流信息不符合要求时,则抛出verifyErrory这样的异常或者是其子异常,验证了那些信息:
()
(2)准备:
当一个class字节流通过了所有验证之后,就开始为该对象的类变量,也就是静态变量,分配内存并且设置初始值了,类变量的内存会被分配到方法区中,不同实例变量会分配到堆内存中,
所谓设置初始值就是为相应的类变量给定一个相关类型在没有被设置值时的默认值,不同数据类型初始值如下:
为类变量设置初始值代码如下:
public class LinkedPrepare{
private static int a =10;//1
private final static int b =10;//2
}
其中static int a=10;在准备阶段不是10;而是初始值0;但是final static int b 还会是1-;因为final修饰的是类变量,不会导致类的初始化,是一种被动引用,因此就不会存在连接阶段了,更加严谨的解释是final static int b=10;在类的编译阶段javac会将其value生产一个constantvalue属性,直接赋予10;
(3)解析:
在连接阶段经历了验证,准备后就可以进入解析过程了。在解析过程中也会交叉验证一些验证的过程,比如符号引用的验证,所谓的解析就是从常量池中寻找类,接口,字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程,
public class classResolve{
static Simple simple =new Simple();
public staic void main (Stirng[] args){
system,out.println(simple);
}
}
classresolve 用到了Simple类,我们在写程序时候可以至二级使用simple这个引用去访问Simple类的可见的方法和属性,但是在class字节码中不是这么简单,他会被编译成相应的助记符。这些助记符称为符号引用,在类的解析过程中,助记符还要得到进一步的解析,才能正确找到所对应的堆内存中的simple数据结构,下面是一段classResolve字节码的信息片段:
在常量池中通过getsatic这个指令获取printStream,同样getstatic也适用于获取simple,然后通过invokevirtual指令将simple传递给printStream 的println方法,在字节码的执行过程中,getstatic被执行之前,就需要进行解析,
虚拟机规范规定了在anewarray,checkcast ,getfield,getstatic ,instanceof.invok-einterface,invokespecial,invokestatic,inbokevirtual,multianewarray,new ,pufield ,putstatic这13个操作符号引用的字节码指令之前,必须对所有的符号提前进行解析,
解析过程主要针对类接口,字段,类方法,和接口方法这四类进行的,分别对应常量池的CONSTANT_Class_info.CONSTANT_Fieldref_info, Constant_Methodref_info和Constant_InterfaceMethodred_info这四种类型的常量
三:类的初始化阶段;
这是类加载的最后一个阶段,最主要一件事就是执行<clinit>()方法,的过程(<clinit>是class initialize 的简写)在<clinit>()方法中所有的类变量都会被赋予正确的值,也就是在程序编写时指定的值,
clinit>() 方法是在编译阶段生成的,也就是说他已经包含在class文件中了,clinit>()中包含了多有类变量的赋值动作和静态语句块的执行代码,编译器收集的顺序是由执行语句在源文件中出现的顺序所决定的,(clinit>() 能保证顺序性)静态语句块只能对后面的静态变量进行赋值,但是不能对其进行访问,如图: 静态代码中对x的访问无法通过编译:
另外 clinit>()方法与类的构造函数有所不同,他不需要显示的调用父类的构造器,虚拟机会保证父类的clinit>() 方法最先执行,因此父类的静态变量总能得到优先赋值:
public class ClassInit {
static class Parent{
static int value=10;
static {
value=20;
}
}
static class Child extends Parent{
static int i=value;
public static void main(String[] args) {
System.out.println(Child.i);
}
}
}
上面输出20 ,不是10 因为父类的public class ClassInit {
static class Parent{
static int value=10;
static {
value=20;
}
}
static class Child extends Parent{
static int i=value;
public static void main(String[] args) {
System.out.println(Child.i);
}
}
}
输出20 不是10,因为父类的<linit>()方法优先得到了执行/
虽然java编译器会帮助class生成<linit>() 方法但是bu并不是总是生产,如果某个类里面没有静态代码块,也没有静态变量,那么就没有生成<linit>() 方法的必要了,接口同样如此,由于接口天生不能定义静态代码块,因此只有接口中有变量的初始化操作时才会生成<linit>() 方法。
<linit>()方法虽然是真实存在的,但是他只能被虚拟机执行,在主动使用触发了类的初始化之后就会调用这个方法,如果多个线程同时访问这个方法,会不会引起线程安全问题,如下:
public class ClassInit {
static {
try {
System.out.println("the classinit static code block whll be invoke,");
TimeUnit.MINUTES.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
IntStream.range(0, 5).forEach(i->new Thread(ClassInit::new));
}
}
运行代码发现同一时间,只能有一个线程执行到静态代码块中的内容,并且静态代码块仅仅会被执行一次,jvm保证<clinit>()方法在多线程执行环境下的同步语义。
开篇有个问题,下面来看一下
private static int x=0;
private static int b;
private static Singleton instance=new Singleton();
在链接阶段的准备过程中,每一个类变量都被赋予了相应的初始值;
x=0;y=0;instance=null
下面跳过解析过程,来看初始化阶段,初始化阶段会为每一个类变量赋予正确的值也就是执行<linit>() 方法的过程;
x=0;y=0;instance=new Singleton()
在 new Singleton 的时候会执行类的构造函数,而在构造函数中分别对x和y进行了自增,所以结果为:
x=1 y=1
再看调换顺序后的输出;
private static Singleton instance=new Singleton();
private static int x=0;
private static int b;
在链接阶段的准备过程中,每一个类变量都赋予了相应的初始值,
执行完instance的构造方法之后,各个静态变量的值如下:
[email protected] x=1 y=1
然后为x初始化,由于x没有显示的进行赋值因此0才是所期望的正确赋值,而y由于没有给定初始值,在构造函数中计算所得的值就是所谓的正确赋值,因此结果又会变成
[email protected] x=0 y=1