类的加载过程

一,类的加载过程

  当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行加载。(要注意的是,对于main函数所在的类,在程序刚开始运行时就会被加载。)

1,加载

  在加载之前,JVM要先通过类的全限定名从磁盘上寻找字节码文件(文件名.class):JVM根据系统环境变量的CLASSPATH里面找字节码文件的搜索路径(.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar)。路径中 . 代表当前工程目录,例如一般自己写的程序编译的字节码文件都在当前工作目录下,但是系统自带的类则在其他路径中,如:System  Out  类等。

  加载指的是JVM的类加载器将对象的字节码文件(student.class)中的二进制数据读入到内存的方法区中,然后类加载器在堆内存中为所有被载入内存中的类生成一个java.lang.Class实例对象,这个对象里面放的是该对象的属性,方法和访问限定等。类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

2,链接

  当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中。而类连接又可分为如下3个阶段。

  • 验证: 验证的目的在于确保Class对象文件的字节流中包含信息符合当前虚拟机要求,能够在JVM上运行,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

                   文件格式:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

                   元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

                   字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

      符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

  • 准备:类准备阶段负责为类的静态成员分配内存,并设置默认初始值(此处的设置初始值并不是真正的赋值)。若该类有基类,则继续加载基类,并为其基类的静态成员分配空间。(这就是为什么打印时先打印基类的static,在打印派生类的,因为是链接中的准备是从派生类一直往基类上进行的,链接准备完后直接停留在基类,所以初始化执行是从基类开始,又一直往下)
  • 解析:将常量池内的符号引用转换成直接引用。符号引用就是一组符号来描述所有引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等。 

       

3,初始化

    初始化是执行类构造器方法<clinit>(),为类的静态变量赋予正确的初始值,和调用类的静态初始化块。此类构造器方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在文件中出现的顺序执行。若该类具有父类,jvm会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。另外类构造器方法是不同于类的构造器(<init>())的。

    如果类中有语句:private static int a = 10,它的执行过程是这样的,(首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,)因为变量a是static的,则给a分配内存,此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,又把a的真正的值10赋给a,此时a=10。

二,类的加载时机

  Java程序对类的使用分为主动使用和被动使用两种情况。

  主动使用:

           1.创建类的实例,也就是new一个对象(首次)

              2. 访问某个类或接口的静态变量,或者对该静态变量赋值

              3.调用类的静态方法
              4.反射(Class.forName("com.lyj.load"))
              5.初始化一个类的子类(会首先初始化子类的父类)
              6.JVM启动时标明的启动类,即文件名和类名相同的那个类 

  除了以上情况,其他使用Java类的方法都被看作是对类的被动使用。需要注意只有在对类进行主动加载时,才会进行初始化,才会加载类的Class对象。

 

除此之外,下面几种情形需要特别指出:

       对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

三,类加载器

        类加载器负责从字节码文件中加载所有的类,并为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个Java类有一个唯一的标识一样,一个载入JVM的Clas类对象也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个Class类对象用其全限定类名(包括包名和类名)和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

  在jvm中表示两个Class对象是否为同一个类时,存在两个必要的条件:1,类的完整的类名包名必须一致  2,加载这两个类的ClassLoader也必须相同。

  要注意ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine(执行引擎)决定。同时加载的类信息存放在一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

  jvm支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)。从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都规划分为自定义类加载器。

              

  1)根类加载器(Bootstrap classloader):它用来加载 Java 的核心类库,是用原生代码(c/c++)来实现的,并不继承自 java.lang.ClassLoader,不是ClassLoader子类。负责加载JAVA_HOME/jre/lib/rt.jar,resourses.jar或sun.boot.class.path路径下的内容,用于提供jvm自身需要的类,是JVM运行时必须依赖的类库,也可理解为JVM是由这里面的class文件生成的,由C++实现。同时它也用来加载扩展类和应用类加载器,并且是他们的父类加载器。但由于该类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。当开发者要访问时,返回的则是null。

   比如获取String类的类加载器时,因为String类是由Bootstrap class loader加载的,所以在获取类加载器时,返回的就是null.

 

  2)拓展类加载器(Extensions class loader):它负责加载JRE的扩展目录,/jre/lib/ext或者由/java/ext/dirs系统属性指定的目录中的JAR包的类。是ClassLoader的子类,由Java语言实现,如果用户创建的JAR放在此目录下,也会由拓展类加载器来加载。其父类加载器为null(在本质上它是有父类加载器,为Bootstrap class loader,但是因为Bootstrap class loader底层的jar包是用C和C++实现的,而在java中是无法识别c和c++语言的,所以当Extensions class loader在获取其父类加载器时,返回的是null。)注意父类和父类加载器的区别!

 

      3)系统类加载器(System class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。由Java语言实现,是ClassLoader的子类,其父类加载器是扩展类加载器。可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为类加载器。

 

  4)ClassLoader:它是一个抽象类,由c或c++语言编写,是所有类加载器的父类,不包括启动类加载器。

 

