JVM类加载机制和创建对象的过程

1、JVM运行和类加载过程

一、类的加载机制

类加载机制:JVM把class文件加载到内存,并对数据进行校验,解析和初始化,最终形成JVM 可以直接使用的Java类型的过程。

主要有三步:加载、连接、初始化。其中连接又可以细分为:验证、准备和解析。

加载:类的加载是指把.class文件中的二进制数据读入到内存(方法区)中,将字节流代表的静态存储结构转化成方法区的运行时数据结构,之后在堆区创建一个(java.lang.Class)Class对象,作为访问方法区的类信息的接口。

链接:将Java类的二进制代码合并到JVM的运行时数据的过程

(1)验证

  确保加载的类信息符合JVM规范,没有安全方面的问题。

(2)准备

  正式为类变量(static 变量)分配内存,并设置类变量的初始值

(3)解析

  虚拟机常量池内的符号引用替换为直接引用的过程

初始化:执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并而成的。

 

(重点)类的主动引用和被动引用的区别

类的主动引用(一定会发生类的初始化)

—— new一个类的对象

—— 调用类的静态成员(除了final常量)和静态方法

—— 使用java.lang.reflect包的方法对类进行反射调用

—— 当虚拟机启动,先启动main方法所在的类

—— 当初始化一个类,如果父类没有被初始化,则先初始化它的父类

 

类的被动引用(不会发生类的初始化)

—— 当访问一个静态域时,只有真正声明这个域的类才会被初始化

    通过子类引用父类的静态变量,不会导致子类初始化

—— 通过数组定义类引用,不会触发此类的初始化

—— 引用final常量不会触发此类的初始化(常量在编译阶段就存入类的常量池中)

二、JVM的内存结构

(1)方法区存放一些类的运行时数据:

1、静态变量

2、静态方法

3、静态代码块

4、常规量池

(2)堆存放对象本身

生成java.lang.Class对象,并且可以指向方法区中定义的类的信息

(3)方法栈

将方法的栈帧压入栈中,当方法执行结束后从栈中pop出来。

 

三、 案例分析

packagecom.oracle.List;

/**

 * 类的加载和初始化只执行一次!

 * @author zhegao

 */

public class M {

  public static void main(String[] args) throws ClassNotFoundException,InstantiationException, IllegalAccessException {

        /**类的主动引用(一定会发生类的初始化),

         *初始化的顺序:初始化静态信息(先父再子)

         */

        //第一种: new一个类的对象

         //A a1 = new A(); 

        /*

         * 结果:

         * Father is very happy

            A is so happy!

            I am A's father...

            A is good

         */   

        //第二种:调用类的静态成员(除了final常量)和静态方法

        // System.out.println(A.age);

       

        //第三种:使用java.lang.reflect包的方法对类进行反射调用

        //Class.forName("com.oracle.List.A");

       

        /**

         *类的被动引用(不会发生类的初始化)

         */

        //第一种:当访问一个静态域时,只有真正声明这个域的类才会被初始化,通过子类引用父类的静态变量,不会导致子类初始化

        //System.out.println(B.age); //这里只有A初始化了,而由于静态域ageA的属性,所以B未被初始化!

       

        //第二种:通过数组

        A[] as = new A[10];

       

        //第三种:引用常量不会触发此类的初始化

        System.out.println(A.height);

         /*

         *结果

         *185

         */      

    }

}

class B extends A{

    public B() {

        System.out.println("B is also good.");

    }

}

class A extends Father{

    public static int age = 27;

    public static final intheight = 185;

    static {

        System.out.println("A is so happy!");

    }

    public A() {

        System.out.println("A is good");

    }

}

class Father{

    static {

        System.out.println("Father is very happy");

    }

    public Father() {

        System.out.println("I am A's father...");

    }

}


2、对象的创建过程(这部分参考于博客https://www.cnblogs.com/chenyangyao/p/5296807.html书籍可以参考《深入理解java虚拟机 第二版》P44-49

   对象的创建过程一般是从new指令(我说的是JVM的层面)开始的(具体请看图1),JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程(具体加载过程可参见我的另一篇博文)。符号引用解析完毕之后,JVM会为对象在堆中分配内存,HotSpot虚拟机实现的JAVA对象包括三个部分:对象头、实例字段和对齐填充字段(具体内容请看图2),其中要注意的是,实例字段包括自身定义的和从父类继承下来的(即使父类的实例字段被子类覆盖或者被private修饰,都照样为其分配内存)。

    为对象分配完堆内存之后,JVM会将该内存(除了对象头区域)进行零值初始化,这也就解释了为什么JAVA的属性字段无需显示初始化就可以被使用,而方法的局部变量却必须要显示初始化后才可以访问。最后,JVM会调用对象的构造函数,当然,调用顺序会一直上溯到Object类。

    至此,一个对象就被创建完毕,此时,一般会有一个引用指向这个对象。在JAVA中,存在两种数据类型,一种就是诸如int、double等基本类型,另一种就是引用类型,比如类、接口、内部类、枚举类、数组类型的引用等。引用的实现方式一般有两种,具体请看图3。

                             JVM类加载机制和创建对象的过程_第1张图片

                                                            图1  对象的创建过程

      在这里,楼主做了一个问题的思考,对于这段java代码: Instance  instance = new Instance(),在单例模式下的线程安全是如何产生的?

     答:基于图一,我们可以知道new Instance()主要进行了三个步骤(假设类已经被加载、链接和初始化):

(1)为对象分配内存空间,并初始化为零值  ---->  (2) 调用对象的实例化init方法  ----> (3)将内存地址指向instance变量。

 (1)(2)(3)这是我们接受的执行顺序,但是由于CPU在执行时做了局部的优化,称之为“重排序”。最终,导致执行的顺序可能是(1)(3)(2)。该顺序在“多例”模式下没有影响,毕竟最终结果都一样,但是在并发环境的“单例”模式下会引发“线程”安全问题,即多个线程新建的对象不一致,或者更严重的是,新建的对象没有被实例化,所以需要“DCL”的双重检查锁并配合volatile关键字使用。具体参考http://blog.csdn.net/qq_29864971/article/details/79321095。

结论:new Instance()并非是一个严格意义的“原子性”操作!,它包含了一些不同的步骤,所以在多线程的“单例”模式下,需要考虑其线程安全问题。这样的问题在一些框架中也会出现,例如:Spring整合Struts1中,原本由Struts控制的action,会兼并到spring容器中(因为struts中,action默认为单例,而兼并到Spring容器后,action的 scope="prototype")

        JVM类加载机制和创建对象的过程_第2张图片

                                                                 图2 对象的组成结构


           JVM类加载机制和创建对象的过程_第3张图片

                                                      图3  对象引用的两种实现方式

     通过图3可见,如果通过“直接指针”访问对象,那么对象的布局中,对象头将持有指向“对象类型数据”/类元数据的指针。

但是这两种方式各有优势:

1)方式一(句柄池)的优缺点

    优点:栈中的reference存储的是“句柄”地址,它比较稳定不易改变,即使对象被移动或者被垃圾回收器回收,只会改变句柄池中的实例对象的指针,reference本身不会被修改

    缺点:创建句柄池,增加内存的开销。

 2)方式二(直接指针)的优缺点

   优点:节省内存,并且直接指针的速度更快

    缺点:指针移动,reference也会做修改




你可能感兴趣的:(JVM学习笔记)