一文带你了解Java虚拟机组成原理

如果一味地只是遭受攻击不反抗,我们就只能止步不前。慢性的无力感是会腐蚀人的。 ——村上春树 《1Q84》

1. JVM 定义

JVM 全称 Java Virtual Machine(Java虚拟机),JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 是运行在操作系统之上的,它与设备硬件并无直接的交互关系

2. Java 的平台无关性

Java 最重要的特性之一就是它的平台无关性,而JVM则是实现平台无关性的关键。Java 源码只需要通过Javac编译成字节码,再由不同平台的JVM 进行解析,JVM 在执行字节码的时候,把字节码转换成具体平台上的机器指令,从而实现Java语言的“一次编译,处处运行”。

3. JVM 生命周期

JVM随Java 程序启动而启动,Java程序的结束而结束。Java线程分为守护线程非守护线程(普通线程)两种:

  • 守护线程指的是程序在后台运行所需的通用线程,比如JVM中的GC线程。
  • 普通线程则相对好理解,就是业务逻辑所产生的线程一般都是普通线程(注:我们可以通过thread.setDaemon(true) 设置普通线程为守护线程),当程序中所有非守护线程结束时,会杀死所有的守护线程,随之程序终止。

4. JVM 架构模型

JVM 结构主要包含四大块:

  • Class Loader : 依照特定的格式,加载Class 文件到内存中。
  • Execution Engine:对命令进行解析
  • Native Interface: 定义了接口规范,通过该接口规范便可以融合其他编程语言原生库为Java所用。
  • Runtime Data Area: JVM 内存结构模型。

JVM架构图

Runtime Data Area 中的 Method Area 和 Heap 是线程共享的,会出现GC操作。Stack、Native Method Stack 和 PC Register 则是线程私有的。

5. ClassLoader 类加载器

5.1 ClassLoader 的作用

ClassLoader在Java 中有着非常重要的作用,它主要工作在Class装载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流转载进系统,然后交给Java虚拟机进行连接、初始化等操作。

ClassLoader 是通过.class 文件中的特定的文件开头标识进行加载,而并非简单的通过.class来判断是否需要加载的。并且ClassLoader 只负责将.class文件中字节码内容加载到内存,并不对格式的正确性进行校验。

5.2 ClassLoader 的种类

通过idea 快捷键Ctrl+n 搜索ClassLoader打开源码位置java.lang.ClassLoader#loadClass可以看到ClassLoader 加载class 是通过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;
        }
    }

查看loadClass方法的代码实现可以发现,代码中出现了parent.loadClass(name, false)进行加载的代码。通过点击parent跳转发现parent事实上也是一个ClassLoader(代码如下)。因此ClassLoader的种类是不止一种的。

public abstract class ClassLoader {

     /代码省略///

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    
    /代码省略///
}

ClassLoader 种类主要分为系统自带自定义加载器两大类。而系统自带的加载器中又分为了三种,所以加上自定义加载器则有四种加载器。

  • 系统加载器
    • BootstrapClassLoader :启动类加载器。 C++ 编写,加载Java核心类库java.*
    • ExtClassLoader: 扩展类加载器。Java 编写,加载扩展库javax.*
    • AppClassLoader: 应用程序类加载器。加载当前应用程序所在目录的类
  • 自定义加载器
    • 自定义ClassLoader: 需继承java.lang.ClassLoader类,用于加载定制化的类。
5.3 实现自定义ClassLoader

通过上文可知,ClassLoader 是 支持自定义实现的。为什么需要自定义类加载器?这主要是因为系统提供的类加载器都是针对加载特定目录的中的类,在开发中我们可能需要去加载一些额外目录下的类,这时则需要去自定义类加载器。

实现自定义类加载器只需实现一下JDK提供的ClassLoader 这个抽象类,该抽象类提供了两个方法findClassdefineClassfindClass 的作用是对.class 文件进行查找并将.class 文件中的字节码读取进来,然后将读取的IO数据交由defineClass去进行解析定义,最后返回一个Class。而我们需要做的事情就是继承ClassLoader并重写findClass 方法。

  • 先定义好一个HelloWorld 类(我把它放在了C:\Users\Administrator\Desktop 目录下,测试时需要指定根目录),然后通过javac 命令编译成.class 文件。
