前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第一章 类加载到卸载的全过程分析

第一章 类加载到卸载的全过程分析

作者头像
Fisherman渔夫
发布2020-02-19 11:31:34
1.3K0
发布2020-02-19 11:31:34
举报
文章被收录于专栏:渔夫渔夫

类加载到卸载的全过程分析

 在Java代码中,类型的加、连接与初始化过程都是在程序运行期间完成的。其中类型指我们定义的一个class、interface、enum,此时并未包含对象。这一点提供了更大的灵活性、增加了更多的可能性。每一个类都是由类加载器class loader 加载到内存当中的。

1. Java虚拟机的生命周期

 JVM虚拟机最最本质上是一个进程,所以JVM和普通的进程一样,都是有生命周期的。Java虚拟机和程序的生命周期,在如下几种情况下,Java虚拟机的将结束生命周期:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

 对于上述给出的第三点给出进一步详细的例子描述,因为其十分常见,比如说我们调用一个程序其会向上抛出异常,但是直至main方法我们还是采取向上抛出异常的异常处理机制,那么此时程序就会结束运行,这是一种很常见的JVM结束生命周期的方式。

示例代码:

System.exit():可以看出,在此方法之后所定义的方法并没能够被有效执行,因为此时虚拟机以及被关闭了。

代码语言:javascript
复制
class GfG
{
    public static void main(String[] args)
    {
        int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};

        for (int i = 0; i < arr.length; i++)
        {
            if (arr[i] >= 5)
            {
                System.out.println("exit...");

                // Terminate JVM
                System.exit(0);
            }
            else
                System.out.println("arr["+i+"] = " +
                        arr[i]);
        }
        System.out.println("End of Program");
    }
}

控制台输出:

代码语言:javascript
复制
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
exit...

2. 类的加载、连接与初始化的概括摘要

2.1 类的加载、连接、初始化的流程说明

  • 加载:查找并加载类的二进制数据
  • 连接:连接是一个比较复杂的分步步骤,具体可以分为以下三步:
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转换为直接引用(涉及变量和方法)
  • 初始化:为类的静态变量赋予正确的初始化值

静态变量:就是在Java代码中使用static修饰的值。

默认值:int类型的默认值为0,boolean类型的默认值为false, 引用的默认值为null。注意,如果我们使用static int a =10;,但是在连接阶段的准备阶段,a变量的值被赋值为0;

在这里插入图片描述
在这里插入图片描述

下图和上图表达的意思实际上是一样的:

在这里插入图片描述
在这里插入图片描述

2.2 符号引用与直接引用的区别

 号引用/直接引用之间的区别:如果想仔细了解这两个概念的区别,不妨查看R大对此的回答R大。如果简单点说,就是JVM在加载完二进制数据之后,并未完成类的内存分配问题,这样一来我们就不能通过内存偏移量来查找方法以及变量了。符号引用(以方法为例)是一个包含类信息、方法名、方法参数的字符串,例如:java/io/PrintStream.println:(Ljava/lang/String;)V,我们根据这个字符串就可以准确地找到相关类。但是,此实现方式速度上还是不够快,所以就出现了基于内存地址偏移量的直接引用:运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。

3.类的加载

备注:在IDEA中查询类有无被加载的方法,请移步第7.3小节。

 类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.class对象(其即是被我们俗称的类对象,JVM虚拟机规范并没有说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中),类对象用来封装类在方法区内的数据结构。JVM规范也没有指定从哪里来加载Class文件。

整个过程可以使用下图表示:

在这里插入图片描述
在这里插入图片描述

 类的加载的最终产品是位于内存中的Class对象(不是我们在Java代码中调用构造方法所产生的对象)。 Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内数据结构的接口。Class对象是整个反射的入口,它就好像是一面镜子一样,能够洞悉出类文件中的所有信息。

3.1 加载.clss文件的的源头

  1. 从本地系统中直接加载(位于本地磁盘额的类路径中被调用,这是我们最常见的一种调用方法)
  2. 通过网络下载.class文件
  3. 从zip,jar等归档文件中加载.class文件(第三包架包就是这样被我们调用的)
  4. 从专有的数据库中提取.class文件(比较少)
  5. 将Java源文件中动态便以为.class文件(web开发中常用)

