Java类加载机制

咕咚 发表于 2015-10-20 阅读次数

作为一个Java程序员,我们写过很多Java类,那他们具体是怎么运行起来的呢? 一开始,我也没有去关心过这个问题,或者说是自己没有在这方面思考过,但是后来觉得,既然每天都在做这些事,为什么不深入了解下具体的原理逻辑呢。知其然还应该知其所以然,作为一个有追求程序员,我们应该对自己有更高的要求。博文是自己对Java类加载的一些认识,如有错误欠缺,欢迎指正补充。

这里,我们在开始主题之前,先梳理一下上面提出的这个问题。

首先举个简单的例子,来引出我们的问题

比如,现在需要我们编写一个Java类Person,他有一个包名比如说com.gudong.demo,然后这个类会有一个修饰符 public,具体代码就应该是下面的样子,

package com.gudong.demo
public class Person{

}

接着类里面我们会定义一些成员变量,比方说,定义一个int型的age来表示Person的年龄

private int age;

也有可能定义一个类属性,比如说人的最大年龄

public static final int MAX_AGR = 122;//from wiki

接着我们还会定义一些方法,比如main方法

public static void main(String[]args){
     System.out.println("hello world!");
}

还有可能定义一些内部类

static class Area{
    private String name;
}

在一个类里,我们还可以定义除上面所说的很多种数据。

当我们写完一个类,我们可能做的就是运行了,点击运行按钮,我们的程序就可以按照我们预期的方式执行了, 这里一定要明白一点,因为我们大多数时候用的是IDE,所以这里省去了自己编译的步骤,如果你使用命令行进行编写代码,那么你一定知道, 在执行Java代码前,一定要先执行javac命令,也就是编译命令。 javac全称应该叫做java compile 如果没猜错的话。

所以我们写完代码,首先要进行应该是编译,那么编译会做什么事呢?

编译会把我们用java写的代码转化成我们经常听说的字节码,字节码是什么鬼?

说到这里,是时候搬出Java的一大特性了-跨平台。想必很多人都知道这个特性,你会发现工作中,你同事用的是Mac在写Java代码,而你用着windows在开发,最终你们写完的代码, 不论在MAC上打包还是windows上打包,最终打包出来的apk,运行起来都是一个样式,根本没有任何差异,其原因就在于不论是MAC上的源码还是Window上的源码, 在运行之前都要经过Java虚拟机进行编译,不同的开发平台,Oracle公司都有一个对应的Java虚拟机实现,但是经过虚拟机编译后的源码都会变成遵循同样规范的一个文件, 这就是我上面说的字节码。

字节码是一个和平台无关的文件,不论任何平台,任何Java虚拟机,编译Java源码后生成的字节码文件,都应该是一致的, 字节码文件具体就是经常在build目录下看到的以.class结尾的文件

所以说到这里,如果你够牛B,你也可以按照Java虚拟机规范(这个规范是公布于众的,市面也有相关的中文书籍),实现自己的Java虚拟机,然后自己公司的代码, 用自己的编译器进行编译,这个听上去,确实刁刁的,不过要能做到 ,呵呵,我就不遐想了,目前的层次还差好多,而且也没什么意义,至少目前是,国内据说腾讯自己有搞,不知道具体如何。

到这里,关于字节码,应该有点概念了,我们写完的代码,经过编译,会变成已.class结尾的java字节码文件。

具体的字节码,是一组以8位字节为基础的二进制流,它包含以下几个部分,具体字节码的数据存储格式都是按照Java虚拟机的规范来的, 任何Java虚拟机必须严格安装规范来,以确保不同的,Java虚拟机编译出的字节码是格式、内容相同的文件。 关于字节码文件的结构,如下所述,下面这部分来自摘抄

魔数和class文件版本:类文件开头的四个字节被定义为CAFEBABE,只有开头为CAFEBABE的文件才可以被虚拟机接受,接下来四个字节为class文件的版本号,高版本JDK可以兼容以前版本的class文件,但不能运行以后版本的class文件。

常量池:可以理解为class文件中的资源仓库,它包含两大类常量:字面量和符号引用,字面量包含文本字符串,声明为final的常量值等,符号引用包含类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

访问标志:常量池结束后,紧接着两个字节表示访问标志,用于识别一些类或接口层次的访问信息,例如是否是public,是否是static等。

类索引,父类索引,和接口索引集合:类索引用来确定这个类的全限定名,父类为父类的全限定名,接口索引集合为接口的全限定名。

字段表集合:用于描述接口或者类中声明的变量,但不包含方法中的变量。

