JVM整理(一)

JVM介绍

背景

通常我们讲的JVM一般都是指oracle公司的hotspot虚拟机,它是java可以"一处编译,到处运行"的关键,是java运行时虚拟环境,屏蔽了底层操作系统间的差异.
除了hotspot,还有JRockit(该公司被oracle公司收购后与hotspot进行了整合),J9(IBM),Graal VM(oracle新一代虚拟机,功能强大,官网地址:https://github.com/oracle/graal)
JVM除了可以运行java语言,还可以运行Groovy,JRuby,Scala,Kotlin,Fantom,Jython等,支持语言还是比较多的.

JDK、JRE、JVM的关系

如图:
JVM整理(一)_第1张图片

JDK

jdk即为java开发工具包,包含编写java程序所必须的编译、运行等开发工具以及JRE

JRE

jre即为java运行环境,提供运行java应用程序所必须的软件环境,包含有java虚拟机和丰富的系统类库

JVM

jvm即为java虚拟机,提供字节码文件(.class文件)的运行环境支持

JDK介绍

jdk从jdk9(2017年9月发布)开始,每半年一个周期发布一代版本,新一代版本发布,非LTS版本就会停止更新.截止到目前,java SE20(2023年3月发布)是最新发布的版本.
目前oracle还在维护的LTS为:java8,java11,java17(springboot3依赖的基础jdk版本)
历史明细参加: https://zh.wikipedia.org/zh-cn/Java版本歷史
javaSE下载目录: https://www.oracle.com/java/technologies/downloads/archive/

Hotspot介绍

目前大部分公司基本都还在使用jdk8的版本,所以本文也重点介绍hotspot在jdk8版本对JVM具体实现介绍,下面jvm的概念实现都指的是hotspot虚拟机.
JVM由四部分组成: 类加载器子系统、运行时数据区、执行引擎、本地库接口
JVM整理(一)_第2张图片

类加载(Class loader)

java文件都是通过类加载器进行加载到jvm运行时数据区的,类加载器分为引导类加载器(Bootstrap)和自定义类加载器(ExtClassLoader和AppClassLoader)

Bootstrap

是java所有自定义加载的父加载器,是用C语言编写实现的,加载类对象是在jdk/jre/lib目录下的rt.jar里面的文件

ExtClassLoader

是AppClassLoader的父加载器,是Launcher类的静态内部类,继承URLClassLoader,加载类对象是在jdk/jre/lib/ext下面的jar包

AppClassLoader

也是Launcher类的静态内部类,继承URLClassLoader,加载类对象是CLASSPATH路径下的包

双亲委派机制

jvm加载模式采用双亲委派模型.所有的类都先交给顶类加载,顶类加载不了在交由它的子类去加载,这样的好处主要由以下几点:

  • 避免类的重复加载:在 JVM 中,每个类都由一个唯一的全限定名和一个对应的类加载器确定,类加载器根据全限定名和类路径来确定类的位置。因此,在一个 JVM 实例中,如果有两个类加载器分别加载了同一个类,JVM 会认为这两个类是不同的,从而导致类型转换异常等问题。通过双亲委派机制,父类加载器在加载类之前会先委托给自己的父类加载器去加载,从而保证一个类在 JVM 中只会有一份,并且由其父类加载器所加载。
  • 安全性考虑:Java 核心类库(如 java.lang 包下的类)都是由启动类加载器加载的,其他的类都是由其它类加载器加载的。这样,我们就可以保证 Java 核心类库的安全性,因为不同的应用程序无法改变这些类的实现。另外,也可以在类加载过程中做一些安全性检查。
  • 模块化开发:在实际应用中,我们经常需要在一个程序中使用多个第三方库,这些库可能会存在同名类。如果使用了双亲委派机制,就可以保证不同的类加载器只会加载自己的类,从而避免了类名冲突的问题。

双亲委派机制的实现是ClassLoader的loadClass的逻辑代码

    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
自定义类加载器

从上面对于ClassLoader的loadClass方法分析来看:
1.如果不想打破双亲委派模型,那么子类只需重写findClass方法即可
2.如果想打破双亲委派模型,那么子类就要重写loadClass方法
当然,我们自定义的类加载器肯定不想打破双亲委派模型

源码如下:
MyDefineClassLoader(自定义类加载器)

package pers.xiaotian.loader;

import java.io.*;

public class MyDefineClassLoader extends ClassLoader{

    public MyDefineClassLoader()
    {

    }

    public MyDefineClassLoader(ClassLoader parent)
    {
        super(parent);
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        try
        {
            byte[] bytes = getClassBytes(name);
            //调用父类的defineClass 将二进制流进行转换为Class对象
            Class c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getClassBytes(String name) throws IOException {
        //这边的pathname为测试类当中需要加载的name编译之后的输出路径
        File file = new File("/Users/xiaotian/workspace/owner/J2EE/target/classes/pers/xiaotian/utils/CollectionsUtil.class");
        FileInputStream fis = new FileInputStream(file);
        byte[] bytes = new byte[fis.available()];
        fis.read(bytes);
        return bytes;
    }

}

MyDefineClassLoaderTest(自定义类加载器测试类)

package pers.xiaotian.loader;

public class MyDefineClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        MyDefineClassLoader mdcl = new MyDefineClassLoader(ClassLoader.getSystemClassLoader().getParent());
        Class cl = Class.forName("pers.xiaotian.utils.CollectionsUtil", true, mdcl);
        Object obj = cl.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

输出结果:
image.png

类加载过程

主要分为三个步骤:

  • 加载
  • 链接
  • 初始化

其中链接又可细分为三个步骤:

  • 验证
  • 准备
  • 解析

如图:
JVM整理(一)_第3张图片

加载

简单来说,加载指的就是把字节码文件通过类加载器装载入内存中.包括本地磁盘class文件,远程网络class文件,以及动态代理实时编译class文件.
类加载器上面已经介绍了,这里就不在赘述了.

验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误.如字节码文件是否以CAFE BABE的格式打头等.

准备

主要是为类变量分配内存,并且赋予初值.注意,这里指的初值是jvm根据不同变量类型默认的初始值.

解析

将常量池内的符号引用替换为直接引用的过程.

  • 符号引用.一个字符串给出一些能够唯一性识别一个方法,一个变量,一个类的相关信息
  • 直接引用.可以理解为一个内存地址,或者一个偏移量.
初始化

主要是对类变量初始化,执行类构造器的过程.就是对静态变量和静态代码块进行初始化.
如果初始化一个类,但该类的父类还没有被初始化,则先初始化它的父类.如果一个类有多个静态变量和静态代码块,则按照从上到下的顺序依次执行

[下一章节 介绍运行时数据区]

你可能感兴趣的:(java)