zoukankan      html  css  js  c++  java
  • JAVA 类加载机制学习笔记

    JAVA 类生命周期

      如上图所示,Java类的生命周期如图所示,分别为加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析这三个步骤统称为链接。

      加载:JVM根据全限定名来获取一段二进制字节流,将二进制流转化为方法区的运行时数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据访问入口。

      验证:验证是链接的第一步,主要验证内容分为文件格式验证、元数据验证、字节码验证、符号引用验证。

      准备:准备阶段是为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

      解析:解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。符号引用所引用的目标不一定在内存中,而转换为直接引用之后,引用直接指向到对应的内存地址。

      初始化:初始化会执行<Clinit>()方法,会对static属性进行赋值,对于static final修饰的基本类型和String类型则在更早的javac编译的时候已经加载到常量池中了。

      经过初始化之后,Java类已经加载完毕。

      

      JVM并没有强制规定什么时候进行类的加载,但是对于初始化规定了有且5种情况必须被初始化:

    1. 遇到new、getstatic、putstatic、invokestatic这四个字节码的执行时,如果类还没有被初始化,则必须被初始化。new为创建对象,剩下三个为操作静态变量。
    2. 使用java.lang.reflect对类进行反射操作的时候,如果该类还没有被加载,则加载该类。
    3. 如果对一个类进行初始化的时候,要先对其父类先进行初始化。
    4. 当虚拟机启动的时候,需要一个Main方法入口,虚拟机会先初始化这个类。
    5. 当使用JDK1.7动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最终解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,如果对应的类没有初始化、则会被初始化。

      常在笔试题中遇到的就是类记载相关知识,如下面代码,先不看答案想想会打印出什么

    例子1:

     1 public class SuperClass {
     2     public static int value = 123;
     3 
     4     static {
     5         System.out.println("super class init");
     6     }
     7 }
     8 
     9 public class SubClass extends SuperClass {
    10 
    11     static {
    12         System.out.println("Sub class init");
    13     }
    14 }
    15 
    16 public class ClassInit {
    17     public static void main(String[] args) {
    18         System.out.println(SubClass.value);
    19     }
    20 }

       打印结果如下:

      

      解析:当Main方法执行的时候,不会对SubClass类进行初始化,因为调用静态变量时只会初始化直接定义该变量的类,因此,上述代码只有SuperClass会被初始化。而SubClass并不会被初始化。

      我们稍微修改一个上述代码,将main方法放入子类中执行,执行main方法之后,代码会怎么执行呢?

    例子2:

     1 public class SuperClass {
     2     public static int value = 123;
     3 
     4     static {
     5         System.out.println("super class init");
     6     }
     7 }
     8 
     9 public class SubClass extends SuperClass {
    10 
    11     static {
    12         System.out.println("Sub class init");
    13     }
    14 
    15     public static void main(String[] args) {
    16         System.out.println(SubClass.value);
    17     }
    18 }

      打印如下图:

      

      解析:根据上述类初始化规定。根据第四条执行main方法时候必须初始当前类,因此触发了SubClass的初始化。根据第三条,如果要触发SubClass,必须先对SuperClass进行初始化。因此会先进行SuperClass的初始化、执行完成后执行SubClass初始化,最后等SubClass初始化完毕,打印出Main方法的中的语句。

    例子3:

     1 public class StaticTest {
     2 
     3     static int b = 200;
     4 
     5     static StaticTest st = new StaticTest();
     6 
     7     static {
     8         System.out.println("1");
     9     }
    10 
    11     {
    12         System.out.println("2");
    13     }
    14 
    15     StaticTest() {
    16         System.out.println("3");
    17         System.out.println("a=" + a + ",b=" + b+",c="+c+",d="+d);
    18     }
    19 
    20     public static void staticFunction() {
    21         System.out.println("4");
    22     }
    23 
    24     int a = 100;
    25 
    26     static int c = 300;
    27 
    28     static final int d=400;
    29 
    30     public static void main(String[] args) {
    31         staticFunction();
    32     }
    33 }

       执行结果如下:

      

      分析:代码执行之后的结果跟我一开始预想的不大一样,我们按照执行顺序进行分析。当我们执行Main方法的之前,javac需要先将代码编译,在这个时候d属性已经完成了赋值。前面说过,在执行main方法之前,会对main方法所在的类进行初始化。根据属性是否静态,我们大概可以将代码分为两部分:

      1、静态代码

     1     static int b = 200;
     2 
     3     static StaticTest st = new StaticTest();
     4 
     5     static {
     6         System.out.println("1");
     7     }
     8     public static void staticFunction() {
     9         System.out.println("4");
    10     }

      2、非静态代码:

     1     {
     2         System.out.println("2");
     3     }
     4 
     5     StaticTest() {
     6         System.out.println("3");
     7         System.out.println("a=" + a + ",b=" + b + ",c=" + c + ",d=" + d);
     8     }
     9 
    10     int a = 100;

      把代码分成两部分,主要是为了区分哪些是类初始化里的代码(<clinit>()中的代码,在类初始化的时候执行),哪些对象初始化代码(<init>()中的代码,对象初始化的时候执行)。main方法触发了类的初始化,因此会执行<clinit>()中的代码,执行顺序从上而下,先完成b=200赋值语句,紧接着执行 static StaticTest st = new StaticTest(),而对st的赋值则触发了对象初始化方法,因此会执行<init>()方法,即非静态代码,对象的初始化执行顺序和类初始化执行顺序不相同,类初始化执行顺序  属性初始化 =》代码块 =》方法初始化。因此在非静态代码中执行顺序为: 第10行=》第2行=》第6行=》第7行。所以最早打印出2、3。紧接着打印a、b、c、d数值的时候a、b、d已经完成赋值。完成对象初始化之后,继续执行上面的静态代码,打印出1。等类已经完成了加载,执行main方法,打印出4

      

    双亲委派模型

      在java类加载器对Class进行加载的时候,如果两个类被不同的类加载器加载,则这两个类不相等。通过equals(),instanceof等方法判断结果为false。关于Java的类加载器,大概可以划分为以下几种:

    • 启动类加载器(Bootstrap ClassLoader):Bootstrap ClassLoader是唯一一个通过JVM内部的类加载器,通过C++实现。负责加载<JAVA_HOME>/lib中,或者被-Xbootclasspath参数所指定的路径中的,或者被虚拟机识别的类库记载到虚拟机内存中,仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib中也不会被加载。启动类加载器无法被Java程序直接使用,如果需要把加载器请求委派给引导类加载,则直接使用null代替即可。
    • 扩展类加载器(Extension ClassLoder):这个加载器负责加载<JAVA_HOME>libext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
    • 应用程序类加载器(Application ClassLoader):应用程序类加载器,负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自义定类加载,则该类加载就是默认的类加载器。

    我们的应用程序大部分是通过上面三种类加载器配合完成的,如果有特殊需求,还可以自定义自己的类加载器。包括类记载器在内,各种类加载器的关系可以这样表示。

      上述关系图称为双亲委派模型,除了Bootstrap ClassLoad加载器,其他的类加载器都有自己的父类。当一个类加载器获取到一个类加载任务时,先将该类丢给父加载器加载, 如果父加载器不能加载则自己加载。这种加载方式就称之为双亲委派。如上图所示,自定义类加载器获取到一个加载任务,一层层往上丢,所以最先让启动类加载器加载,如果启动类加载器能加载,则启动类加载器加载,启动类加载器不能加载,则丢给扩展类加载器,如果扩展类加载器不能加载,则丢给应用类加载器,如果应用类加载器不能加载,才丢给自定义加载器加载。

      上述的加载方式看起来特别麻烦,但是却解决了一个很重要的问题。比如自定义类加载器获取到一个Java.lang.Object的任务,则让Bootstrap ClassLoader加载,否则如果用户自己定义了一个Java.lang.Object会跟rt.jar中的类产生冲突,通过双亲委派模型,则用户自己写的Object将永远不会被加载到。

       双亲委派模型是Java虚拟机推荐给开发者的类加载实现,并不是一个强制性约束。在一些情况下双亲委派模型是会被破坏的,比如为了加载JNDI提供者的代码,设计出来的线程上下文加载器。又比如OSGI环境下规则也不大一样。

  • 相关阅读:
    随机森林算法参数调优
    BAYES和朴素BAYES
    阿里云 金融接口 token PHP
    PHP mysql 按时间分组 表格table 跨度 rowspan
    MySql按周,按月,按日分组统计数据
    PHP 获取今日、昨日、本周、上周、本月的等等常用的起始时间戳和结束时间戳的时间处理类
    thinkphp5 tp5 会话控制 session 登录 退出 检查检验登录 判断是否应该跳转到上次url
    微信 模板消息
    php 腾讯 地图 api 计算 坐标 两点 距离 微信 网页 WebService API
    php添加http头禁止浏览器缓存
  • 原文地址:https://www.cnblogs.com/null-qige/p/10059345.html
Copyright © 2011-2022 走看看