JVM类加载(Java)1.0

JVM是什么

我们先了解一下什么是操作系统,先从离我们生活比较近的概念切入,它并不是具体的指Linux和Windows。

日常我们开发的程序会运行在不同的硬件上,例如服务器,台式机,笔记本,手机,电视机机顶盒,智能家居等待这些硬件上。我们要为这些硬件设备写程序,就不可避免的要跟这些硬件物理上的接口打交道,这个打交道过程是非常麻烦和复杂的。我们开发程序时,希望关注点在业务逻辑上,而不是和业务逻辑没有关系的硬件的细节上,因此就有了我们操作系统。

操作系统就是在我们开发的程序和硬件之间,作为程序和硬件沟通的桥梁。我们的程序只要和操作系统打交道,和硬件打交道这些复杂的内容交给了操作系统,另一方面硬件不对外暴露,保证了硬件的安全。

JVM,它的全称是Java Virtual Machine ,Java 虚拟机,也是作为我们开发的Java程序和操作系统之间沟通的桥梁,我们只要关注Java程序的开发和JVM是如何运行实现的,并不需要关心Java程序和不同操作系统之间打交道的细节,把这些交给JVM即可。使Java程序实现平台独立和可移植性,一次编译,随处运行。

JVM就是在计算机上再虚拟一个计算机,来运行我们的Java程序,它存在内存中。我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备,那这个JVM 也是有这成套的元素。

JVM 也有自己的指令集,Java是一门高级语言,计算机并不认识Java语言,我们必须先编译Java代码,将Java语言都编译成一条条JVM的指令,再交给JVM运行。这些指令与汇编的命令集有点类似,每一种汇编命令集针对一个系列的CPU ,比如8086 系列的汇编也是可以用在8088 上的,但是就不能跑在8051 上,而JVM 的命令集则是可以到处运行的,因为JVM 做了翻译,根据不同的操作系统 ,翻译成不同的操作指令。

JVM的启动是jdk中Java.exe来运行的,本篇文章的JVM默认为Hotspot。

.class文件是什么

它就是我们Java代码编译后的文件,是JVM的类加载器能够理解的,一组以8位字节为基础单位的二进制流。

我们打开本地安装的Java/jdk1.8.0_171/bin目录,日常我们在IDE界面中开发Java用到的命令都是依靠这些实现。我们配置的JDK环境变量,也是一种快捷键,可以在命令行中快速执行Java命令。
在这里插入图片描述

我们可以看到有一个javac.exe,全称java compiler,它是java语言编程编译器。负责读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。

我们用记事本编写一段Java代码,后缀名修改为java。再编译该文件,查看内容。

public class Test {

    public static void main(String[] args) throws Exception {
        System.out.println("Hello World");
    }
}

在这里插入图片描述
运行成功后,我们再看看JVM读取到的class文件内容都是些什么。我们直接用文本编辑器打开发现是一堆乱码,要切换成16进制查看。(我用的是editplus 打开class文件,点击Hex Viewer查看的)
在这里插入图片描述
这里我们再用一个工具javap,可以反编译,也可以查看java编译器生成的字节码,是分析代码的一个好工具。
在这里插入图片描述
对照JVM字节码指令表(Java指令集相当于Java程序的汇编语言),我们知道了这些指令的作用。虽然在Java虚拟机规范中很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。(需要JVM 虚拟机字节码指令表的可以私信我)

操作符			助记符			指令含义

0xb2			getstatic		获取指定类的静态域, 并将其压入栈顶
0x12			ldc			    将int,float或String型常量值从常量池中推送至栈顶
0xb6			invokevirtual	调用实例方法
0xb1			return			从当前方法返回void
JVM体系结构

JVM是Java程序运行的容器,但是他同时也是操作系统的一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。下图为JVM的体系结构图
在这里插入图片描述

  • 类装载器:主要作用是将class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

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

  • 堆:是一个运行时数据区,类的实例(对象)从中分配空间,被所有线程共享的一块内存区域。

  • 执行引擎:处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。(可以参考上上图的JVM字节码指令)

  • Java栈:线程私有的,它的生命周期与线程相同,是Java方法执行的内存模型。每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 程序计数器:每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。

  • 本地方法栈:代码中主要是native关键字修饰的方法。当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。

JVM通过类装载器加载了class文件,将信息保存在方法区,在堆中创建对象,在栈中执行方法。下图为JVM的运行过程
在这里插入图片描述

