JVM(1) 类加载 更新至p40


1 类加载ClassLoading

在Java中,类型(Class、interface、枚举等,不是对象)的加载、连接和初始化过程都是在程序运行期间(Runtime)完成的。更为灵活。(注意是运行期)

在这里插入图片描述

1.1 加载:

最终产品就是Class对象。

类的加载指的是将class文件中的二进制数据读入到内存,将其放入运行时数据区的方法区,然后在内存中创建一个java.lang.Class对象(HotSpot虚拟机将Class对象放在了方法区中,但是一般来说Class应该在堆区),这个Class对象用来封装类在方法区中的数据结构。

方法区: 各个线程共享,存储被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码 等数据。

将已经存在的class文件从磁盘加载到内存。

虚拟机参数 :

 -XX:+TraceClassLoading   用于追踪类的加载信息并打印
 -Xms512M -Xmx4096M   JVM初始内存,JVM最大可用内存

IDEA中这样添加参数:
在这里插入图片描述


1.2 连接

类与类之间的关系。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

1.2.1 验证

确保被加载的类的正确性。

1.2.2 准备

为类的静态变量分配内存,并将其初始化为默认值
例如:
实际上是先在连接阶段中的准备阶段赋值为0(默认值),再在初始化阶段赋值为1。

class Test {
	public static int a = 1;
}
1.2.3 解析

把类的符号引用转换为直接引用。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符类符号引用进行。如在A类中a方法中引用了B类中的b方法,将b方法放入A类中


1.3 初始化

为类的静态变量赋予正确的初始值。按照代码从上到下的顺序。
例如1.2.2小节。
当JVM初始化一个类时,要求他的所有父类都已经初始化了。但不适用于接口。如何理解:

  • 在初始化一个类时,并不会先去初始化他的实现的接口。
  • 在初始化一个接口时,并不会先去初始化他的父接口。

因此,一个父接口不会因为他的子接口或者实现类的初始化而初始化,只有程序调用特定接口的静态变量的时候,才会导致该接口的初始化。

1.3.1 主动使用/被动使用

Java程序对类的使用方式有两种:

  • 主动使用
  • 被动使用

所有的jvm实现必须在每个类或者接口被Java程序首次主动使用时才初始化他们。是否初始化不影响是否被加载和连接
实例参考:Java 中对类的主动引用和被动引用

被动调用的一个有意思的点:
final常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类(被调用类),因此不会触发定义常量的类的初始化。甚至可以在编译之后删除被调用类的class文件。
但是: 如果如下,则被调用类还是会初始化:

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

因为无法在编译期就确定str的值,也就不能把str放入到常量池。

1.3.2 一个例子有助理解
public class Test{
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1:" + singleton.counter1);
        System.out.println("counter2:" + singleton.counter2);
    }
}

class Singleton {
    public static int counter1;
    public static int counter2 = 0; // here
    private static Singleton singleton = new Singleton();
    
    private Singleton(){
        counter1++;
        counter2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

会输出:
在这里插入图片描述
但是,如果把public static int counter2 = 0;换个位置,如下所示,输出什么?

public class MaximumSubarray53 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1:" + singleton.counter1);
        System.out.println("counter2:" + singleton.counter2);
    }
}

class Singleton {
    public static int counter1;
    
    private static Singleton singleton = new Singleton();

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

    public static int counter2 = 0; // here
    
    public static Singleton getInstance(){
        return singleton;
    }
}

在这里插入图片描述
准备阶段赋给默认值0或者null。
在初始化阶段会从上到下依次执行代码,新的值覆盖掉了默认值。
参考: B站视频

1.4 还有类的使用、卸载

卸载:当类A被加载连接初始化后,生命周期开始。当代表类A的Class对象不再被引用(即不可触及)时,Class对象就会结束生命周期,类A在方法区的数据也会被卸载,从而结束类A的生命周期。

  • 一个类何时结束生命周期,取决于代表他的Class对象。
  • 由JVM自带的类加载器所加载的类,在JVM的生命周期中不会被卸载。
  • 因为jvm本身会始终引用自带的加载器,自带的加载器会始终引用其所加载过的类。
  • 由用户自定义的类加载器加载的类是可以被卸载的.

1.5 类的加载顺序

静态初始化块 > 初始化块 > 构造器
父类 > 子类
综合下来顺序就是:

父类静态初始化块
子类静态初始化块
父类初始化块
父类构造器
子类初始化块
子类构造器

需要注意静态初始化块是在类第一次加载的时候就会进行初始化。


2 类加载器ClassLoader

2.1 JVM结束生命的几种情况

  • 执行了System.exit()方法
  • 程序正常运行结束
  • 程序执行中遇到了异常或者错误而异常终止(抛到了main方法,main方法又抛给了jvm)。换言之,程序执行过程中遇到未捕获的异常或者错误而结束.
  • 操作系统出现错误而导致Java虚拟机终止

