前言


从本篇文章开始进入JVM的学习,后面咱们介绍了JAVA、JVM等等

这篇文章咱们从类加载子系统开始进入学习

一、内存构造概述


请先看以下的简图,class Files咱们称为字节码,从字节码开始后续操作都须要JVM负责

第一步:咱们要将Class文件加载到内存当中,而类加载须要用到类加载子系统Class Loader来进行加载

同时对应到咱们的内存当中,生成一个大的Class对象并且将必要的动态属性进行初始化等等(办法区提现)

第二步:当咱们真正去执行字节码指令的时候,就须要执行引擎去发挥作用,依照咱们程序的字节码指令去顺次执行(波及到虚拟机栈里去局部变量表取数据,以及操作入栈),若须要创建对象的话还须要用到堆空间

第三步:当程序持续往下走的时候,还会用到程序计数器,若用到本地的C类库,还须要用到本地办法栈

下面是简图,咱们能够看以下的具体图

依据具体图,咱们能够看到类加载子系统分三个局部

  • 加载阶段
  • 链接阶段
  • 初始化阶段

如图所知加载阶段分三个环节:疏导类、扩大类、零碎类等加载器

如图所知链接阶段分三个环节:验证、筹备、解析

紧接着就是动态变量的一个显示初始化,接下来就将每个字节码文件要用到的,在对应的在内存中把类或者接口加载进来

在内存层面运行时数据区有:PC寄存器(程序计数器)、栈(虚拟机栈)、本地办法栈、堆区、办法区

PC寄存器:每一个线程一份

虚拟机栈:每一个线程一份,每一个线程用的栈外面一个一个构造称为栈桢,栈桢又分为局部变量表、操作数栈、动静链接、办法返回地址等

本地办法栈:波及到本地办法接口API调用叫本地办法栈

堆区:次要应答Java对象等都放在堆空间中,也是GC重点思考的一个空间因为堆区会被线程共享的

办法区:次要寄存类的信息(常量、办法信息等等)都放在办法区

留神:办法区只有HotSpot虚拟机有,J9,JRockit都没有

执行引擎又分解释器、即时编译器、垃圾回收器,将咱们的指令变成机器指令供CPU去执行,要想和操作系统打交道须要关注执行引擎打交道

若想真正理解一个虚拟机,能够手写一个虚拟机

如果本人想手写一个Java虚拟机的话,次要思考类加载器、执行引擎构造

二、类加载器与类的加载过程


咱们刚刚提到类加载子系统呢分三个阶段:加载、链接、初始化等阶段

JAVA虚拟机提到阐明:任何语言能够思考用间接的编辑器生成合乎Java虚拟机标准的Class文件来在Java虚拟机进行解释运行

类加载器ClassLoader角色

1.class file(在上图中就是Car.class文件)存在于本地硬盘上,通过类加载器把它加载到内存运行时数据区

2.class file加载到JVM中后会被称为DNA元数据模板放在办法区

3.car.class文件能够调用getClassLoader()办法获取加载此类的加载器,同时能够依据car.class的结构器创在堆空间中创立多个对象

4.对应的对象能够通过getClass()获取到类的自身,晓得由那个类创立的对象

而本地磁盘的Class文件是由二进制流的形式加载到内存中,类加载器起到快递员的身份

接下来咱们应用一段代码来领会一下加载过程

public class HelloLoader {    public static void main(String[] args) {        System.out.println("谢谢ClassLoader加载我....");        System.out.println("你的大恩大德,我下辈子再报!");    }}//运行后果如下:谢谢ClassLoader加载我....你的大恩大德,我下辈子再报!

那么咱们的这个HelloLoader类,它的加载过程是怎么样的呢?

  • 执行 main() 办法(静态方法)就须要先加载main办法所在类 HelloLoader
  • 若HelloLoader没有加载,须要应用相应的ClassLoader进行加载
  • 加载失败则抛出异样
  • 加载胜利则进行链接、初始化等操作
  • 加载实现后调用 HelloLoader 类中的静态方法 main