注意:这些都是可能的源头,实际上在不同的开发环境下应当会有不同的源头,而不同的源头就要求我们使用不同的字节处理流,比如说文件流:FileInputStream

3.2 类加载器的分类以及自带加载器的概念

有两种类型的类加载器:

  1. Java虚拟机自带的类加载器
    • 根类加载器(Bootstrap)
    • 扩展类加载器(Extension)
    • 系统(应用)类加载器(System)
  2. 用户自定义的类加载器
    • 其一定是java.lang.ClassLoader抽象类(这个类本身就是提供给自定义加载器继承的)的子类
    • 用户可以定制的加载方式

默认情况下,我们自定义的类的加载器是系统类加载器

java虚拟机自带的几种加载器: (1) 根(Bootstrap)类加载器:该类加载器没有父加载器,他负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,他并没有继承java.lang.ClassLoader类。比如说java.lang.Object就是由根类加载器加载的。 (2)扩展(Extension)类加载器:它的父类加载器为根类加载器。他从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动有扩展类加载器加载。扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。 (3) 系统(System)类加载器:也称为应用加载器,他的父类加载器为扩展类加载器。他从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。他是用户自定义的类加载器的默认父加载器。系统类加载器是纯java类,是java.lang.ClassLoader子类。

在这里插入图片描述
在这里插入图片描述

 上图是类加载器的层次关系图。从表象上看这些加载器是一种继承关系,但是实际上是一种包含关系。比如说,系统类加载器加载一个类,首先会委托给扩展类加载器,后者又委托给根类加载器,如果根类加载器加载失败,那么就委托回扩展类加载器,如果还不行,那么就系统类加载器加载,最后还不行,则抛出异常。但是实际上系统类加载器包含了扩展类加载器,后者又包含了根类加载器。

上述类加载器父子(非继承中的父子关系)结构的代码证明:

代码语言:javascript
复制
public class MyTest13 {
    public static void main(String[] args) {
        /**
         * 默认情况下,系统类加载器是用户自定义加载器的双亲,典型情况下其是应用启动的类加载器。
         *
         */
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        System.out.println(classLoader);

        while (null != classLoader) {
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

输出:

代码语言:javascript
复制
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

注意:应用类加载器就是系统类加载器,启动类加载器被null表示,所以就证明了这个加载器的层次关系(但注意:这不是子类和父类关系)。

3.3 类加载和类初始化的关系

 类加载完成时类初始化的必要条件,但是类被加载了不意味着其一定会被初始化。类加载器并不需要等到某个类被“首次主动使用”时再加载它。JVM规范允许类加载器再预料某个类将要被使用时就预先加载它,如果再预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须再程序首次主动使用该类时才报告错误(LinkageError链接错误)。如果一个类一直没有被程序主动使用,那么类加载器就不会报告错误(即使我们已经将类文件预先加载了)。

3.4 获得类加载器的方法

小总结:获得ClassLoader的途径,有:

  1. 获得当前类的ClassLoader,clzz为类的类对象,而不是普通对象 clazz.getClassLoader();
  2. 获得当先线程上下文的ClassLoader Thread.currentThread().getContextClassLoader();
  3. 获得系统的ClassLoader ClassLoader.getSystemClassLoader();
  4. 获得调用者的ClassLoader DriverManager.getCallerClssLoader();

两个概念(了解即可,基于3.5小节中的双亲委托机制才有这两种叫法):

  1. 定义类加载器:若有一个类加载器能成功加载Test类(自己写的),那么这个类加载器被称为定义类加载器
  2. 初始类加载器:所有能够成功返回Class对象引用的类加载器(包括自定义类加载器)都被称为初始类加载器

下面我们给出一个使用根类加载器的一段代码:

代码语言:javascript
复制
public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz;
        clazz = Class.forName("java.lang.String");
        System.out.println(clazz.getClassLoader());

    }

}

上述方法在控制台输出:null,预示着这个String类由根类加载器加载。

每个类在被加载时都会对应一个类对象,而Class.forName()方法就是返回一个类对象。

getClassLoader()是Class对象(类对象)的方法,有以下性质:

  1. 返回类对象所对应的类或接口的加载器,如果加载器是根加载器可能会返回null,也可能不会,主要看相关类的实现(返回null所用的加载器就是根加载器);
  2. 如果类对象对应的是基本类型以及void,那么一定返回null;

第二个代码块:

代码语言:javascript
复制
 public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz;
        clazz = Class.forName("classloader.C");
        System.out.println(clazz.getClassLoader());

    }

}
class C{
}

控制台输出:

代码语言:javascript
复制
sun.misc.Launcher$AppClassLoader@18b4aac2

 可见这是一个引用类加载器完成的类加载(其为Launcher类的内部类,且Launcher类为反编译出来的类)。

3.5 类加载器的父亲(双亲)委托机制

 类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用了父类委托机制,这种机制能够更好第保证Java平台的安全性。在此委托机制中,除了Java虚拟机自带的根加载器之外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载loader1加载Sample类时,loader1首先委托父加载器区加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

 在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。下面举一个例子:

在这里插入图片描述
在这里插入图片描述

备注:我们自定义的加载器当然也可以有所谓的父子关系:

 我们想通过loader1加载器来加载Sample类,但是实际上loader1不会直接去进行加载Sample类的相关操作,而是交给系统类加载器来加载,而后者又交给扩展类加载器来完成,接着又交给了跟类加载器来尝试加载。但是实际上根类加载器并不能我们自定义的Sample类,于是加载失败,就委托给扩展类加载器加载。而扩展类加载器也会失败(这两个加载器加载Sample类失败的原因待会儿会说)。实际上一般最终会由系统类加载器来加载Sample类,而loader1并没有加载Sample类。

小节

 除了根类加载器,每个加载器被委托加载任务时,总是第一时间选择让其父类加载器来执行相关加载操作,最终总是会让根类加载器来尝试加载,如果加载失败,则再次依次返回加载,只要这个过程有一个加载器加载成功,那么这个加载任务就会执行完成(这是Oracle公司的Hotpot虚拟机默认执行的类加载机制,并且大部分虚拟机都是如此执行的),整个过程如下图所示:

在这里插入图片描述
在这里插入图片描述

上图中所含的系统自带类加载的概念可以参看3.2小节。

3.6 自定义类加载器的代码实现

首先介绍自定义类的应用场景:

(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

代码语言:javascript
复制
package classloader;

import java.io.*;

/**
 * @author Fisherman
 * @date 2019/10/19
 */
public class MyTest16 extends ClassLoader {

    private String classLoaderName; //定义一个类加载器的名字,既然这里是类加载器类,那么这个名字就取决于对象的设计好了

    private final String fileExtension = ".class";//需要用此来进行本地文件的后缀定位

    public MyTest16(String classLoaderName) {
        super(); //默认无参构造器会将系统类加载器当作该类加载器的父加载器(参看此源代码既能理解这个含义)
        this.classLoaderName = classLoaderName;//得到自定义加载器的名字
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);//将传入的指定类加载器当作该类加载器的父加载器
        this.classLoaderName = classLoaderName;//得到自定义加载器的名字
    }

    /**
     * 重写toString方法
     *
     * @return 标准格式的返回类加载器对象的名字字符串
     */
    @Override
    public String toString() {
        return "[" + this.classLoaderName;
    }

    /**
     * 如果你仔细查看类加载过程的需要完成工作的话,你会知道这个过程需要进行类.class文件的查找
     * loadClassData 方法:这是自定义的方法,用于返回对应类名的字节数组
     * defineClass方法:通过字节数字以及类名(与系统无关)返回一个类对象,此方法是由JDK实现的,是一个本地方法
     *
     * @param className
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {

        byte[] data = this.loadClassData(className);

        return defineClass(className, data, 0, data.length);

    }
    /**
     *
     * @param name 输入的本地硬盘上的类名
     * @return 对应类名的.class文件的字节数组
     */
    private byte[] loadClassData(String name) {

        byte[] data = null;
        byte[] temp = new byte[128];//设置中间操作字节数组
        /**
         * 这里只是相当于将类类加载器的二进制名字转为Windows系统文件路径,
         * 如果是MacOS系统将"\\"替换为“.”即可,并将其赋值给className字符串变量
         */

        this.classLoaderName = this.classLoaderName.replace(".", "\\");
        /**
         * 下面则是使用try-with-resources语句块来进行读取.class文件于字节数组中
         * 只有读语句块使用了缓冲流,因为只有其涉及了硬盘读写操作
         */
        try (InputStream is = new FileInputStream(name + this.fileExtension);
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             BufferedInputStream bis = new BufferedInputStream(is);) {
             int EffectiveLength = 0;

            while (-1 != (EffectiveLength = bis.read(temp,0,temp.length))) {
                baos.write(temp,0, EffectiveLength);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return data;
    }


    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16 loader = new MyTest16("loader1");//得到类加载器对象
        test(loader);
    }

    /**
     * 下面是一个测试方法,用于得到由类加载生成的普通对象,并在控制台上打出其类信息
     * loadClass方法会调用我们MyTest14所实现的方法findClass方法
     *
     * 输出:classloader.MyTest1@1b6d3586
     * 其中classloader为我给此类的包名而已
     * @param classLoader
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class<?> clazz = classLoader.loadClass("classloader.MyTest1");
        Object object = clazz.newInstance();
        System.out.println(object);
    }
}

4. 类的连接阶段

类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

4.1 类的验证阶段

类的验证中的重要内容:

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性的验证

4.2 类的准备阶段

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如以下代码:

代码语言:javascript
复制
public class Sample{
    
   private static int a =1;
   public static long b;
   public static String str;
    
   static{
       b=2;
       str="hello world"
   }
}

 在此例的准备阶段中,我们会把上述int类型的静态变量 a 分配4个字节(32位)的内存空间,并赋值为默认值0;为long类的静态变量 b 分配8个字节(64位)的内存空间,并默认赋值为0;为String类型的静态变量 str 默认赋值为null。

4.3 类的解析阶段

在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。列如:如在Worker类的gotoWork()方法钟会引用Car类的run()方法。

代码语言:javascript
复制
public void gotoWork(){
   car.run();//这段代码在worker类的二进制数据中表示为符号引用
}

 在Worker类的二进制数据中,包含了一个父Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的位置,这个指针就是一个直接引用。

5. 类的初始化:给静态变量赋予正确的值

5.1 初始化的工作以及内部执行逻辑

  1. 声明类变量是指定初始值
  2. 使用静态代码块为类变量指定初始值

 可见静态变量的声明语句,以及静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。

JVM对于类初始化的判断执行逻辑:

  1. 是否完成前置步骤:假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 父类是否初始化:假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 类中有无初始化语句:假如类中有初始化语句,则系统依次执行这些初始化语句

 所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化它们(要求有俩:1.首次 2.主动使用,而被动使用不会触发类的初始化),关于主动使用的概念请看第7小节。、

5.2 final修饰的静态类变量的初始化(反汇编:javap -c)

final关键字修饰静态变量:

代码语言:javascript
复制
public class MyTeest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

class MyParent2{
    public static final String str ="hello world";

    static{
        System.out.println("MyParent2 static block");
    }

}

控制台输出:

代码语言:javascript
复制
hello world

如果将final修饰符去掉,那么控制台输出:

代码语言:javascript
复制
MyParent2 static block
hello world

 可以看到,仅仅一个final修饰就可以产生如此巨大的区别,那么究其原因是为何呢?

final修饰的变量在Java代码的编译阶段就会被存入到调用这个变量方法所在的类的常量池当中,就这个例子来说,字符串"hello world"作为一个常量就会被存入在MyTest2类所在的常量池当中,本质上,调用方法的类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。注意:这里指的是将常量存放到了调用类MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了,甚至,我们可以将MyParent2的class文件删除!我们使用反编译的操作来验证此说法:

在windows系统中,在IDEA编译器自带的Terminal中输入:

代码语言:javascript
复制
cd build\classes\java\main\classloader
dir
javap -c MyTest2.class

dir:此指令只是单纯输出此文件夹下有多少文档,这也可以给我们提供是否进入了正确的文件夹下的判断依据。

在MacOS系统中只需要更改前两句指令,\换成/以及dir换成ls

代码语言:javascript
复制
cd build/classes/java/main/classloader
ls

然后Terminal会输出:

代码语言:javascript
复制
Compiled from "MyTest2.java"
public class classloader.MyTest2 {
  public classloader.MyTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String hello world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

 首先,第一个被调用的方法是类:MyTest2的构造方法,这个不是我们此次学习关注的重点。接下来就是main方法的分析:

  1. getstatic:其对应着main方法中调用的System.out静态方法,返回一个PrintStream对象(之前也说了此JVM助记符代表的就是访问静态的成员变量);
  2. ldc:其后面已经跟着Sting hello world 了,代表的就是此时MyParent2.str已经成为一个固定的hello world了;

 助记符ldc的含义:表示将int,float或者是String类型的常量值从常量池中推送至栈顶,所谓栈顶就是结下的代码马上就要用的代码。如果类型为short,那么则需要bipush了,其表示将单字节(-128-127)的常量推送至栈顶,反编译结果如下所示:

代码语言:javascript
复制
public class classloader.MyTest2 {
  public classloader.MyTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        100
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       8: return
}

sipush则是表示将一个短整型常量值(-32768-32767)推送至栈顶。

inconst_1表示将int类型1推送至栈顶(-1,0,2,3,4,5都类似于此,比如inconst_3就是将整型数组3推入栈顶,但是6及以上则是使用sipush,再大点,就使用ldc了)

代码语言:javascript
复制
 3: iconst_5
 
 3: bipush        6

 3: sipush        32767

 3: ldc           #4                  // int 32768

再举一个例子,代码如下:

代码语言:javascript
复制
public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}


class MyParent3 {

    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("MyParent3 static code.");
    }

}

控制台输出:当然,每次运行的UUID都是一个不同大小的值。

代码语言:javascript
复制
MyParent3 static code.
7c380593-cfcf-466b-8aa0-2ca483b59cb3

 同样是final修饰的静态变量,这里就需要MyParent3类进行初始化操作了,可见final并未有关键的作用,而更为关键的作用是这个静态不可变变量在编译阶段能够确定取值,如果不能,则需要进行类的初始化,如果可以进行确定,那么就不需要进行类的初始化。

总结一下:当一个常量的值并非编译期间可以确定,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,自然就会导致这个类会进行初始化。

5.3 只有在第一次主动使用类时才导致类的初始化

 下面的代码块则是为了验证只有在第一次主动使用类的时候,才会进行初始化操作:

代码语言:javascript
复制
public class MyTest4 {
    public static void main(String[] args) {
        MyParent4 myParent4 = new MyParent4();
        System.out.println("第二次创建对象");
        MyParent4 myParent4_2 = new MyParent4();
    }

}


class MyParent4{
    static{
        System.out.println("MyParent4 static block");
    }
}

控制台输出:

代码语言:javascript
复制
MyParent4 static block
第二次创建对象

 虽然我们调用了MyParent4类的构造方法两次,对应于主动调用该类两次,但是只有第一次主动使用类的时候才进行类的初始化操作。

5.4 原始类型以及引用类型数组的初始化

数组例子:

代码语言:javascript
复制
public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[1];

        System.out.println(myParent4s.getClass());

        MyParent4[][] myParent4s1 = new MyParent4[1][1];
        System.out.println(myParent4s1.getClass());

        System.out.println(myParent4s.getClass().getSuperclass());
        System.out.println(myParent4s1.getClass().getSuperclass());

    }

}

class MyParent4 {
    static {
        System.out.println("MyParent4 static block");
    }
}

控制台输出:

代码语言:javascript
复制
class [Lclassloader.MyParent4;
class [[Lclassloader.MyParent4;
class java.lang.Object
class java.lang.Object

 对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示为:class [Lclassloader.MyParent4;这种以方括号加大写L形式。动态生成的类型,其父类就是Object类。正因为这样动态生成类型的特性所以创建数组实例并不属于主动地使用类,所以静态代码块没有得到执行。

 对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型。

 但是上面的数组是引用类型的数组,但是对于原生类型的数组又是如何呢?

代码语言:javascript
复制
int[] arr = new int[10];
System.out.println(arr.getClass());
System.out.println(arr.getClass().getSuperclass());

控制台输出:

代码语言:javascript
复制
class [I
class java.lang.Object

 此时类型是[I,父类还是Object,类似的有char类型的数组,其class类型用:[C来表示。

我们对整个程序反编译一下,主要还是看看助记符:

代码语言:javascript
复制
Compiled from "MyTest4.java"
public class classloader.MyTest4 {
  public classloader.MyTest4();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: anewarray     #2                  // class classloader/MyParent4
       4: astore_1
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: aload_1
       9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      15: iconst_1
      16: iconst_1
      17: multianewarray #6,  2             // class "[[Lclassloader/MyParent4;"
      21: astore_2
      22: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      29: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      32: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      35: aload_1
      36: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      39: invokevirtual #7                  // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
      42: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      45: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      48: aload_2
      49: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      52: invokevirtual #7                  // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
      55: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      58: bipush        10
      60: newarray       int
      62: astore_3
      63: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      66: aload_3
      67: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      70: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      73: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      76: aload_3
      77: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      80: invokevirtual #7                  // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
      83: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      86: return
}

助记符:

anewarray:表示创建一个引用类型的(如类、接口、数组)数组,

newarray:表示创建一个原始类型(int、float、char等)的数组,并将其压入栈顶

6. 类的卸载

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  2. 加载该类的ClassLoader已经被GC。
  3. 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法

7. 类的主动/被动使用

7.1 主/被动使用的概念

Java程序对类的使用方式可以分为两种:

  • 主动使用
  • 被动使用

主动使用类的七种方式,即类的初始化时机:

  1. 创建类的实例;
  2. 访问某个类或接口的静态变量(无重写的变量继承,变量其属于父类,而不属于子类),或者对该静态变量赋值(静态的read/write操作);
  3. 调用类的静态方法;
  4. 反射(如:Class.forName("com.test.Test"));
  5. 初始化一个类的子类(Chlidren 继承了Parent类,如果仅仅初始化一个Children类,那么Parent类也是被主动使用了);
  6. Java虚拟机启动时被标明为启动类的类(换句话说就是包含main方法的那个类,而且本身main方法就是static的);
  7. JDK1.7开始提供的动态语言的支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_public,REF_invokeStatic句柄对应的类没有初始化,则初始化;

 除了上述所讲七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,比如:调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

注意:初始化单单是上述类加载、连接、初始化过程中的第三步,被动使用并不会规定前面两个步骤被使用与否,也就是说即使被动使用只是不会引起类的初始化,但是完全可以进行类的加载以及连接。例如:调用ClassLoader类的loadClass方法加载一个类,这并不是对类的主动使用,不会导致类的初始化。

 需要铭记于心的一点:只有当程序访问的静态变量或静态变量确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用,通过子类调用继承过来的静态变量算作父类的主动使用。

class文件的助记符(此处先列具一些简单且常见的助记符):

  1. getstatic:读取类的静态变量
  2. putstatic:写类的静态变量
  3. invokestatic:调用类的静态方法

7.2 主/被动使用的案例分析

类的主动使用和被动使用的区别案例:

代码语言:javascript
复制
public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str);
    }

}


class MyParent1 {
    public static String str = "hello world";

    static {
        System.out.println("MyParent static block.");
    }
}

class MyChild1 extends MyParent1 {
    static {
        System.out.println("MyChild1 static block.");
    }
}

控制台输出:

代码语言:javascript
复制
MyParent static block.
hello world

 我们可以发现,子类MyChild1中的静态代码块并没有得到执行,这是为什么呢?

 我相信看本文的童鞋即时没学过JVM,也知道JAVA中静态域中若涉及继承,那么父类静态域加载早于子类静态域的加载,那么这也至少说明了一点,子类的静态域也是会得到执行的,那么为何在此例中,子类的静态域没有得到执行呢(况且是在mian方法中通过子类来进行静态域的调用的)?实际上回答很简单,就是我们是被动地使用了子类,对于静态字段来说只有直接定义了该字段的类才会被初始化,当一个类在初始化时,要求其父类全部都已经初始化完毕了。

 如果在子类中重写了父类的str变量,将上述代码中的相关语句修改为以下语句:

public static String str = "hello world---------by child";

那么此时就会初始化子类,控制台则会输出

代码语言:javascript
复制
MyParent static block.
MyChild1 static block.
hello world-----------by child

当然还有很多方法使子类主动地被使用,比如构造子类对象。

7.3 在IDEA中查询类有无被加载的方法

 现在我们知道了在这个例子中如果被动地使用子类,那么子类就不会进行初始化,那么问题来了:子类有被加载和连接吗?实际上这个JVM规范对此事没有相关规定的,所以我们需要使用虚拟机的运行选项来显示是否有此类的加载:

-XX:+TraceClassLoading:用于追踪类的加载信息并打印出来,在IntelliJ IDEA中使用此虚拟机参数,那么只需修改运行时参数即可,软件点击的顺序大概如此:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

 注意,如果打错了VM options,那么虚拟机则会运行失败,程序自然也无法运行,并且,编译器会告诉你:你可能所需的VM optional参数值。

 再次运行程序,可以控制台输出了许许多多的关于类加载的信息,如下所示,其顺序就是JVM真实加载这些类的顺序:其他的你可能不知道,至少我们发现JVM加载的第一个类为java.lang.Object,因为其是所有类的父类。

在这里插入图片描述
在这里插入图片描述

 我们在控制台输出行向上找找,可以发现我们感兴趣的类加载过程:

在这里插入图片描述
在这里插入图片描述

 可以看到实际上虽然MyChlid1作为一个子类是被动使用,没有参与初始化,但还是有被加载。特别提一点,有main方法的类都是主动使用,所以可以看到第一个红框中显示了有main方法的MyTest1类的加载。

下面简单提一下JVM参数设置的三种方式(实际运行时无须加入<>,这里只是为了显示方便):

-XX:+<option>:表示开启option选项

-XX:-<option>:表示关闭option选项

-XX:<option>=<value>:表示将option选项的值设置为value

反射是对类的主动使用的例子:

代码语言:javascript
复制
public class TempTest {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> testReflection = Class.forName("classloader.TestReflection");
    }
}


class TestReflection {

    static {
        System.out.println("类:" + TestReflection.class + "被初始化了!");
    }

}

控制台输出:

代码语言:javascript
复制
类:class classloader.TestReflection被初始化了!

10. 类的初始化过程中顺序问题

直接上代码吧:

代码语言:javascript
复制
public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(Singleton.counter1);
        System.out.println(Singleton.counter2);
    }

}

class Singleton {
    public static int counter1;
    public static int counter2 = 0;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

 这就是一个单例模式,由于变量都被修饰为static静态,为类变量,所以再调用了构造方法后,两个数都被加1了,所以控制台输出值为:

代码语言:javascript
复制
1
1

 你自然会认为这是理所当然的,的确也是,毕竟代码是按照我们平时写的顺序来的,那么如果做出以下改变呢?即把public static int counter2 = 0;移动到私有的构造方法以下呢?即如下代码,结果还是这般吗?

代码语言:javascript
复制
public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(Singleton.counter1);
        System.out.println(Singleton.counter2);
    }

}

class Singleton {
    public static int counter1;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        counter1++;
        counter2++;
    }
    public static int counter2 = 0;

    public static Singleton getInstance() {
        return singleton;
    }
}

这里不卖关子了,直接说出答案。控制台输出:

代码语言:javascript
复制
1
0

 从表面上看,counter1的值成功加1,counter2的值却没有加1,保持了不变,但是事实真的如此吗?为了检验这个说明的正确性,我们在构造方法中加入:

代码语言:javascript
复制
System.out.println("为了验证counter2有无成功加1:"+counter2);

我们发现控制台输出了:

代码语言:javascript
复制
为了验证counter2有无成功加1:1
1
0

现在可以说了,counter2是曾被成功加1的,但是后面被以某种方式修该为1,那么究竟是如何做到的呢?

 实际上,这个问题用本文中JVM类对加载以及初始化整个过程掌握的好的话就能够轻松回答了,以下说一下JVM的类加载顺序和缘由:

  1. main方法中调用了Singleton类的静态方法:getInstance(),这属于类的主动使用之一,所以触发了JVM对Singleton类的初始化操作;
  2. 初始化的前提是类有被加载,而加载过程中有准备步骤,即:为类的静态变量分配内存,并将其初始化为默认值,在这一步中我们按照顺序有:counter1被赋值为0,私有引用变量singleton被赋值为null,counter2被赋值为0;
  3. 接着开始类的加载操作,类的加载操作目的就是给类的静态变量赋予正确的值,这里还是按JVM执行顺序说:
    1. counter1因为并没有指定值,所以值保持不变,还是为默认值0;
    2. 私有引用变量singleton通过私有构造器指定了值,所以调用私有构造器,在这里我们执行了:counter1++;以及counter2++;操作,所以在这counter1值更新为1,counter2的值被更新为1;
    3. counter2因为语句public static int counter2 = 0;指定了初始值0,所以这里counter2又被更新为0;

 所以类就这样被加载完成了,可以发现正是由于类静态变量准备阶段赋予默认值以及类加载过程中赋予正确初始值这两个过程的按代码编写的顺序执行这一特性,这才有上述所表现的反常理。

11. 类的实例化

类的实例化按序完成的作用:

  1. 为新的对象分配内存
  2. 实例变量赋予默认值
  3. 实例变量赋予正确的初始值

Java编译器在为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为<init>。针对源代码中每一个类的构造方法,Java编译器都产生一个<init>方法。

12. javap-c 反编译中的JVM助记符

根据栈的使用原则,我们可以知道:一个元素或者其值被推向栈顶,意味着其即将被使用,或者如果新加入栈的元素,其应当放在栈顶,这是遵循了后进先出的内存分配原则。

助记符

含义

ldc

将int/float/String类型的常量值从常量池中推送至栈顶

bipush

将单字节(-128 ~ 127)的常量值从常量池中推至栈顶

sipush

将一个短整型(-32768 ~ 32767)的常量值从常量池中推至栈顶

inconst_1

将int型的常量值1从常量池中推至栈顶(jvm专门为0/1/2/3/4/5这5个数字开的助记符),iconst_m1则表示的是-1

anwearray

创建一个引用类型(如类、接口、数组)的数组,并将其引用值推至栈顶

newarray

创建一个指定的原始类型(如int/float)的数组,并将其引用值推至栈顶

 上述说法中所涉及的基本数据类型的内存模型如下所示:

数据类型

表示范围

2进制表示范围

byte

-128 ~ 127

-2^7 ~ 2^7-1

short

-32768 ~ 32767

-2^15 ~ 2^15 -1

int

-2,147,483,648 ~ 2,147,483,647

-2^31 ~ 2^31 -1

 实际上就是8位计数、16位计数、32位计数方法分为三种不同的操作方法:bipushsipush以及ldc,实际上用大小为100的整型,用byte/short/int修饰最终都是使用bipush助记符,可见最终决定助记符的不是Java代码中对变量的修饰是用int/short…,而是实际需要多少位来进行数据的存储。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类加载到卸载的全过程分析
    • 1. Java虚拟机的生命周期
      • 2. 类的加载、连接与初始化的概括摘要
        • 2.1 类的加载、连接、初始化的流程说明
        • 2.2 符号引用与直接引用的区别
      • 3.类的加载
        • 3.1 加载.clss文件的的源头
        • 3.2 类加载器的分类以及自带加载器的概念
        • 3.3 类加载和类初始化的关系
        • 3.4 获得类加载器的方法
        • 3.5 类加载器的父亲(双亲)委托机制
        • 3.6 自定义类加载器的代码实现
      • 4. 类的连接阶段
        • 4.1 类的验证阶段
        • 4.2 类的准备阶段
        • 4.3 类的解析阶段
      • 5. 类的初始化:给静态变量赋予正确的值
        • 5.1 初始化的工作以及内部执行逻辑
        • 5.2 final修饰的静态类变量的初始化(反汇编:javap -c)
        • 5.3 只有在第一次主动使用类时才导致类的初始化
        • 5.4 原始类型以及引用类型数组的初始化
      • 6. 类的卸载
        • 7. 类的主动/被动使用
          • 7.1 主/被动使用的概念
          • 7.2 主/被动使用的案例分析
          • 7.3 在IDEA中查询类有无被加载的方法
        • 10. 类的初始化过程中顺序问题
          • 11. 类的实例化
            • 12. javap-c 反编译中的JVM助记符
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档