四、类加载机制:

1.JVM的类加载机制主要有如下3种。

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,就是当要求子类去加载某类时,则先让父类加载器试图加载该Class,如果该父类加载器还存在其父类加载器,则继续向上委托,依次递归,直到请求到启动类加载器。只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类 的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

双亲委派机制的优势:采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

 五.类的生命周期

类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 连接,连接又包含三块内容:验证、准备、初始化。1)验证,文件格式、元数据、字节码、符号引用验证;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用
  • 初始化,为类的静态变量赋予正确的初始值
  • 使用,new出对象程序中使用
  • 卸载,执行垃圾回收

六,加载类的三种方式

1,Class   c = Student .class;

   该方法仅能将Student类的Class对象加载到内存,不会进行初始化。

注意:要用类名.class,因为此时没有生成对象

    所以不会有打印结果。

 2,Class  c2=Class.forName(" 包名.要加载的类的类名");

    将类加载进来,并进行内存分配和初始化赋值

    此时借助的使Class对象的方法,并将包名.要加载的类的类名赋给该方法

   所以打印结果会有静态初始化块中的语句。

 3.Student student =new Student("张三“,20,‘男’,”西安工业大学“);

    Class  c3=student.getClass();

    将类加载进来,并进行内存分配和初始化赋值,此时也会生成类的实例.

    注意:此时是通过生成对象的getClass()来进行类的加载。

 运行结果

 ******************************************************************

 1 package L18;
 2 class Student{
 3     private  String name;
 4     private int age;
 5     private char sex;
 6     private static String school;
 7     static{
 8         System.out.println("类的静态初始化块");
 9         school="西安工业大学1";
10     }
11     {
12         System.out.println("类的实例初始化块");
13     }
14     public Student(String name,int age,char sex,String school){
15         System.out.println("类的构造函数");
16         this.name=name;
17         this.age=age;
18         this.sex=sex;
19         this.school=school; //静态成员变量也可以在构造函数中对其赋初值
20     }
21 
22     @Override
23     public String toString() {
24         return "Student{" +
25                 "name='" + name + '\'' +
26                 ", age=" + age +
27                 ", sex=" + sex +
28                 ", school=" + school +
29                 '}';
30     }
31 } 
32 public class ClassLoadCase {
33     public static void main(String[] args) throws Exception {
34         //Class c=Student.class;   //仅将类的Class对象加载进来
35         // Class c2=Class.forName("L18.Student");  
36         Student student=new Student("张三",20,'男',"西安工业大学");
37         Class c3=student.getClass();  //将类加载进来,并进行内存分配和初始化赋值,此时也会生成类的实例
38     }
39 }

 六,JVM的内存划分

   栈内存(stack): 函数及函数里面的局部变量(简单/引用)的内存

                              (函数定义处开辟栈空间,当出了函数右大括号,则栈内存回收,函数里面定义的变量的内存同时也会被回收)

    堆内存(heap):new的对象,constant  pool 常量池

                              (new的时候开辟空间,当没有被引用的时候,就会被GC在下一个回收周期所回收(不是立即!!此外还要判断对象有没有重写finalize方法))

     方法区(method area):Class对象, static的成员变量

                              (类加载的时候就在方法区开辟空间,一般持续到应用程序结束)

 

posted @ 2019-11-01 19:00  小L要努力吖  阅读(998)  评论(0编辑  收藏  举报