接下来咱们对类加载器进行加载、链接、初始化不同阶段进行开展,看看做了哪些事件

三、类加载器中的加载阶段


咱们一起看看加载阶段的一些加载阐明:

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为办法区这个类的各种数据的拜访入口

那么对于一些加载.class文件的形式咱们能够进行一些举例说明

  • 从本地零碎中间接加载
  • 通过网络获取,典型场景:Web Applet
  • 从zip压缩包中读取,成为日后jar、war格局的根底
  • 运行时计算生成,应用最多的是:动静代理技术
  • 由其余文件生成,典型场景:JSP利用从专有数据库中提取.class文件,比拟少见
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施

四、类加载器中的链接阶段


后面咱们提到过链接阶段分三个环节:验证、筹备、解析

对于后面的加载阶段实现后,咱们就曾经生成了一个比拟大的Class对象,第一个验证环节次要做以下几件事件

  • 目标在于确保Class文件的字节流中蕴含信息合乎以后虚拟机要求,保障被加载类的正确性,不会危害虚拟机本身平安
  • 次要包含四种验证:文件格式验证,元数据验证,字节码验证,符号援用验证

咱们能够将下面举例的HelloLoader类查看它的字节码

应用 BinaryViewer软件查看字节码文件,能被java虚拟机辨认的其结尾均为 CAFE BABE

class文件在文件结尾有特定的文件标识说的就是这

如果发现你不是一个非法的字节码文件,那么将会验证不通过

刚刚介绍的是验证环节,接下来是链接阶段的筹备环节介绍次要以下事件

  • 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
  • 这里不蕴含用final润饰的static,因为final在编译的时候就会调配好了默认值,筹备阶段会显式初始化

留神:这里不会为实例变量调配初始化,类变量会调配在办法区中,而实例变量是会随着对象一起调配到Java堆中

咱们能够举个例子一起来看看static动态变量在筹备阶段的初始值

public class HelloApp {    //prepare:a = 0 ---> initial : a = 1    private static int a = 1;    public static void main(String[] args) {        System.out.println(a);    }}

刚刚介绍的是筹备环节,接下来是链接阶段的解析环节介绍次要以下事件

