简述JVM——类加载器和运行时数据区

Posted KookNut39

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简述JVM——类加载器和运行时数据区相关的知识,希望对你有一定的参考价值。

大家好,我是KookNut39也是Tommy,在CSDN写文,写Java时候我是Tommy,分享一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。感兴趣的欢迎关注博主,和博主一起学习Java知识。大家还可以去专栏查看之前的文章,希望未来能和大家共同探讨技术。

了解JVM(Java虚拟机)首先我们必须了解VM(虚拟机)是什么。

所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

我们经常见到的 VMware 就属于系统虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。

程序虚拟机典型的代表就是 Java 虚拟机了,它专门为执行某个单个计算机程序而设计。在 Java 虚拟机中执行的指令我们称为 Java 字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源。Java 虚拟机是一种执行 Java 字节码文件的虚拟计算机,它拥有独立的运行机制。

Java 技术的核心就是 Java 虚拟机,因为所有的 Java 程序都运行在 Java 虚拟机内部。

JVM概述

JVM就是Java虚拟机,虚拟机就是一台虚拟的计算机,是一款软件。Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释或编译为对应平台上的机器码指令执行,每一条 Java 指令,Java 虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿等。JVM是运行在操作系统上的,不与硬件直接交互。

JVM整体的四个部分

JVM整体组成可以分为4个部分:

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地方法接口(Native Interface)

程序在执行之前先要把 Java 代码转换成字节码(.class 文件),JVM 首先需要把字节码通过一定的类加载器(Class Loader)把文件加载到内存中运行时数据区(Runtime Data Area),而字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器(执行引擎(Execution Engine)) 将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程中需要调用其他语言的接口(本地方法接口(Native Interface)) 来实现。整个程序的功能,这就是这 4 个主要组成部分的职责与功能。

1.1 类加载器

Java编译器会为虚拟机转换源指令。虚拟机代码存储在以 .class 为扩展名的类文件中,每个类文件都包含某个类或者接口的定义和实现代码。

​ ——Java核心基础(卷二)

1.1.1 类加载器过程

类加载器子系统负责从文件系统或者网络中加载 class 文件, class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头)。classLoader 只负责 class 文件的加载,至于它是否可以运行,则由执行引擎决定。加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 class 文件中常量池部分的内存映射)。

类加载的过程

1.加载过程是把class文件(字节码文件)加载到内存中(I/O读写)。类加载器把文件加载到内存中,会为每个类创建一个Class类的对象,调用Class类中的方法获取类的相关信息。

2.验证是检验加载的类是否有正确的内部结构并和其他类协调一致。

3.准备阶段为类的静态属性分配内存,并设置默认初始值(不包含final修饰的static常量),也不会给实例变量初始化。

4.解析是将二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针)。

5.类初始化

​ 5.1 什么时候初始化类

​ 1 )创建类的实例,也就是 new 一个对象
​ 2)访问某个类或接口的静态变量,或者对该静态变量赋值
​ 3)调用类的静态方法
​ 4)反射
​ 5)初始化一个类的子类(会首先初始化子类的父类)

​ 5.2 类的初始化顺序:

​ 父类static --> 子类static --> 父类构造方法 --> 子类构造方法

1.1.2类加载器的分类

1.1.2.1启动类加载器(引导类加载器)

是由c/c++实现,用来加载Java的核心类库

1.1.2.2扩展类加载器

由Java实现,派生于 ClassLoader 类。上层类加载器是引导类加载器(启动类加载器),加载底层类库

1.1.2.3应用程序类加载器

Java实现,派生于 ClassLoader 类。上层类加载器是扩展类加载器,加载自定义类。

1.1.3 双亲委派机制

加载类时,向上委派,交给最上层的加载器启动类加载器加载核心类库,加载不到就用扩展类加载器加载底层类库,加载不到就用应用程序类加载器加载自定义类

双亲委派机制的优点

  1. 安全,可避免用户自己编写的类动态替换 Java 的核心类(自定义的类名和Java核心类名相同)
  2. 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加载)

1.1.4沙箱安全机制

作用是防止恶意代码污染 Java 源代码

如果一个类在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的类,后面的一概不能使用,这就保证了不被恶意代码污染。

1.1.5 类的主动使用和被动使用

JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就有被动使用。
主动使用:

  1. 通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,
    他肯定会导致类的加载并且初始化
  2. 访问类的静态变量,包括读取和更新
  3. 访问类的静态方法
  4. 对某个类进行反射操作,会导致类的初始化
  5. 初始化子类会导致父类的的初始化
  6. 执行该类的 main 函数

被动使用:其实除了上面的几种主动使用其余就是被动使用了