JVM类加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类加载的落地实现是靠JVM的一个子系统类加载器实现的。站在Java虚拟机的角度来讲,只存在两种不同的类加载器。
启动类加载器:它使用C++实现,是虚拟机自身的一部分;
其它类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类;

站在Java开发人员的角度来看,类加载器可以大致划分为以下四类:
启动类加载器: BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器:自定义类加载器一般都是继承自 ClassLoader类,重写父类方法。

我们可以写一段代码验证一下,我们可以看到AppClassLoader和ExtClassLoader,ExtClassLoader的父类是BootstrapLoader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

public class Test {

    public static void main(String[] args) throws Exception {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("loader = " + loader);
        System.out.println("parent = " + loader.getParent());
        System.out.println("parent.parent = " + loader.getParent().getParent());
    }
}

我们再写一个自定义的类加载器,实现从本地文件中读取加载到JVM内存中。我们先编译资源类

public class MyClass {
    private static int a;
    private static int b;
    static {
        a = 1;
        b = 2;
        System.out.println("MyClass.static Finished");
    }
    public static void main(String[] args) {
        System.out.println("add = " + a + b);
    }
}
MyClass.static Finished
add = 12
public class Test extends ClassLoader {

    private String filePath;

    @Override
    protected Class<?> findClass(String s) throws ClassNotFoundException {
        byte[] bytes = loadClassData(s);
        if (bytes == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(s, bytes, 0, bytes.length);
        }
    }

    private byte[] loadClassData(String className) {
        String classFilePath = filePath + className + ".class";
        try (
                InputStream is = new FileInputStream(classFilePath);
                ByteArrayOutputStream os = new ByteArrayOutputStream()
        ) {
            byte[] buffer = new byte[1024];
            int length = 0;
            while ((length = is.read(buffer)) != -1) {
                os.write(buffer, 0, length);
            }
            return os.toByteArray();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public static void main(String[] args) throws Exception {
        Test test = new Test();
        test.setFilePath("C:\\Users\\China\\Desktop\\");

        Class<?> myClass = test.loadClass("MyClass");
        Object o = myClass.newInstance();
        System.out.println("loader = " + o.getClass().getClassLoader());
    }
}

我们可以看到我们的MyClass的Class对象加载是由我们自定义的Test加载实现的,这样可以实现动态地创建符合用户特定需要的定制化构建类。还可以从特定的场所取得java class,例如数据库中和网络中。

上述4中类加载器,除了BootstrapClassLoad,我们都在代码中验证。实际代码应用中进行类加载除了ClassLoader.loadClass()方式实现,还可以用Class.forName()。
我们把上面的main方案稍微改造一下,运行后,我们发现方式一static块中的输出语句没有打印。而方式二打印了静态块中的输出语句。方式一只有在我们调用newInstance()时,才会初始化这个类对象。方式二也可以设置不初始化类对象,将第二个入参修改为false。

    public static void main(String[] args) throws Exception {
        Test test = new Test();
        test.setFilePath("C:\\Users\\China\\Desktop\\");

        //方式一
        Class<?> myClass = test.loadClass("MyClass");
        System.out.println("myClass = " + myClass.getClassLoader());

        System.out.println();

        //方式二
        Class<?> myClass1 = Class.forName("MyClass", true, test);
        System.out.println("myClass1 = " + myClass1.getClassLoader());
    }

到这里我们了解了什么是JVM,class文件的内容是什么,class文件是如何加载到JVM中的,如何自定义类加载器。我们再讲最后一个概念双亲委派模型。它组织了类加载器之间的关系,定义了不同类加载器的加载顺序,来保证Class对象再JVM中的唯一性。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
在这里插入图片描述
结合源码将上图代码实现(java.lang.ClassLoader#loadClass(java.lang.String, boolean))。

protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
		// 线程安全 同步代码块
        synchronized(this.getClassLoadingLock(var1)) {
        	// 首先判断该类型是否已经被加载
            Class var4 = this.findLoadedClass(var1);
            if (var4 == null) {
                long var5 = System.nanoTime();
				//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (this.parent != null) {
                    	//如果存在父类加载器,就委派给父类加载器加载
                        var4 = this.parent.loadClass(var1, false);
                    } else {
                    	//如果不存在父类加载器,就检查是否是由启动类加载器加载的类
                        var4 = this.findBootstrapClassOrNull(var1);
                    }
                } catch (ClassNotFoundException var10) {
                    ;
                }

                if (var4 == null) {
                    long var7 = System.nanoTime();
                    //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    var4 = this.findClass(var1);
                    PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (var2) {
                this.resolveClass(var4);
            }

            return var4;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值