  • 将常量池内的符号援用转换为间接援用的过程
  • 符号援用就是一组符号来形容所援用的指标。符号援用的字面量模式明确定义在《java虚拟机标准》的class文件格式中。间接援用就是间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄
  • 解析动作次要针对类或接口、字段、类办法、接口办法、办法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

咱们能够反编译 class 文件后能够查看符号援用,上面带# 的就是符号援用

四、类加载器中的初始化阶段


当执行完加载阶段、链接阶段达到初始化阶段时,就会执行类结构器办法<clinit>()的过程。

此办法不需定义,是javac编译器主动收集类中的所有类变量的赋值动作和动态代码块中的语句合并而来

咱们能够举例一个示例代码一起来看看具体的<clinit>()

public class ClassInitTest {        private static int num = 1;            public static void main(String[] args) {        System.out.println(a);    }  }

当然咱们运行后会输入:1,这时咱们应用 BinaryViewer软件查看字节码文件的<clinit>()办法里有什么

咱们刚刚<clinit>()是对所有类变量的赋值动作和动态代码块中的语句合并而来

若咱们没有类变量它会呈现吗?

public class ClinitTest {        private int a = 1;        public static void main(String[] args) {        System.out.println(a);    }}

这时咱们再应用应用BinaryViewer软件查看字节码文件的<clinit>()办法里有什么

你就会发现没有,这就阐明没有类变量的赋值动作和动态代码块中的语句它就不会有

咱们之前说任何一个类申明后,至多存在一个类的结构器,应用看看并且察看一下

public class ClinitTest {        private int a = 1;        public ClinitTest(){        a =10;        int d =20;    }        public static void main(String[] args) {        System.out.println(a);    }}

那么咱们应用BinaryViewer软件查看字节码文件init办法里有什么

结构器办法中指令按语句在源文件中呈现的程序执行。

而<clinit>()不同于类的结构器。(关联:结构器是虚拟机视角下的<init>())

并且当若该类具备父类,JVM会保障子类的<clinit>()执行前,父类的<clinit>()曾经执行结束。

咱们举例一个示例代码进行阐明这种状况并通过字节码察看看看是否这样

public class ClinitTestl {        static class Father {                public static int A = 1 ;                statict{            A=2;        }    }        static class Son extends Father {                public static int B= A;    }        public static void main (String[] args){        //加载Father类,其次加载Son类。        System.out.println(Son.B);//2    }}

当咱们执行执行 main() 办法须要加载 ClinitTest1 类,再调用另一个类Son的动态变量所以此时须要加载 Son 类(此时执行<Clinet>()办法)然而在此之前须要执行父类的加载,一起来看看字节码是怎么样的

以及虚拟机必须保障一个类的<clinit>()办法在多线程下被同步加锁。

也就是说保障咱们的类只加载一次

咱们能够应用示例代码来领会一下这个说法

class DeadThread{    static{        if(true){            System.out.println(Thread.currentThread().getName() + "初始化以后类");            while(true){            }        }    }}
public class DeadThreadTest {    public static void main(String[] args) {        Runnable r = () -> {            System.out.println(Thread.currentThread().getName() + "开始");            DeadThread dead = new DeadThread();            System.out.println(Thread.currentThread().getName() + "完结");        };        Thread t1 = new Thread(r,"线程1");        Thread t2 = new Thread(r,"线程2");        t1.start();        t2.start();    }}//运行后果如下:线程2开始线程1开始线程2初始化以后类//程序卡死了...

当咱们的两个线程同时去加载 DeadThread 类,先加载 DeadThread 类的线程抢到了同步锁,而后在类的动态代码块中执行死循环,而另一个线程在期待同步锁的开释

所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)

应用死循环是模仿虚拟机在加载的时候只执行一次,而其余线程进入阻塞状态

五、几品种加载器的介绍与应用领会


个别JVM反对反对两种类型的类加载器别离为

  • 疏导类加载器(Bootstrap ClassLoader)
  • 自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器个别指的是程序中由开发人员自定义的一类类加载器

然而Java虚拟机标准却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

咱们能够看上面这个图,能够清晰的晓得这个继承树的意义

所以将扩大类加载器、零碎类加载器也认为是自定义类加载器

启动类加载器(疏导类加载器,Bootstrap ClassLoader)

  • 这个类加载应用C/C++语言实现的,嵌套在JVM外部
  • 它用来加载Java的外围库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path门路下的内容),用于提供JVM本身须要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器
  • 加载扩大类和应用程序类加载器,并作为他们的父类加载器
  • 出于平安思考,Bootstrap启动类加载器只加载包名为java、javax、sun等结尾的类

扩大类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs零碎属性所指定的目录中加载类库,或从JDK的装置目录的jre/lib/ext子目录(扩大目录)下加载类库。如果用户创立的JAR放在此目录下,也会主动由扩大类加载器加载

应用程序类加载器(也称为零碎类加载器,AppClassLoader)

