JVM解析<一> 字节码文件和类加载

83 阅读8分钟

一、JVM架构

二、字节码文件

2.1前端编译器

前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM探范的字节码文件。javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。

2.1.1 javac(IDEA默认使用的)

javac是一种能够将Java源码编译为字节码的前端编译器

2.1.2 ECJ编译器

在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的EC(Eclipse Compiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。

2.3Class的对象

(1) class:
外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
( 2interface:接口
(3)[]:数组
(4) enum:枚举
(5annotation:注解@interface 
(6) primitive type:基本数据类型
( 7) void

i++的过程

bipush 1010放入操作数栈
istore_1 将栈中值放入局部变量表下标为1的位置
iload_1 将局部变量表中的值放入栈中
iinc 1 by 1 将局部变量表中的值加1
istore_1 将栈中的值放入局部变量表中

	static void test2(){
        Integer a = 128;
        Integer b = 128;
        log.info("输出的结果为{}",a == b);//false -128~127都会存入缓存中
    }

public void test3(){
        Integer a = 4;
        int b = 4;
        log.info("是否相等{}",a == b );//true Integer会自动进行拆箱(能拆不装)
    }

2.4class文件的组成部分(16进制)

进制在线转换
https://www.sojson.com/hexconvert.html

魔数
Class文件版本
常量池
访问标识(或标志)
类索引,父类索引,接口索引集合
字段表集合
方法表集合
属性表集合

常量池

常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量:1.文本字符串 2.声明为final的常量值
符号引用: 1.类和接口的全限定名 2.字段的名称和描述符 3.方法的名称和描述符

常量类型和结构

常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。如下表格所示:

字节码指令

https://blog.csdn.net/qq_33521184/article/details/105622903?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163939550716780274184839%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163939550716780274184839&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-105622903.first_rank_v2_pc_rank_v29&utm_term=java%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4&spm=1018.2226.3001.4187

java数据类型

三、类加载器

3.1类的加载过程

3.1.1 Loading(装载)阶段

将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
步骤:
1.通过类的全名,获取类的二进制数据流。
2.解析类的二进制数据流为方法区内的数据结构(Java类模型)
3.创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

类模型的位置
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后元空间)。

Class实例的位置
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个class类型的对象。

3.1.2 Linking(连接)阶段 ①验证

保证加载的字节码是合法、合理并符合规范的

②准备

为类的静态变量分配内存,并将其初始化为默认值。
需要注意的点:
1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
2.注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
3.在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

③解析

将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或"偏移量。如果直接引用存在,那么可以肯定系统中存在该类、方法或者段。但只存在夺号引用,不能确定系统中一定存在该结构。

3.1.3 Initialization(初始化)阶段

为类的静态变量赋予正确的初始值,以及加载静态代码块。执行类的初始化方法:<clinit>()方法

<clinit>():只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
<init>():一定会出现在Class的method表中。

不会有()的类

public class Test {
    //场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public int num = 1;
    //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法 
    public static int num1;
    //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public static final int num2 = 1;
}

()产生死锁的问题

class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.atguigu.java.StaticB");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticA init OK");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.atguigu.java.StaticA");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticB init OK");
    }
}

public class StaticDeadLockMain extends Thread {
    private char flag;

    public StaticDeadLockMain(char flag) {
        this.flag = flag;
        this.setName("Thread" + flag);
    }

    @Override
    public void run() {
        try {
            Class.forName("com.atguigu.java.Static" + flag);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " over");
    }

    public static void main(String[] args) throws InterruptedException {
        StaticDeadLockMain loadA = new StaticDeadLockMain('A');
        loadA.start();
        StaticDeadLockMain loadB = new StaticDeadLockMain('B');
        loadB.start();
    }
}

3.2类的加载器

类的加载器只在loading阶段,只能影响到类加载的第一个阶段
显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用class.forName(name)或this.getClass().getClassLoader( ).loadClass()加载class对象。
隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

3.2.1启动类加载器(引导类加载器)

1.这个类加载使用C/C++语言实现的,嵌套在VM内部。
2.它用来加载]ava的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供VM自身需要的类。
3.并不继承自java.lang.ClassLoader,没有父加载器。
4.出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
5.加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

使用以下参数配置会打印被加载的类()
-XX:+TraceClassLoading

3.2.2拓展类加载器

1.Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。继承于classLoader类
2.父类加载器为启动类加载器
3.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

3.2.3系统类加载器

1.java语言编写,由sun.misc.Launcher$AppClassLoader实现继承于ClassLoader类
2.父类加载器为扩展类加载器
3.它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库应用程序中的类加载器默认是系统类加载器。
4.它是用户自定义类加载器的默认父加载器
5.通过classLoader的getSystemClassLoader()方法可以获取到该类加载器

3.2.4自定义类加载器

1.通过类加载器可以实现非常绝妙的插件机制,如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
2.自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器
3.所有用户自定义类加载器通常需要继承于抽象类java.lang.classLoader。

3.3ClassLoader源码

ClassLoader

protected Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //查看当前类是否有被加载过  First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {//给引导类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class. 如果没有类加载器加载,就自己加载当前类
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {//是否需要解析,默认为false
                resolveClass(c);
            }
            return c;
        }
    }

保护机制,即使我们重写了classLoader破坏双亲委派机制,引导类加载器也会加载java下的类

  /* Determine protection domain, and check that:
        - not define java.* class,
        - signer of this class matches signers for the rest of the classes in
          package.
    */
    private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) checkCerts(name, pd.getCodeSource());

        return pd;
    }

3.5自定义类加载器

3.5.1自定义类加载器的用途

1.隔离加载类
2.修改类加载的方式
3.扩展加载源
4.防止源码泄漏