jvm加载class文件的原理机制分析

来源:互联网 发布:最优化算法 中科院 编辑:程序博客网 时间:2024/06/16 04:00

个人博客:www.letus179.com

案例分析

A、B类中均包含静态代码块,非静态代码块以及构造器,A类是B类的父类。

public class A {  static {    System.out.print("A中静态代码块>>>");  }  {    System.out.print("A中非静态代码块>>>");  }  public A() {    System.out.print("A中构造器>>>");  }}
public class B extends A{    static {        System.out.print("B中静态代码块>>>");    }    {      System.out.print("B中非静态代码块>>>");    }    public B() {        System.out.print("B中构造器>>>");    }}

那么看看下面代码的运行结果。

public class ABTest {  public static void main(String[] args) {    A ab = new B();    System.out.println("\n==========================\n");    ab = new B();  }}

执行结果为:

A中静态代码块>>>B中静态代码块>>>A中非静态代码块>>>A中构造器>>>B中非静态代码块>>>B中构造器>>>==========================A中非静态代码块>>>A中构造器>>>B中非静态代码块>>>B中构造器>>>

总结:
1. 同一类中:静态代码块 => 非静态代码块 => 构造器
2. 父子类中:父类 => 子类;
3. 静态代码块只在第一次实例化(new)执行了,非静态代码块在每次实例化都执行。

看执行结果,上面的3条总结都没问题,对于第3点,需要注意下:静态代码块其实不是跟着实例走的,而是跟着类走。看如下测试,通过Class.forName()动态加载类:

  public static void main(String[] args) throws ClassNotFoundException {    Class.forName("B");  }

执行结果:

A中静态代码块>>>B中静态代码块>>>

这里并没有执行实例化过程,但是静态代码块却执行了,这也证明了静态static代码块并不是跟着实例走。下面将简单介绍下类加载相关概念及过程,介绍完后再看看上面的例子,印象会更深刻。首先得了解下几个比较重要的JVM的内存概念。

jvm的几个重要内存概念

方法区

专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域。

常量池

是方法区的一部分,主要用来存放常量和类中的符号引用等信息。

堆区

用于存放类的对象实例,如new、数组对象。

栈区

由一个个栈帧组成的后进先出的结构,主要存放方法运行时产生的局部变量、方法出口等信息。

java类的生命周期

我们编写完一个.java结尾的源文件后,经过编译后生成对应的一个或多个.class后缀结尾的文件。该文件也称为字节码文件,能在java虚拟机中运行。而类的生命周期正是:从类(.class文件)被加载到虚拟机内存,到从内存中卸载为止。整个周期一共分为7个阶段:

加载,验证,准备,解析,初始化,使用,卸载

其中

  • 验证,准备,解析统称为连接
  • 加载,验证,准备,初始化,卸载,这5个的顺序是确定的。

值得注意的是,通常我们所说的类加载指的是:加载,验证,准备,解析,初始化,这5个阶段。

加载

该阶段虚拟机的任务主要是找到需要加载的类,并把类的信息加载到jvm的方法区中,然后中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

连接

连接阶段有三个阶段:验证,准备,解析。主要任务是加载后的验证工作以及一些初始化前的准备工作

验证

当一个类被加载后,需要验证下该类是否合法,以保证加载的类能在虚拟机中正常运行。

准备

该阶段主要是为类的静态变量分配内存并设置为jvm默认的初始值;对于非静态变量,则不会为它们分配内存。这里静态变量的初始值,不是由我们指定的,是jvm默认的。

  • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0;
  • 引用类型默认值是null;
  • 常量的默认值为我们设定的值。比如我们定义final static int a = 1000,则在准备阶段中a的初始值就是1000。

解析

这一阶段的任务是把常量池中的符号引用转换为直接引用,也就是具体的内存地址。在这一阶段,jvm会将所有的类、接口名、字段名、方法名等转换为具体的内存地址。譬如:我们要在内存中找到一个类里面的一个叫call的方法,显然做不到,但是该阶段,由于jvm已经将call这个名字转换为指向方法区中的一块内存地址了,也就是说我们通过call这个方法名会得到具体的内存地址,也就找到了call在内存中的位置了。

初始化

有且仅有 5种情况必须立即对类进行“初始化”

  1. 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已经在编译器把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候;
  2. 使用java.lang.reflect包的方法进行反射调用时,若类没有进行初始化,需要先触发其初始化;
  3. 当初始化一个类时,若其父类还没有进行初始化,则需要先触发其父类的初始化;
  4. 执行main方法,虚拟机会先初始化其包含的那个主类
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化(这一点不是很懂)。

在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。(这正好解释了案例中第3点结论)

使用

初始化阶段的5种情况用了很强烈的限定词:有且仅有,这5种行为称为对一个类进行“主动引用”。其他所有引用类的方法(行为)都不会对类进行初始化,称之为“被动引用”

《学习深入Java虚拟机》一书中列举了3个被动引用例子,我验证了下,确实如此,不过还得到了新的启发。这里列出其中的2个例子,如下:

例子1:通过子类引用父类的静态字段,不会导致子类初始化

package classloading;public class SuperClass {    static {        System.out.println("SuperClass init!");    }    public static int value = 123;}
package classloading;public class SubClass extends SuperClass{    static {        System.out.println("SubClass init!");    }}
package classloading;public class NotInitialization {    public static void main(String[] args) {        System.out.println(SubClass.value);    }}

执行结果:

SuperClass init!123

结论:
通过子类SubClass来引用父类SuperClass的静态字段value,初始化的只是父类,并不会触发子类的初始化。

例子2:常量在编译阶段会存入调用类的常量池中,不会触发定义常量的类的初始化

package classloading;public class ConstClass {    static {        System.out.println("ConstClass init!");    }    public static final String HELLO_WORLD = "hello world";}
package classloading;public class NotInitialization {    public static void main(String[] args) {        System.out.println(ConstClass.HELLO_WORLD);    }}

执行结果:

hello world

结论:
从打印的结果可以看到,并没有初始化ConstClass类;但是从源码上看是引用了ConstClass类的常量。因为在NotInitialization类的编译期中,通过常量优化,已经将常量 "hello world"存储到了NotInitialization类的常量池中了。也就是说,NotInitialization中引用ConstClass.HELLO_WORLD其实是对自身常量池中常量引用

卸载

在使用完类后,需满足下面,类将被卸载:
1. 该类所有的实例都已经被回收,也就是java队中不存在该类的任何实例;
2. 加载该类的ClassLoader已经被回收了;
3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

当上面三个条件都满足后,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程本质上就是在方法区中清空类信息,结束整个类的生命周期。

jvm加载class文件的原理机制

面试题中经常会问到JVM加载Class文件的原理机制,结合上面的分析,引用下面网上的分析,更加容易理解。

        JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
        由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:  1)  如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;  2)  如果类中存在初始化语句,就依次执行这些初始化语句。
        类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。

下面是关于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
  • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
  • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
原创粉丝点击