JVM之类加载

 


C/C++:代码 --编译--> 机器码,而不同平台(操作系统和指令集)的机器码是不一样的,所以就不能跨平台

Java:代码 --javac 编译--> 字节码(*.class) --> Java虚拟机

占用空间 描述 实际存储
4byte 魔数 0xCAFEBABE
4byte 版本号 次版本号/主版本号
2byte 常量池的个数 0x0016
......

常量池:

  1. 字面量:字符串、声明为final的常量
  2. 符号引用:包名、类和接口的全限定名、字段和方法的名称和描述符等等

Javac编译时没有像C/C++那样“连接”的操作,即不会保存各个方法、字段最终在内存中的布局信息,如果没有经过JVM在虚拟机在运行期转换(动态链接)的话是无法得到真正的内存入口地址

  1. 编译,本机(win mac),java -> class,进一步打包成jar或者war
  2. 运行,Linux

一个字节码文件从磁盘加载到虚拟机内存中开始,到卸载出内存为止,会经历:

  1. 加载
  2. 连接
    • 验证
    • 准备
    • 解析
  3. 初始化
  4. 使用
  5. 卸载

加载需要做哪些工作?

  1. 将字节码文件读到内存
  2. 校验文件的合法性
  3. 想要创建对象,需要初始化,做必要的准备
  4. 我们写的类可能依赖很多的类,所以需要将必要的类链接进来

类加载时机

  1. 使用new实例化对象
  2. 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
  3. 调用一个类型的静态方法的时候
  4. 对类型进行反射调用,如:Class.forName("xxx")
  5. 初始化类时,父类还没有初始化,则父类初始化
  6. 使用类对象,xxx.class

加载

加载不是创建对象,而是class文件读到JVM的内存中,方法区

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
    • jar、war
    • 网络
    • 运行时计算生成,动态代理
    • 加密文件解密,防止反编译
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存(堆内存)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

方法区中就有了这个类的信息,可以通过这个来创建堆区的对象

双亲委派

类加载器:

  1. 启动类(引导类)加载器(C++实现),Bootstrap,加载Java的核心库,JAVA_HOME/jre/lib
  2. 扩展类加载器,Extension,JAVA_HOME/jre/lib/ext
  3. 应用程序类加载器,Application,classpath
    • 系统类加载器
      ClassLoader sys = ClassLoader.getSystemClassLoader();
    • 用户自定义加载器
      什么时候会用?一般很少用到,Tomcat里面有使用

2和3都是派生于ClassLoader的

xxx.class.ClassLoader();  // 获取类的类加载器,即哪一个ClassLoader加载了这个类

My.class.ClassLoader();  // ClassLoader.getSystemClassLoader() sun.misc.Launcher$AppClassLoader
Boolean.class.ClassLoader();  // null

组合模式,将ext类加载器作为参数传递给app

为什么使用双亲委派?

  1. 安全,防止核心API被随意篡改
  2. 避免重复加载,父加载器已经加载过的话,子加载器就不用加载了,保证类加载的唯一性
  3. 能够实现动态加载、按需加载(没明白啥意思)

为什么使用打破双亲委派?

验证

确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

  1. 文件格式验证
    • 是否以魔数开头
    • 版本是否在JVM的接受范围
    • ...
  2. 元数据验证
    • 这个类是否有父类
    • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
    • ...
  3. 字节码验证,元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
  4. 最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在
    连接的第三阶段——解析阶段中发生
    • 符号引用中通过字符串描述的全限定名是否能找到对应的类

准备

准备阶段是为类变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

public static int value = 123;,准备阶段后value为0,如果是final的,那就是123

解析

解析阶段是Java虚拟机将常量池内的符号引用(字节码里面)替换为直接引用(Java内存模型里面)的过程

  1. 类或接口的解析
  2. 字段解析
  3. 方法解析
  4. 接口方法解析

初始化

这里的初始化是初始化类的信息,并不是创建一个对象

public class Main {
    static {
        a = 20;
    }
    public static int a = 10;
    public static void main(String[] args) {
        System.out.println(a); // 输出为10
    }
}
  1. 声明类变量是指定初始值,如:public static int value = 123;
  2. 使用静态代码块为类变量指定初始值

例子

只看概念还是很空,所以结合着例子一起看一下

首先定义两个类:

public class Father {