  • Java语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩大类加载器
  • 它负责加载环境变量classpath或零碎属性java.class.path指定门路下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java利用的类都是由它来实现加载
  • 通过classLoader.getSystemclassLoader()办法能够获取到该类加载器

咱们应用代码来领会一些这几种提到加载器

public class ClassLoaderTest {    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@1540e19d        //获取扩大类加载器其下层:获取不到疏导类加载器        ClassLoader bootstrapClassLoader = extClassLoader.getParent();        System.out.println(bootstrapClassLoader);//null    }}

那么对于这些加载器别离能加载哪些门路下的文件呢?

public class ClassLoaderTest1 {    public static void main(String[] args) {        System.out.println("**********启动类加载器**************");        //获取BootstrapClassLoader可能加载的api的门路        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();        for (URL element : urLs) {            System.out.println(element.toExternalForm());        }    }}//运行后果如下:**********启动类加载器**************file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/resources.jarfile: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/rt.jarfile: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/sunrsasign.jarfile: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jarfile: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jce.jarfile: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/charsets.jarfile: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jfr.jarfile: /D:/developer.tools/Java/jdk1.8.0_131/jre/classes

咱们能够关上门路下的jsee.jar包里的Class文件反查看加载器是什么

public class ClassLoaderTest1 {    public static void main(String[] args) {                //file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar        //从门路中随便抉择一个类,来看看他的类加载器是什么:疏导类加载器        ClassLoader classLoader = Provider.class.getClassLoader();        System.out.println(classLoader);//运行后果:null    }}

接下来咱们接着看看扩大类的加载器有哪一些

public class ClassLoaderTest1 {    public static void main(String[] args) {                System.out.println("***********扩大类加载器*************");        String extDirs = System.getProperty("java.ext.dirs");        for (String path : extDirs.split(";")) {            System.out.println(path);        }    }}//运行后果如下:***********扩大类加载器**** *** ******D: \developer_tools\Java\jdk1.8.0_131\jre\lib\extC: \Windows\Sun\Java\lib\ext

同理咱们关上文件门路通过Class文件反查一下加载器是什么

public class ClassLoaderTest1 {    public static void main(String[] args) {                //file: D:\developer_tools\Java\jdk1.8.0_131\jre\lib\ext        ClassLoader classLoader1 = CurveDB.class.getClassLoader();        System.out.println(classLoader1);    }}//运行后果如下:sun.misc.Launcher$ExtClassLoader@1540e19d

六、用户自定义加载器


在Java的日常利用程序开发中,类的加载简直是由上述3品种加载器相互配合执行的,在必要时咱们还能够自定义类加载器,来定制类的加载形式。

那为什么还须要自定义类加载器?