2.2 两种类加载器

参考 类加载器工作原理
在这里插入图片描述
看起来是继承,实际上是“包含”,下面包含了上面

2.2.1 jvm自带的
  • 根类加载器(Bootstrap):也叫启动加载器。没有父类,加载JVM的核心类库。启动类加载器不是Java写的,而是特定与平台的及其指令。
  • 扩展类加载器(Extension):父加载器是根类加载器。(只能从jar包中加载class文件)
  • 系统(应用)类加载器(System):也称为应用类加载器。他的父加载器是扩展类加载器。
  • 除了启动类加载器之外,所有加载器都被实现为Java类。不过总归要有一个组件来加载第一个Java实现的加载器,这就是启动类加载器的职责。
    查看具体的路径:
System.getProperty("sun.boot.class.path");
System.getProperty("java.ext.dirs");
System.getProperty("java.class.path");
2.2.2 用户自定义
  • 必须是 java.lang.ClassLoader 的子类, java.lang.ClassLoader 是抽象类
  • 可以定制类的加载方式

2.3 类的加载不需要“首次主动使用”才加载

只有“初始化”才需要首次主动使用。
JVM允许类加载器在预料某个类将要被使用时就预先加载它。
如果预先加载的过程中找不到class文件,则仅仅在首次主动使用时才报LinkageError错误。


2.4 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

这就是和Class.forName()的区别

2.5 父亲委托机制

更好的保证Java平台的安全。
除了根加载器外,其他加载器都有且仅有一个父加载器。当程序要求加载器X加载Sample类时,X会去先委托父加载器去加载Sample类,如果父加载器不能加载才会让X发挥作用。
并不是所有的虚拟机实现都是这样的机制。

好处

  • 可以确保Java核心库的类型安全:例如,Object类总是会被加载,但如果是自定义加载器来加载的Object类,那么很可能会有多个版本的Object类,这些类之间还不兼容,互相不可见(命名空间的原因)。
  • 确保Java核心类库不会被自定义的类所替代:例如自定义了一个Object类,但根类加载器会先去加载官方的Object,之后系统加载器就不会去加载自定义的Object了。
  • 不同的类加载器可以为相同名字的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的加载器来加载即可。相当于创建了多个相互隔离的类空间。

代码实例:

package JVM;

public class TestClassLoader {
    public static void main(String[] args) throws Exception {
        /**
         * clazz.getClassLoader()
         * 返回加载了这个对象所代表的类(或者接口)的类加载器。
         * 如果返回null,则代表是根类(启动类)加载器(或者其他情况)
         */
        Class<?> clazz = Class.forName("java.lang.String");
        System.out.println(clazz.getClassLoader());

        Class<?> clazz2 = Class.forName("JVM.C");
        System.out.println(clazz2.getClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2 系统类加载器(应用类加载器)
    }
}
class C {

}

上面的例子中,如果用getClassLoader得到的loader再去执行

  loader.loadClass("某个类的全限定名称");

不会导致该类的初始化:

  • Class.forName得到的class是已经初始化完成的
  • Classloder.loaderClass得到的class是还没有链接的,仅加载

2.6 获取ClassLoader

  1. 获取当前类的ClassLoader:
    clazz.getClassLoader();
  2. 获取当前线程上下文的ClassLoader: Thread.currentThread().getContextClassLoader();
  3. 获取系统的ClassLoader:
    ClassLoader.getSystemClassLoader()
  4. 获取调用者的ClassLoader:
    DriverManager.getCallerClassLoader()

2.7 命名空间