方法表集合:用于表述接口或者类中的方法。

属性表集合:class文件,字段表,方法表中的属性都源自这里。

经过编译,一个.java文件已经变成了一个.class字节码文件,下面要真真使用这个.class时,虚拟机对这个文件又会做什么,这就是接下来的类加载。

类加载

首先从一个简单的实例开始。比如说,现在我们已经写好了自己Person类,并且编译成功,现在是Person类以Person.class的形式存在于计算机的一个地方(必然是自己的项目编译目录下)。

现在自己写一个Test类,通过使用Person来实例化一个对象,从而分析Person类的加载过程,如下

public class Test{
    //主方法
    public static void main(String[]args){

         Person p1 = new Person();
         p1.name = “大虾”;

          System.out.println(“p1 name is “+p1.name);

         Person p2 = new Person();
         p2.name = “咕咚”;

         System.out.println(“p2 name is “+p2.name);

         System.out.println(“p1 max age “+Person.MAX_AGE);
    }
}

可以看到,上面我们定义了两个Person对象p1和p2。

对于Test类,他经过编译,然后运行,在第一次运行时,虚拟机也会先加载Test.class文件到内存,然后执行方法,暂且不细究Test的加载。

当执行Main方法,执行到第一条代码语句时,他看到这里需要new 一个Person,此时他会查看内存中是不是已经存在Person.class类对象,如果没有,就会根据Person的包路径, 也就是具体的本地存储路径,然后找到Person.class的存储位置,然后虚拟机就会把这个字节码的二进制流加载到内存,具体类加载分为以下几个过程

加载

加载是类加载的第一个阶段,虚拟机要完成以下三个过程:

  • 1)通过类的全限定名获取定义此类的二进制字节流。
  • 2)将字节流的存储结构转化为方法区的运行时结构。
  • 3)在内存中生成一个代表该类的Class对象,作为方法区各种数据的访问入口。

验证

目的是确保class文件字节流信息符合虚拟机的要求。

准备

为static修饰的变量赋初值,例如int型默认为0,boolean默认为false。

解析

虚拟机将常量池内的符号引用替换成直接引用。

初始化

初始化是类加载的最后一个阶段,将执行类构造器()方法,注意这里的方法不是构造方法。 该方法将会显式调用父类构造器,接下来按照java语句顺序为类变量和静态语句块赋值。

在执行完成类加载的第一步初时,会在内存中(具体是在堆内存)中生存一个代表该类的Class对象,针对Person类,此时虚拟机会在堆内存空间,开辟一块空间 用于存储Person.class这个对象,如下图所示

usage

接着如果这个类有类属性,如我们定义在Person中的MAX_AGE,那么他在类准备阶段,虚拟机也会在在类对象所在的内存块,给她分配一段空间,最终在类初始化完成时,类对象的状态如下。

usage

此时类加载完毕,现在需要去实例化Person p1 ,此时虚拟机会利用已经加载到内存中的Person.class,来生成一个p1的实例,此时p1这边变量处于栈空间,具体的对象位于堆空间,

继续向下执行,需要实例化p2了,因为虚拟机检查到Person.class已存在于内从,此时直接执行实例化过程,最终的内存分配情况如下图所示

usage

到这里就可以看到为什么有类加载这个过程了。

通过上面分析会发现,如果为一个类设置了类属性,也就是用static去修饰的成员变量,他会在类加载完成后,就一直存在于内存,当然如果发生GC,他有可能被回收,一般情况下,可以直接 通过类名去访问这块空间,因为他是在类对象的空间内,

相比类属性,对成员属性来说,如Person的age、name属性,都是在具体实例化类时才会去单独分配内从。

关于类加载,这是只说了一部分,具体成员变量的内存分配,以及临时变量的内存分配还有方法的执行过程,还有很多内存操作相关的知识点,自己可以去搜索更多的博客去学习,这里只是 把自己的理解说了出来,如有错误,欢迎指正。

最后,了解类加载还有内存分配有什么意义?

其实我们平时的开发过程中,好像用不到这些知识,但是这些都是最根本的基础,不了解好像对日常的开发也没什么大碍,但是如果你了解了这些细节的东西, 那么你在编写每一个类,定义每一个变量时,你就可以更加清楚的知道,它在内存中的状态。有利于对代码有更深刻的认识和了解。

这些如果你细究,都是有很多的学问,这里只是说出了一点点的东西,做个记录!

###参考文档 Java 虚拟机类加载机制和字节码执行引擎

Java虚拟机类加载和执行机制