尚硅谷2020最新版宋红康JVM教程学习笔记 一 类加载子系统

点击查看合集
JAVA语言是跨平台的语言,JVM虚拟机是跨语言的平台。JVM虚拟机并不关心字节码文件是由什么语言生成的。只有不同的语言通过编译器生成符合JVM虚拟机规范的字节码文件,JVM就能够运行。JAVA就是通过编译生成字节码文件,然后再在JVM上运行。因此JAVA语言可以一次编译到处运行。

虚拟机

虚拟机是一台虚拟计算机,是用来执行虚拟计算机指令的软件。虚拟机可以分为系统虚拟机和程序虚拟机。
如VMware就是系统虚拟机,他是对物理计算机的仿真。
Java虚拟机是程序虚拟机,他专门为执行单个计算机程序而设计,在java虚拟机中执行的指令我们称为Java字节码指令。Java技术的核心就是java虚拟机。它能够执行字节码文件,任何语言生成的字节码文件都可以共享java虚拟机带来的跨平台性、垃圾回收器,即时编译器。

java虚拟机

java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为机器指令。JDK默认的虚拟机是HotSpot,采用解释器和即时编译器并存的架构。

JVM的架构模型

java编译器输入的指令基本上是基于栈的指令集架构。另一种是基于寄存器的指令集架构。

区别

基于栈式架构的特点:
1.设计和实现更简单,适用于资源受限的系统
2.避开了寄存器的分配难题:使用零地址指令方式分配
3.指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
4.不需要硬件支持,可移植性更好,更好实现跨平台。
跨平台性,指令集小,指令多;性能比寄存器差。
基于寄存器架构的特点:
1.指令集架构依赖硬件,可移植性差。
2.性能优秀和执行更高效。
3.花费更少的指令去完成一项操作。
4.在大部分情况下基于寄存器架构的指令都以一地址指令,二地址指令和三地址指令为主。

JVM生命周期

启动

java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。

执行

程序开始时他才运行,程序结束时他就结束。执行java程序实际上是在执行一个java虚拟机的进程。

退出

1.程序执行结束
2.程序执行时遇到异常或者错误
3.操作系统出现错误
4.调用runtime类或System类的exit方法。或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作

类加载子系统

类加载器子系统负责加载Class文件,Class文件在文件开头有特定的文件标识。类加载器只负责class文件的加载。加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面值和数字常量。(这部分常量信息是class文件中常量池部分的内存映射)
类加载器子系统包括三个步骤加载 链接 初始化。
在这里插入图片描述

加载阶段

1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
jvm规范规定了只有两种类加载器引导类加载器和自定义类加载器( 所有派生于抽象ClassLoader的类加载器都划分为自定义类加载器)。自定义类加载器又包括了扩展类加载器,系统类加载器和用户自己定义的类加载器。加载器之前不是继承关系,而是一种组合/包含关系。

public class Test1 {
    
    
    public static void main(String[] args) {
    
    
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        //获取扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1b6d3586
        //获取引导类加载器
        ClassLoader bootstrapClassLoader  = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null
        //获取Test1类的类加载器
        ClassLoader test1ClassLoader = Test1.class.getClassLoader();
        System.out.println(test1ClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        //获取String类的类加载器
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println(stringClassLoader);//null
    }
}

用户自定义的类是由系统类加载器加载的,而核心类是由引导类加载器加载的。并且我们不能获取到引导类加载器,因为它是由c/c++写的。

引导类加载器(启动类加载器/BootStrap ClassLoader)

1.这个类加载器使用c/c++语言实现,嵌套在JVM内部
2.它用来加载Java的核心类库

public class Test3 {
    
    
    public static void main(String[] args) {
    
    
        //获取启动类加载器加载的类的路径
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
    
    
            System.out.println(urL);
        }
    }
}

在这里插入图片描述

3.没有父类加载器,不是继承自java.lang.ClassLoader
4.用来加载扩展类和系统类加载器,并指定为他们的父类加载器。

扩展类加载器

1.由java语言编写,是sun.misc.Launcher的一个内部类。
2.派生于ClassLoader类
3.父类加载器为BootStrap类加载器
4.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

public class Test4 {
    
    
    public static void main(String[] args) {
    
    
        //获取系统属性java.ext.dirs包含的值
        String path = System.getProperty("java.ext.dirs");
        for (String s : path.split(";")) {
    
    
        //打印扩展类加载的类路径
            System.out.println(s);
        }
    }
}

在这里插入图片描述

系统类加载器(应用程序加载类/AppClassLoader)

1.由java语言编写,是sun.misc.Launcher的一个内部类。
2.派生于ClassLoader类
3.父类加载器为扩展类加载器
4.他负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
5.是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载。