    // 这里设置成public是为了后续测试方便
    public static int fatherStaticVal = 1;
    public static final int fatherStaticFinalVal = 2;
    public int fatherNoStaticVal;

    static {
        System.out.println("father static code =========>");
        System.out.println("fatherStaticVal = " + fatherStaticVal);
        System.out.println("fatherStaticFinalVal = " + fatherStaticFinalVal);
    }

    Father() {
        System.out.println("father init =========>");
        System.out.println("fatherStaticVal = " + fatherStaticVal);
    }

}

public class Child extends Father {

    public static int childStaticVal = 1;
    public static final int childStaticFinalVal = 2;

    static {
        System.out.println("child static code =========>");
        System.out.println("childStaticVal = " + childStaticVal);
        System.out.println("childStaticFinalVal = " + childStaticFinalVal);
    }

    Child() {
        System.out.println("child init =========>");
        System.out.println("childStaticVal = " + childStaticVal);
    }

}

测试代码:

public class ClassLoaderTest {

    public static void main(String[] args) {

        // ========================================================================================
        // 测试0
        // System.out.println(Father.fatherNoStaticVal);  // Error

        // ========================================================================================
        // 测试1
        // System.out.println(Father.fatherStaticVal);
        /*

         输出为:
           father static code =========>
           fatherStaticVal = 1
           fatherStaticFinalVal = 2
           1

         解释:
           这里触发类加载时上面的第2条:读取或设置一个类型的静态字段
           大致流程:
             准备:
               fatherStaticVal = 0
               fatherStaticFinalVal = 2
             (忽略解析)
             初始化:
               fatherStaticVal = 1
               执行静态代码块
             并没有执行构造方法

         */

        // ========================================================================================
        // 测试2
        // System.out.println(Father.fatherStaticVal);
        /*

         输出为:
           2

         解释:
           发现没有触发类加载,这是因为如果使用其他类的static final类型的变量(且为基本数据类型)时
           会直接将该常量放入到本类中,如果常量的范围为:-32768~32767,直接使用sipush指令将常量压入栈中
           如果超过这个范围就将该变量放到Class文件的常量池中,并通过ldc指令加载

         */

        // ========================================================================================
        // 测试3
        // System.out.println(Child.childStaticVal);
        /*

         输出为:
           father static code =========>
           fatherStaticVal = 1
           fatherStaticFinalVal = 2
           child static code =========>
           childStaticVal = 1
           childStaticFinalVal = 2
           1

         解释:
           这里触发类加载时上面的第2条:读取或设置一个类型的静态字段
           以及第5条:初始化类时,父类还没有初始化,则父类初始化
           大致流程:
             父类准备:
               fatherStaticVal = 0
               fatherStaticFinalVal = 2
             (忽略解析)
             父类初始化:
               fatherStaticVal = 1
               执行静态代码块
             子类准备:
               childStaticVal = 0
               childStaticFinalVal = 2
             (忽略解析)
             子类初始化:
               childStaticVal = 1
               执行静态代码块

         */

        // ========================================================================================
        // 测试4
        // Child child = new Child();
        /*

         输出为:
           father static code =========>
           fatherStaticVal = 1
           fatherStaticFinalVal = 2
           child static code =========>
           childStaticVal = 1
           childStaticFinalVal = 2
           father init =========>
           fatherStaticVal = 1
           child init =========>
           childStaticVal = 1

         解释:
           测试4前面其实和测试3是一样的,不过最后多了构造器的方法

         */

        // ========================================================================================
        // 总结:
        /*

         流程:
         1. 准备:赋默认值,如果是static final的则直接赋值
         2. 初始化:执行static变量的赋值,静态代码块中的内容,按照出现的顺序先后执行
         3. 构造函数

         */

    }

}

最后再来看一篇博客的例子【3】:
其实文中的最后结论已经解释的很明白了,不过我觉得可以再稍微改动一下代码的顺序,这样对这个问题的理解就更深刻了

class SingleTon {
    
    public static int count1;
    public static int count2 = 0;
    private static SingleTon singleTon = new SingleTon();
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

// 这样打印的结果就是:
// count1=1
// count2=1








说明

===================================

仅作为校招时的《个人笔记》,详细内容请看【参考】部分

===================================

参考

  1. 《深入理解Java虚拟机》
  2. B站,黑马满老师
  3. https://www.cnblogs.com/javaee6/p/3714716.html
posted @   optimjie  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示