  • 每个类加载器都有自己的命名空间,命名空间有该加载器和父加载器所加载的类组成
  • 在同一个命名空间中,不会出现类的完整名字相同的两个类。
  • 但是在不同的命名空间中,可能出现类的完整名字相同的两个类
  • 子加载器所加载的类能够访问父加载器所加载的类
  • 父加载器所加载的类不能访问子加载器所加载的类

前面所说的一个类不能加载两次,是有前提的,也就是必须在同一个命名空间中。
在运行期,一个Java类是由该类的完全限定名称+加载该类的加载器所共同决定的。如果相同名字的类由两个不同的加载器所加载,那么这些类也是不同的,即便class文件的字节码完全一样,并且从相同的文职加载亦如此。

2.8 上下文加载器

sout(Thread.currentThread().getContextClassLoader());
sout(Thread.class.getClassLoader);
输出:
AppClassLoader
null (代表根类加载器)

1.当前类加载器(Current ClassLoader)指加载了当前类的加载器。
每个类都会使用自己的类加载器去加载其他所依赖的类。
如果ClassA引用了ClassY,那么ClassX的加载器就会尝试去加载ClassY(前提是ClassY尚未被加载)。

2.线程上下文类加载器(context classloader)
Thread类中有context classloader的setget方法。如果没有通过set方法来设置的话,线程降级成府县丞的上下文类加载器。
Java的初始线程的上下文加载器是系统类加载器。

线程上下文类加载器的重要性
SPI(service provider interface)
父加载器可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的上下文加载器所加载的类。
这就改变了父classloader不能使用子class loader或是其他没有直接父子关系的加载器所加载的类的情况,即改变了双亲委托模型。

线程上下文类加载器就是当前线程的current classloader。

注意接口和接口实现类:
在双亲委托模型下,下层的类加载器回去委托上层进行加载。但是对于SPI来说,有些接口是Java核心库提供的,而核心库是启动类加载器来加载的,接口的实现缺来自于不同的jar包(厂商提供),启动类加载器不会加载其他来源的jar包,这样双亲委托机制就无法满足SPI的要求。而通过给当前线程设置上下文加载器,就可以有上下文加载器来实现对于接口实现类的加载。


3 类加载的经典例子

会显示什么?

public class Father{
	private int i = test();
	private static int j = method();
	
	static{
		System.out.print("(1)");
	}
	Father(){
		System.out.print("(2)");
	}
	{
		System.out.print("(3)");
	}
	
	
	public int test(){
		System.out.print("(4)");
		return 1;
	}
	public static int method(){
		System.out.print("(5)");
		return 1;
	}
}

public class Son extends Father{
	private int i = test();
	private static int j = method();
	static{
		System.out.print("(6)");
	}
	Son(){
//		super();//写或不写都在,在子类构造器中一定会调用父类的构造器
		System.out.print("(7)");
	}
	{
		System.out.print("(8)");
	}
	public int test(){
		System.out.print("(9)");
		return 1;
	}
	public static int method(){
		System.out.print("(10)");
		return 1;
	}
	public static void main(String[] args) {
		Son s1 = new Son();
		System.out.println();
		Son s2 = new Son();
	}
}

在这里插入图片描述
考点有三个:

  1. 类的初始化过程
  2. 实例的初始化过程
  3. 方法的重写

3.1 类的初始化过程

在这里插入图片描述

3.2 实例的初始化过程

在这里插入图片描述

3.3 方法的重写override

在这里插入图片描述

3.4 本实例的具体分析

  1. 第一行的Son s1 = new Son(); 是对Son类的首次主动使用,需要初始化Son类,但其继承了Father,所以需要先初始化Father类。

  2. Father类中,从上到下执行静态变量赋值和静态代码块:
    输出 51
    在这里插入图片描述

  3. Son类中,从上到下执行静态变量赋值和静态代码块:
    输出 106,此时类的初始化完毕。
    在这里插入图片描述

  4. 接下来是实例的初始化。首先是父类:
    调用子类的test,输出9
    运行父类的非静态代码块,输出3
    调用父类的构造函数,输出2
    在这里插入图片描述

  5. 子类实例的初始化:
    同上一步,输出987
    此时第一行的Son s1 = new Son();执行完毕。

  6. 接下来第二个Son s1 = new Son();
    因为不再是首次主动使用了,所以不会进行的初始化,直接是实例的初始化。步骤和 4 , 5 一致,输出932987




4 字节码

  1. 反编译查看class文件:javap -c 或者 -verbose xxx.class后缀省略。
    将会分析该字节码文件的魔数、版本号、常量池、类信息、构造方法、方法信息、类变量与成员变量等信息。
  2. 魔数:所有的.class文件的前四个字节,永远是CAFEBABE
  3. java版本信息:魔数之后的四个字节,分别表示次版本号(minor version)和主版本号(major version)。可以通过java -version来验证。
  4. 常量池:主版本号之后就是常量池入口,第九个字节。可以将常量池看作Class文件的资源仓库,比如说Java类中定义的方法和变量信息(常量池中不一定都是常量),就是存储在常量池中。常量池中存储两类常量:字面量和符号引用。
    字面量如文本字符串、Java中声明为final的常量值等。
    符号引用比如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符等。
    常量池入口的两个字节表示常量池数量,之后就是常量池数组。常量池数组中不同的元素的类型、结构都是不同的,自然长度也不同。但是每一个元素的第一个字节都是u1类型,表示标志位。jvm会根据u1类型来获取元素的具体类型。
    常量池数组中的元素的个数 = 常量池数量 - 1.(目的是 索引为0也是一个常量,只不过对应null,保留常量不显示。)
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言中常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过中序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言中都有相应的实现方式,可以应用于各种不同的场景。C语言中的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存中是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值