  • 隔离加载类(比如说我假如当初Spring框架,和RocketMQ有包名门路齐全一样的类,类名也一样,这个时候类就抵触了。不过个别的支流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
  • 批改类加载的形式
  • 扩大加载源(还能够思考从数据库中加载类,路由器等等不同的中央)
  • 避免源码透露(对字节码文件进行解密,本人用的时候通过自定义类加载器来对其进行解密)

如何自定义类加载器?

  • 开发人员能够通过继承抽象类java.lang.ClassLoader类的形式,实现本人的类加载器,以满足一些非凡的需要
  • 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()办法,从而实现自定义的类加载类,然而在JDK1.2之后已不再倡议用户去笼罩loadClass()办法,而是倡议把自定义的类加载逻辑写在findclass()办法中
  • 在编写自定义类加载器时,如果没有太过于简单的需要,能够间接继承URIClassLoader类,这样就能够防止本人去编写findclass()办法及其获取字节码流的形式,使自定义类加载器编写更加简洁。
public class CustomClassLoader extends ClassLoader {    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        try {            //将门路下的文件以流的模式存入到内存中            byte[] result = getClassFromCustomPath(name);            if (result == null) {                throw new FileNotFoundException();            } else {                //defineClass和findClass搭配应用                return defineClass(name, result, 0, result.length);            }        } catch (FileNotFoundException e) {            e.printStackTrace();        }        throw new ClassNotFoundException(name);    }    //自定义流的获取形式    private byte[] getClassFromCustomPath(String name) {        //从自定义门路中加载指定类:细节略        //如果指定门路的字节码文件进行了加密,则须要在此办法中进行解密操作。        return null;    }}

七、对于ClassLoader


ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包含启动类加载器)

以下这些办法都不是形象办法,能够具体的实现

对于ClassLoader的路径

咱们能够依据代码示例领会看看一下

public class ClassLoaderTest2 {    public static void main(String[] args) {        try {            //1.            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();            System.out.println(classLoader);            //2.            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();            System.out.println(classLoader1);            //3.            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();            System.out.println(classLoader2);        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }}//运行后果如下:nullsun.misc.Launcher$AppClassLoader@18b4aac2sun.misc.Launcher$ExtClassLoader@1540e19d

八、双亲委派机制


Java虚拟机对class文件采纳的是按需加载的形式,也就是说当须要应用该类时才会将它的class文件加载到内存生成class对象。

而且加载某个类的class文件时,Java虚拟机采纳的是双亲委派模式,即把申请交由父类解决,它是一种工作委派模式

咱们应用一个案例引入这个双亲委派机制,咱们在本人的src门路下创立本人的java.lang.String类

public class String {    //    static{        System.out.println("我是自定义的String类的动态代码块");    }}

这时咱们在创立一个新的Test类来援用它,并且看看他的加载器是什么

public class StringTest {    public static void main(String[] args) {        java.lang.String str = new java.lang.String();        System.out.println("hello,atguigu.com");        StringTest test = new StringTest();        System.out.println(test.getClass().getClassLoader());    }}//运行后果如下:hello,atguigu.comsun.misc.Launcher$AppClassLoader@18b4aac2

咱们发现程序并没有输入咱们动态代码块中的内容,可见依然加载的是 JDK 自带的 String 类。

这时咱们将代码进行批改一下,再来运行起来看看是怎么样的输入后果

package java.lang;public class String {    //    static{        System.out.println("我是自定义的String类的动态代码块");    }    //谬误: 在类 java.lang.String 中找不到 main 办法    public static void main(String[] args) {        System.out.println("hello,String");    }}//运行后果如下:谬误:在类java.lang.String中找不到main办法,请将main办法定义为:public static void main (String[] args)否则JavaFX 应用程序类必须扩大javafx.application.Application

因为双亲委派机制始终找父类,所以最初找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 办法,所以就报了下面的谬误

双亲委派机制原理

  • 如果一个类加载器收到了类加载申请,它并不会本人先去加载,而是把这个申请委托给父类的加载器去执行;
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,顺次递归,申请最终将达到顶层的启动类加载器;
  • 如果父类加载器能够实现类加载工作,就胜利返回,假使父类加载器无奈实现此加载工作,子加载器才会尝试本人去加载,这就是双亲委派模式。

双亲委派机制劣势

接下来咱们在创立一个示例来java.lang包下看看是否能运行起来

package java.lang;public class ShkStart {    public static void main(String[] args) {        System.out.println("hello!");    }}//运行后果如下:java.lang.SecurityException: Prohibited package name: java.lang    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)    at java.security.AccessController.doPrivileged(Native Method)    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)Error: A JNI error has occurred, please check your installation and try againException in thread "main" 

即便类名没有反复,也禁止应用java.lang这种包名。这是一种爱护机制

在比方咱们应用加载jdbc.jar 用于实现数据库连贯的时候须要用到SPI接口,而SPI接口属于rt.jar包中Java外围api

这个时候咱们就要应用双清委派机制,疏导类加载器把rt.jar包加载进来针对具体的第三方实现jar包时应用零碎类加载器来加载

从这外面就能够看到SPI外围接口由疏导类加载器来加载,SPI具体实现类由零碎类加载器来加载

通过下面的例子,咱们能够晓得,双亲机制能够

  • 防止类的反复加载
  • 爱护程序平安,避免外围API被随便篡改

    • 自定义类:自定义java.lang.String 没有被加载。
    • 自定义类:java.lang.ShkStart(报错:阻止创立 java.lang结尾的类)

沙箱平安机制

当咱们运行自定义String类main办法的时候呈现了报错,这种其实就是沙箱平安机制,不容许你在程序中毁坏外围的源代码程序

九、其余


如何判断两个class对象是否雷同?

在JVM中示意两个class对象是否为同一个类存在两个必要条件:

  • 类的残缺类名必须统一,包含包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须雷同

换句话说,在JVM中,即便这两个类对象(class对象)起源同一个Class文件,被同一个虚拟机所加载,但只有加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的

参考资料


尚硅谷:JVM虚拟机(宋红康老师)