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

1、JVM运行和类加载过程

一、类的加载机制

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

 

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

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

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

(1)验证

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

(2)准备

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

(3)解析

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

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

 

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

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

—— new一个类的对象

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

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

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

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

 

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

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

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

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

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

 

二、JVM的内存结构

(1)方法区/永久代(元数据区)

    永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据.。

    1、静态变量

    2、静态方法

    3、静态代码块

    4、运行时常量池

(2)堆存放对象本身

   创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域

(3)虚拟机栈(线程私有)

   每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 

(4) 程序计数器(线程私有)

    一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。 这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。 

(5)本地方法栈

  本地方法区和函数栈作用类似, 区别是虚拟机栈为执行Java方法服务, 而本地方法栈则为 Native方法服务, 如果一个VM实现使用C-linkage模型来支持Native调用, 那么该栈将会是一个 C栈,但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。 

注意:

    1、线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束

    2、Java程序的每个线程都与操作系统的本地线程直接映射

   3、线程共享区域随虚拟机的启动/关闭而创建/销毁。 

   4、直接内存并不是JVM运行时数据区的一部分,。 在JDK 1.4引入的NIO提 供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作, 这样就避免了在 Java 堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能。 

 

三、 案例分析

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);

   /*

         * 结果:

         * Father is very happy

            A is so happy!

            27

         */   

        //第三种:使用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)

  当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。其中要注意的是,实例字段包括自身定义的和从父类继承下来的(即使父类的实例字段被子类覆盖或者被private修饰,都照样为其分配内存)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化实例代码块初始化 以及 构造函数初始化。当然,构造函数的调用顺序会一直上溯到Object类。

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

                            

                                                            图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")

       

                                                                 图2 对象的组成结构

 

          

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

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

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

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

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

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

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

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

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

 

 

 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值