public class HelloWorld {
    public void sayHi(String name){
        System.out.println("hello~ " + name);
    }
}

  • 编写自定义ClassLoader
package com.hzh.test;
import java.io.*;

/**
 * @author Huang Zaizai
 * @since 1.0.0
 */
public class CustomClassLoader extends ClassLoader {

    /**
     * 类根目录
     */
    private String rootPath;

    public CustomClassLoader(String rootPath){
        this.rootPath = rootPath;
    }

    /**
     * 重写findClass 方法
     * @param className
     * @return
     */
    @Override
    protected Class<?> findClass(String className) {

        String classPath = getClassPath(className);

        try(
            InputStream inputStream = new FileInputStream(new File(classPath));
            ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream()
            ) {

            int i = 0;
            while ((i = inputStream.read()) != -1) {
                byteOutputStream.write(i);
            }
            byte[] bytes = byteOutputStream.toByteArray();
            
            // 将IO字节码数据传入defineClass ,并返回对应的class
            return defineClass(className, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 获取加载类完整路径
     * @param className 类名
     * @return
     */
    private String getClassPath(String className){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(rootPath).append(className.replace('.', File.separatorChar)).append(".class");
        return stringBuilder.toString();
    }
}
  • 编写程序入口进行测试
public static void main(String[] args) throws Exception {
        // 使用自定义类加载器装载HelloWorld 类
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\Administrator\\Desktop\\");
        // 加载类名为HelloWorld的类
        Class<?> clazz = customClassLoader.loadClass("HelloWorld");

        // 获取对象实例
        Object o = clazz.newInstance();

        // 执行HelloWorld 类中的  sayHi 方法
        Method method = clazz.getMethod("sayHi", String.class);
        method.invoke(o,"world");

        // 打印加载HelloWorld.class 的ClassLoader
        System.out.println("ClassLoader is :" + clazz.getClassLoader());


    }
  • 执行结果
hello~ world

ClassLoader is :com.hzh.test.CustomClassLoader@4554617c

通过执行结果可以看到,HelloWorld类中sayHi 方法已被正常执行,说明该类已被正常加载。通过 System.out.println("ClassLoader is :" + clazz.getClassLoader()); 这行代码也验证了HelloWorld类的确是通过我们自定义加载器所加载出来的。

5.4 ClassLoader 双亲委派机制

在ClassLoader的种类小节中写到,ClassLoader 是有多种的,不同的ClassLoader 的加载路径和方式必然有所不同。为了实现不同ClassLoader的分工,各自负责各自的区块,使得逻辑更加的明确,让它们相互协作,形成一个整体,双亲委派机制由此而生了。

使用双亲委派机制的最大的作用是保证一个类在内存中只保留一份字节码,即一个类不论交由哪个类加载器去执行,该类都只会被同一个类加载器所执行。保证最后得到的都是同一个类对象。

双亲委派机制执行流程图

双亲委派机制是自底而上检查当前类是否已被加载,自顶而下去尝试是否能够加载当前类的。当一个类被当前类加载器所接收,类加载器首先会到自身缓存中查找是否已被加载过了,如果该类没被加载,那么当前类加载器则会将该类交由上级加载器去检查加载,以此类推直至Bootstrap ClassLoader检查该类并未加载过,那么Bootstrap ClassLoader会尝试去加载该类,如无法加载则交由下级去加载,以此类推。如果最底层的类加载器也无法加载该类,程序则会抛出ClassNotFountException。

5.5 loadClass与forName 的区别

在Java中类的加载主要分为隐式加载和显示加载两种方式,隐式加载通过new关键字,也就是开发中常用的new对象的方式加载,该方式在日常都比较熟悉,便不再继续探讨。在显示加载中可以通过loadClassforName两种方式进行类的加载,那么这两种类加载的方式又有什么区别?

在类的加载过程中,JVM会分成加载、链接、初始化三步进行:

  • 加载:通过ClassLoad加载class文件字节码,生成class对象
  • 链接:在这一步中首先会去校验检查加载的class的正确性和安全性,然后进入准备阶段为类变量分配存储空间并设置类变量初始值,最后进入解析阶段JVM将常量池内的符号引用转换为直接引用
  • 初始化:执行类变量赋值和静态代码块

在ClassLoad.loadClass源码中,loadClass并不会去链接和初始化类,仅仅只是对类进行了加载,而Class.forName中,通过该方法得到的类是已经初始化好了的,有兴趣的同学可以通过源码查看具体的实现

6. JVM 内存结构模型

JVM 内存结构模型图

JVM 内存结构模型其实指的就是JVM 组成中的Runtime Data Area部分。Java 程序运行在JVM 之上,运行时需要内存空间。JVM 在执行Java程序的过程中,为了方便管理,JVM 会把自身管理的内存空间划分为不同的数据区域。

如上图所示,运行时数据区(Runtime Data Area)主要分为Method AreaHeapStackNative Method StackPC Register 五个数据区域。从线程划分的角度上去看可以分为线程私有线程共享两大类:

  • 线程私有:StackNative Method StackPC Register
  • 线程共享:Method AreaHeap
6.1 程序计数器(PC Register)

每个线程都对应着一个程序计数器 ,为线程私有的。程序计数器的主要作用是用来记录当前线程所执行字节码的行号位置,在程序执行的过程中通过改变计数器的行号指向来获取下一条字节码的执行指令,如:循环、跳转、异常、线程恢复等功能。由于程序计数器只是对当前线程执行位置的记录,所占用的内存很小,因此该数据区域不会出现内存溢出的情况。另外,当线程执行带有native 关键字的方法时,这个计数器则是Undefined,原因很简单,因为native 方法并不属于Java程序,所以不归Java管。

6.2 Java虚拟机栈(Stack)

Java 虚拟机栈也是线程私有的,可以理解为方法执行时的一个内存模式。每个方法在执行时都会创建一个栈帧(Stack Frame),里面包含了方法执行的基础数据结构,如:局部变量表(Loacal Variable Table)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)和方法返回地址(Return Address)等信息。

6.3 本地方法栈(Native Method Stack)

本地方法栈与Java虚拟机栈类似,工作机制原理相似,不同的是该区域主要作用于带有native关键字的方法

6.4 运行时数据区(Method Area)

方法区是Java虚拟机中一个共享的内存区域,随虚拟机启动而创建,主要用于存储已被加载的类信息、常量池、静态变量、即时编译后的代码等关键数据。这一区域虽逻辑上属于堆的一部分,但为了明确区分,常被称为Non-Heap(非堆)。它承载着所有已定义方法的详细信息,一旦方法区的内存分配请求无法得到满足,系统则会抛出OutOfMemoryError

早期,方法区常被通俗地称为“永久代”(Permanent Generation space),但在JDK 8及以后版本中,永久代已被元空间(Metaspace)概念取代,意味着“永久代”这一术语不再适用。若遇到提示

java.lang.OutOfMemoryError: PermGen space

,这表明为永久代分配的内存不足以应对当前应用的需求,常见于启动时加载大量第三方库或在单一Tomcat服务器上部署过多应用的场景,以及频繁动态生成并加载类的情况,从而导致Perm区耗尽。
针对此类内存溢出问题,传统上通过调整MaxPermSize参数大小来解决,但鉴于永久代已被元空间取代,现代实践中应关注调整与元空间相关的参数,如

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

,以避免因元空间不足引发的内存问题。

6.5 堆(Heap)

堆是JVM管理的内存中最大的一部分,Java堆是被所有线程共享的一块,在虚拟机启动时创建,是对象实例的分配区域,其主要职责是存放程序运行过程中创建的对象实例和数组。当使用new关键字创建对象时,该对象就存储在堆中。

在内存布局方面堆内存区虽然逻辑上是连续的,但物理上Java堆可以不连续。现代JVM通常实现为可扩展的,这意味着堆的大小可以在运行时动态调整。

此外,该区域是垃圾回收(GC)的主要作用区,由于堆内存的分配是动态的,即在程序运行期间,JVM会根据需要自动分配内存给新创建的对象,并通过垃圾回收器(GC)自动回收不再使用的对象所占用的内存空间,以实现内存的有效管理和重用。

7. 最后

本篇通过JVM 定义、Java 的平台无关性、JVM 生命周期、JVM 架构模型、ClassLoader 类加载器以及JVM 内存结构模型六大部分快速简单了解了Java虚拟机的原理与组成,后面我将基于此继续展开详写JVM的内容。下一遍将详细介绍JVM中另一个内容:垃圾回收算法(GC),欢迎关注后续~


关注公众号,内容更多更及时~

本文由mdnice多平台发布

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值