用户自定义类加载器

在java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
使用用户自定义类加载器的目的
1.隔离加载类
2.修改类加载的方式
3.扩展加载源
4.防止源码泄露
实现方式
1.继承ClassLoader类重写loadClass()方法(JDK1.2前)
2.继承ClassLoader类并重写findClass()方法(JDK1.2后)
3.直接继承URLClassLoader类(如果没有太过复杂的需求)

ClassLoader抽象类

除了启动类加载器外,其他的类加载器都是直接或间接地继承自ClassLoader
在这里插入图片描述
在这里插入图片描述

获取ClassLoad的方法

方式一:

public class Test5 {
    
    
    public static void main(String[] args) {
    
    
        ClassLoader classLoader = Test5.class.getClassLoader();
    }
}

方式二

public class Test5 {
    
    
    public static void main(String[] args) {
    
    
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    }
}
方式三

```java
public class Test5 {
    
    
    public static void main(String[] args) {
    
    
       
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader parent = systemClassLoader.getParent();
    }
}

链接阶段

连接分为三个过程 验证 准备 解析
一 验证
检验class文件中包含的信息符合虚拟机要求,保证被加载类的正确性,不会危害虚拟机。
二准备
1.为类变量分配内存并且设置该类变量的默认初始值(0)。
2.这里不包含用final 修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。
3.这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
三:解析
将常量池内的符号引用转换为直接引用的过程。
符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化阶段

初始化阶段就是执行类构造器方法clinit的过程。此方法是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

在这里插入图片描述

构造器方法中的指令按语句在源文件中出现的顺序执行。

public class Test1 {
    
    
    static int a1 = 10;
    static {
    
    
        int a2 = 20;
    }
    static int a3 = 30;
}

在这里插入图片描述

clinit不同于类得构造器(类的构造器方法是init方法)

public class Test1 {
    
    

    Test1(){
    
    
        int a1 = 10;
    }
}

在这里插入图片描述

若该类具有父类,JVM会保证子类的clinit执行前父类的clinit已经执行完毕

public class Test1 {
    
    
    static class A{
    
    
        static int num1 = 10;
    }
    static class B{
    
    
        static int num2 = A.num1;
    }
    public static void main(String[] args) {
    
    
        System.out.println(B.num2);
    }
    static {
    
    
        A.num1 = 100;
    }
}

在这里插入图片描述

虚拟机必须保证一个类的clinit方法在多线程下被同步加锁。

public class Test1 {
    
    
    public static void main(String[] args) {
    
    
        Runnable run = () -> {
    
    
            System.out.println(Thread.currentThread().getName()+"开始");
            Test2 t2 = new Test2();
            System.out.println(Thread.currentThread().getName()+"结束");
        };
        Thread t1 = new Thread(run,"线程1");
        Thread t2 = new Thread(run,"线程2");
        t1.start();
        t2.start();
    }
}
class Test2 {
    
    
    static {
    
    
        int a = 0;
        System.out.println(Thread.currentThread().getName()+"正在初始化Test2");
        boolean f = true;
        while (f){
    
    

        }
    }
}

在这里插入图片描述
只有线程1进入了static代码块,因此可以得出,虚拟机在执行clinit时确实会加锁。

双亲委派机制

Java虚拟机对class文件采用的是按需加载方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

原理

1.如果一个类加载器收到要加载类的请求,它自己不会直接加载类,而是请求父加载器加载。直到没有父加载器为止(启动类加载器)。如果父类可以加载该类,就由父类加载,若不能加载,子加载器才会尝试加载。

public class Test6 {
    
    
    public static void main(String[] args) {
    
    
        String string = new String();
        System.out.println(string.getClass().getClassLoader());
    }
}
package java.lang;

/**
 * @Auther: Maple
 * @Date: 2020/11/30
 */
public class String {
    
    
    static {
    
    
        System.out.println("自定义String");
    }
}

在这里插入图片描述

沙箱安全机制

自定义String类,但是在加载自定义String类时会率先使用启动类加载器加载,而启动类加载器在加载的过程中会先加载JDK自带的文件,报错信息说没有main方法,就是因为加载的时rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

优势

1.避免类被重复加载
2.保护核心API不被恶意篡改

怎么判断类相等

1.类的全路径类名相同
2.加载该类所用的类加载器相同

对类加载器的引用

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中,当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

一:主动使用
1.创建类的实例。
2.访问某个类或结构的静态变量,或者对该静态变量赋值。
3.调用类的静态方法
4.反射
5.初始化一个类的子类
6.java虚拟机启动时被标明为启动类的类
7动态语言.
除了以上七种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化

猜你喜欢

转载自blog.csdn.net/qq_30033509/article/details/110293712