Java类加载机制与JVM运行时数据区各逻辑内存区域与JDK的版本相关差异浅谈

Java 类加载机制与 JVM 运行时数据区各逻辑内存区域与 JDK 的版本相关差异浅谈

【摘要】

JVM(Java Virtual Machine)作为Java研发人员工作的每天都会接触到的虚拟机,其运行机制与底层原理想必大家都略知一二,今天我将从初学者的角度出发,结合甲骨文官方的技术文档,对部分Java虚拟机的相关知识做一些简单的梳理。

(Oracle官方文档地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2)

目录

一 JVM(Java Virtual Machine)简介... 3

1.1 源码文件到.class文件的过程... 3

1.2 如何阅读字节码文件... 3

二 类加载过程... 4

2.1 装载... 4

2.2 链接... 4

2.2.1 验证:保证被加载类的正确性... 4

2.2.2 准备... 5

2.2.3 解析... 5

2.3 初始化... 5

三 类加载器与双亲委派模型... 5

3.1 类加载器基础概念... 5

3.2 双亲委派机制基础概念... 6

3.3 优势... 6

3.4 安全性... 6

3.5 如何破坏双亲委派模型... 7

四 运行时数据区... 8

4.1 方法区... 8

4.2 堆... 9

4.3 Java虚拟机栈... 9

4.3.1 栈帧... 10

4.3.2 理解栈帧... 11

4.4 程序计数器... 12

4.5 本地方法栈... 12

五 参考文献... 12

一 JVM(Java Virtual Machine)简介

Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有其自己相相应的指令系统。

Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是**"一次编译,多次运行"。**

图1:JDK与JRE关系图

1.1 源码文件到.class文件的过程

使用javac编译 Person.java--->Person.class

Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器-> 注解抽象语法树 -> 字节码生成器 -> Person.class文件

1.2 如何阅读字节码文件

甲骨文官方文档中有说明解释(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1),文档中对字节码.class文件做了如下解释:

A class file consists of a single ClassFile structure:

复制代码
ClassFile {
复制代码
    u4             magic;
复制代码
    u2             minor_version;
复制代码
    u2             major_version;
复制代码
    u2             constant_pool_count;
复制代码
    cp_info        constant_pool[constant_pool_count-1];
复制代码
    u2             access_flags;
复制代码
    u2             this_class;
复制代码
    u2             super_class;
复制代码
    u2             interfaces_count;
复制代码
    u2             interfaces[interfaces_count];
复制代码
    u2             fields_count;
复制代码
    field_info     fields[fields_count];
复制代码
    u2             methods_count;
复制代码
    method_info    methods[methods_count];
复制代码
    u2             attributes_count;
复制代码
    attribute_info attributes[attributes_count];
复制代码
}

The items in the ClassFile structure are as follows:magic

The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.

说明:编译后的字节码文件通过 16进制 编码后就形成了可阅读的格式,参照官方文档的说明:u4 代表的含义就是前8 位16 进制数,含义即文件格式magic 的值为:0xCAFEBABE

二 类加载过程

2.1 装载

(1) 通过一个类的全限定名获取定义此类的二进制字节流

(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

(3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

2.2 链接

2.2.1 验证:保证被加载类的正确性

文件格式验证/元数据验证/字节码验证/符号引用验证

2.2.2 准备

为类的静态变量分配内存,并将其初始化为默认值(比如整型此时初始化为默认值0)

2.2.3 解析

把类中的符号引用转换为直接引用(指向真实的内存物理地址

2.3 初始化

对类的静态变量、静态代码块执行初始化操作。(赋予真实的变量值)

图2:类加载过程

三 类加载器与双亲委派模型

3.1 类加载器基础概念

图3:三种类加载器的层级关系

3.2 双亲委派机制基础概念

定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

图4:不同加载器负责的目录范围和功能的差异

3.3 优势

Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的

Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

3.4 安全性

这种模式也一定程度上出于安全考虑,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛java.lang.ClassCaseException的异常。

3.5 如何破坏双亲委派模型

我们来看看Java的源码内容:

复制代码
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;
复制代码
        }
复制代码
    }}

这是public abstract class ClassLoader这个类中的loadClass方法代码片段的粘贴,我们从标记红色的代码块可以看到它寻找父类加载器的方法,显而易见,只要我们在自己的类加载器中重写这个 loadClass 方法,就可以破坏类加载的双亲委派机制

四 运行时数据区

图5:运行时数据区示意图

4.1 方法区

官方文档原文:

翻译:

简单总结:

(1) 方法区是各个线程共享的内存区域,在虚拟机启动时创建

(2) 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

(3) 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap (非堆),目的是与Java堆区分开来

(4) 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

(5) 方法区在JDK 8 中就是Metaspace 【元空间】,

在JDK6 或7 中就是Perm Space 【永久代】。

(6) Run-Time Constant Pool (常量池)在方法区分配

4.2 堆

官方文档原文:

懒得贴了,有兴趣的话给大家网址自己去看吧

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2)

翻译:(勉强贴一下)

总结:

(1) 堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享

(被多个线程共享,首先想到的就是这部分的数据是线程不安全的,堆的生命周期跟虚拟机的生命周期一致)。

(2) Java 对象实例以及数组都在堆上分配。

(3) 当堆无法满足内存分配需求时,将抛出OutOfMemoryError异常。

4.3 Java虚拟机栈

官方文档原文:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2)

翻译:

懒得翻译了,大家自行谷歌,不太想凑字数。

总结:

Java虚拟机栈

(1) 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是f,独有的,随着线程的创建而创建。

(2) 每一个被线程执行的方法,为该栈中的栈帧,即每个方法的执行对应一个栈帧

(3) 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出

4.3.1 栈帧

每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接、方法返回地址(Return Address)和附加信息。

(方法返回地址:记录上一级方法调用该方法后回调的行数位置)

举例:有如下方法:

图6:一个三层链式调用的方法

那么对应的栈帧操作过程就是:

图7:Java虚拟机栈对应的栈帧操作过程

4.3.2 理解栈帧

源码文件:

编译后的字节码文件含义(顺序执行):

图8:虚拟机栈示意图

4.4 程序计数器

官方文档原文:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2)

别问,问就是自己翻译。

一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置

(1)程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。

(2)如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。

(3)如果正在执行的是Native方法,则这个计数器为空。

4.5 本地方法栈

官方文档原文:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2)

嘿嘿嘿,鸡汤来咯!

一句话明白本地方法栈:如果当前线程执行的方法是 Native 类型的,这些方法就会在本地方法栈中执行。

五 参考文献

1. 《The Structure of the Java Virtual Machine》https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.2

2. 《Java 语言和虚拟机规范》

https://docs.oracle.com/javase/specs/index.html

3. 《The class File Format》

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1

4. 《Java ® 虚拟机 规范》

https://docs.oracle.com/javase/specs/jvms/se10/html/index.html