注意:主动使用和被动使用的区别在于类是否会被初始化.

  1. 引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致初始化,比如:
    public final static int number = 5 ; //不会导致类初始化,被动使用
    public final static int random = new Random().nextInt() ; //会导致类的初始化,主动使用
  2. 构造某个类的数组时不会导致该类的初始化,比如:
    Student[] students = new Student[10] ;

1.2 运行时数据区

JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

1.2.1 程序计数器(Program Counter Register)

JVM 中的程序计数寄存器中的 Register 命名源于CPU 的寄存器,寄存器存储指令相关的现场信息。这里,并非是广义上所指的物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)会更加贴切(也称为程序钩子)。JVM 中的PC 寄存器是对物理 PC 寄存器的一种抽象模拟。

  1. 它是一块很小的内存空间,几乎可以忽略不计,但是它是运行速度最快的存储区域。
  2. 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致。
  3. 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。如果是在执行native方法,则是未指定值(undefined)。
  4. 它是程序控制流的指示器,分支、循环、跳转、异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
  5. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  6. 它是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域.

那么为什么使用程序计数器记录当前线程的执行地址呢?我们都知道CPU需要不停的切换各个线程,这时候从其他线程切换到这个线程时,根据程序计数器就知道该从哪里开始执行了。JVM 的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令,由于这个原因,必须为每个线程分配一个程序计数器,这样各个线程就可以互不干扰。

1.2.2 Java 虚拟机栈(Java Virtual Machine Stacks)

首先我们需要学会区分栈和堆:

栈是运行时的单位,而堆时存储的单位。

  1. 栈:解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  2. 堆:解决的是数据存储的问题,即数据怎么放,放在哪儿。
1.2.2.1 Java虚拟机栈概述

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用。Java 虚拟机栈是线程私有的。生命周期和线程一致。栈中的数据都以栈帧为单位存储。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

作用:主要负责Java程序的运行,保存方法内的局部变量,还有部分结果,还参与方法的调用和返回。

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM 直接对 Java 栈的操作只有两个:

  1. 调用方法,进栈。
  2. 执行结束后出栈。

注意:对于栈来说不存在垃圾回收问题。

1.2.2.2栈的运行原理
  1. JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循”先进后出“后进先出的原则。
  2. 在一条活动的线程中,一个时间点上,只会有一个活动栈。即只有当前在执行的方法的栈帧(此时这个栈桢在栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class)。
  3. 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  4. 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。
  5. 不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。
  6. 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常。不管哪种方式,都会导致栈帧被弹出.

1.2.2.3 栈帧的内部

每个栈帧中都有:局部变量表、操作数栈、动态链接、方法返回地址、一下附加信息。

1.2.2.3.1局部变量表(Local Variables)

局部变量表用于存放方法参数和方法内部定义的局部变量。

对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

1.2.2.3.2 操作数栈(Operand Stack)(或表达式栈)

程序中的所有计算过程都是在借助于操作数栈来完成的。

1.2.2.3.3 动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)

因为在方法执行的过程中有可能需要用到类中的常量或方法,所以必须要有一个引用指向运行时常量池。

1.2.2.3.4 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

1.2.2.3.5 一些附加信息

例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。

1.2.3 本地方法栈

  1. Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用。

  2. 本地方法栈也是线程私有的.

  3. 允许被实现成固定或者是可动态扩展的内存大小。

内存溢出方面也是相同的。
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,会抛出StackOverflowError。
如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出OutOfMemoryError。

  1. 本地方法是用 C 语言写的.

1.2.4 堆内存

堆内存概述:

  1. 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域。
  2. Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间。
  3. 堆内存的大小是可以调节。一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。
  4. 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。
  5. 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区。
  6. 《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上。
  7. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
  8. 堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

我们在之后在细讲堆内存的区域划分及垃圾回收机制

1.2.5方法区

方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据、static final 常量、static 变量、编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。所以,方法区看做是一块独立于 Java 堆的内存空间。方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的。
方法区的大小:跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误 :java.lang.OutOfMemoryError:Metaspace。

结语

这次就先写这些,文中如果存在不对的地方,欢迎各位读者批评指正。我会在今后更新本地方法接口执行引擎垃圾回收机制等相关内容,如果大家感兴趣,可以关注博主,我们一起交流学习。

本次的分享到这里就结束了,码字不易,如果喜欢,赏个点赞+评论+收藏🤞🤞🤞,感谢您的支持

以上是关于简述JVM——类加载器和运行时数据区的主要内容,如果未能解决你的问题,请参考以下文章

简述JVM——类加载器和运行时数据区

「JAVA」Java基础之堆栈方法区类加载器——JVM内存模型分析

JVM启动过程

JVM运行和类加载的全过程

JVM | 类加载器和双亲委派模型

探秘类加载器和类加载机制