JAVA虚拟机理解

一、类加载过程
类从被加载到虚拟机内存中开始,到卸载为止,生命周期包括:加载(loading),验证(verification),准备(preparation),初始化(initialition),使用(using),卸载(unloading)七个阶段,其中准备、验证、解析3个部分统称为连接。
实例图
加载-验证,准备,初始化和卸载这5个阶段的顺序是确定的,类加载过程必须按照这中顺序按部就班的开始,而解析阶段则不一定:有可能在初始化开始之后才开始,这是为了支持JAVA语言的动态绑定,

加载
加载阶段要完成
1、通过一个类的全限定名来获取此类的二进制字节流(可以在class文件,网络、动态生成、数据库等)
2、将这个字节所代表的静态存储结构转化为方法去的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.class对象。作为方法区这个类的各种数据的访问入口;
夹在阶段和链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,在加载阶段尚未完成,链接阶段可能已经开始,但然保持着先后顺序。
验证
这一阶段是为了确保class文件的字节流中包含的信息符合虚拟机的要求,并不会伤害到虚拟机的安全
几个阶段
1、文件格式验证:雅正字节流是否符合ckaa文件格式的规范;
例如:是否以魔术0xCAFEBABE开头,主次版本好是否在当前虚拟hi的处理范围之内。常量池中的常量是否又不被支持的类型。
2、元数据认证:对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求;
例如:L这个类是否有父类。
3、字节码验证:通过数据流和控制流分析,却dig程序雨衣是合法的,符合逻辑的
4、字符引用验证:却把解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,他对城西运行期没有影响。

准备
准备夹断是正式为类变量分配内存并设置变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰额变量),而不包括实例的变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
pulic static int vallue = 123;
那变量value在准备阶段过后的初始值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putsatic指令是程序变异后,存放于类结构器方法之中。所以把value赋值为
123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value = 123,即当类型字段的字段属性是cnstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0

解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、借口方法、方法句柄和调用点限定符7类符号引用进行。

初始化
雷初始化阶段是类加载过程的最后一部,在初始化阶段,才是真正开始执行类中定义的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序指定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程。
()方法是右边一起自动手机类中的所有类变量的赋值动作和静态语句块static{}只能当问到定义在静态语句跨之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

public class Test
{
    static
    {
        i=0;
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}

clinit()方法与实例构造器init()方法不同,他不需要显式的调用的父类够扎起,虚拟机会保证在子类init()执行之前,父类的clinit()方法已经执行完毕
clinit()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产clinit()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法。但接口与类不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果在一个类的clinit()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

package jvm.classload;

public class DealLoopTest
{
    static class DeadLoopClass
    {
        static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init DeadLoopClass");
                while(true)
                {
                }
            }
        }
    }

    public static void main(String[] args)
    {
        Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread()+" run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

输出为

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass

需要注意的是,其他线程虽然会被阻塞,但如果执行clinit()方法的那条线程退出clinit()方法后,其他线程唤醒之后不会再次进入clinit()方法。同一个类加载器下,一个类型只会初始化一次。
将上面代码中的静态块替换如下:

static
        {
            System.out.println(Thread.currentThread() + "init DeadLoopClass");
            try
            {
                TimeUnit.SECONDS.sleep(10);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }

输出为

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (之后sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over

虚拟机规范严格规定了有且只有5中情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含mian()方法的那个类),虚拟机会现初始化这个主类。
  5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

有趣的问题解决

package jvm.classload;

public class StaticTest
{
    public static void main(String[] args)
    {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static
    {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest()
    {
        System.out.println("3");
        System.out.println("a="+a+",b="+b);
    }

    public static void staticFunction(){
        System.out.println("4");
    }

    int a=110;
    static int b =112;
}

输出为

2
3
a=110,b=0
1
4

原文解答
个人理解
*

  1. 在准备阶段,主要做的是类变量分配内存并且设置默认值,因此st为null,b为0
    初始化阶段需要执行类构造器(类构造器是编译器收集所有静态语句块和类变量的赋值语句在源码中的顺序合并生成的类构造器,对象的构造方法是init(),类的构造方法是clinit(),可以在堆栈信息中看到)

  2. 开始初始化阶段,先进行静态初始化。
    1)遇到了mian方法触发初始化,
    2)因此第一条执行的是st = new staticTest(),此时开始对象的初始化
    3)对象的初始化是现初始化成员变量再执行构造方法,因此,顺序为a=110–>打印2–>执行构造方法(a=112,b=0)
    4)继续执行剩余的类构造器的语句,也就是输出1,输出4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值