淘先锋技术网

首页 1 2 3 4 5 6 7

一、JVM主要组成部分及其作用

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载器)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地库接口)。在这里插入图片描述

  • Class loader(类加载器):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区中的方法区;

  • Execution engine(执行引擎):执行引擎也叫解释器,负责解释命令,交由操作系统执行;

  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存,我们所有所写的程序都被加载到这里,之后才开始运行。

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

二、类加载器

类加载器负责动态加载类,在运行时(而非编译时),当一个类初次被引用的时候,它将被加载、链接、初始化。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在JVM中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将class文件加载到JVM内存,然后再转化为class对象。

但是人生不如意事十之八九,有些情况不得不违反这个约束,例如JDBC

你先得知道SPI(Service Provider Interface),这玩意和API(Application Programming Interface)不一样,它是面向拓展的,也就是我定义了这个SPI,具体如何实现由扩展者实现。我就是定了个规矩。

JDBC就是如此,在rt里面定义了这个SPI,那mysql有mysql的jdbc实现,oracle有oracle的jdbc实现,反正我java不管你内部如何实现的,反正你们都得统一按我这个来,这样我们java开发者才能容易的调用数据库操作。

所以因为这样那就不得不违反这个约束啊,Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到<JAVA_HOME>lib中,其他的它无能为力。这就违反了自下而上的委托机制了。

Java就搞了个线程上下文类加载器,通过setContextClassLoader()默认情况就是应用程序类加载器,然后Thread.current.currentThread().getContextClassLoader()获得类加载器来加载。

2.1 加载(Load)

类加载子系统主要有三个具体的类加载器,包括启动类加载器(BootStrap ClassLoader), 扩展加载器(Extension ClassLoader),应用加载器(Application ClassLoader)

  • 启动类加载器 (BootStrap ClassLoader) –是虚拟机自身的一部分,C++实现,负责从 bootstrap classpath 中加载类(J<JAVA_HOME>jrelib目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库),有且只有一个 rt.jar 文件,该加载器具有最高优先级;
  • 扩展加载器(Extension ClassLoader) – 它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>jrelibext目录中或被java.ext.dirs系统变量所指定的路径的类库;
  • 应用加载器(Application ClassLoader)– 它是Java实现的,独立于虚拟机,负责从用户定义的 classpath 中加载类,用户可以通过指定环境变量的方式定义该目录,如: java -classpath。一般情况下,如果没有自定义类加载器默认就是用这个加载器。

以上类加载器通过双亲委派模型执行类加载:
在这里插入图片描述
当一个类加载器收到类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载类无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

2.2 链接(Link)

  • 校验(Verify) – 字节码验证器会验证生成的字节码是否正确,如果校验失败,会返回校验错误。
  • 准备(Prepare) – 所有的静态变量会被分配内存。
  • 解析(Resolve) – 虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用可以理解为一个标示,而直接引用是直接指向内存中的地址。

2.3 初始化

初始化是类加载的最后一步,对静态变量和静态代码块执行初始化工作。在这里静态变量会被赋予初值,静态方法区会被执行。

三、运行时数据区

Java虚拟机在运行时会将其内存划分为不同的区域,这些区域都有特定的用途。但顾名思义,总的用途都是存储数据,只是存储的东西不同罢了.

简单的过程可以理解为:我们的源代码文件(.java文件)经过编译生成的字节码文件(.class文件),由class loader(类加载器)加载后交给执行引擎执行。在加载后和执行引擎执行的过程中产生的数据会存储在一块内存区域,这块内存区域就是运行时数据区。

下面我们就来说说这5块区域:

程序计数器(Program Counter Registers)
用于记录当前线程的正在执行的字节码指令位置。由于虚拟机的多线程是切换线程并分配cpu执行时间的方式实现的,不同线程的执行位置都需要记录下来,因此程序计数器是线程私有的。这个区域是唯一一个不会抛出任何异常的区域。

虚拟机栈(Java Threads)
虚拟机栈是线程私有的。虚拟机栈是java方法执行的内存结构,虚拟机会在每个java方法执行时创建一个“栈桢”,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。当方法执行完毕时,该栈桢会从虚拟机栈中出栈。其中局部变量表包含基本数据类型和对象引用;在java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(对于单个线程来说的栈溢出):

public class StackOverFlowTest {

    public void recursionMethod() {
        double a = 20;
        double b = 20;
        recursionMethod();
    }

    public static void main(String[] args){
            StackOverFlowTest stackOverFlowTest = new StackOverFlowTest();
            stackOverFlowTest.recursionMethod();

    }
}

结果:
在这里插入图片描述
如果虚拟机栈可以动态扩展(现在大部分java虚拟机都可以动态扩展),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError异常(没有足够的内存,对于栈中所有的线程来说,整个虚拟机栈已经获取不了再多的内存了,剩下的内存还要给堆、方法区它们呢~)
对于溢出,让我想起了一部电视剧,里面的有一个程序猿和一个程序媛,合理的解释了为什么会溢出(因为满了,所以溢出):
因为满了
所以溢出
他们在策马奔腾的时候还在聊JVM架构,我们有什么理由不好好学呢?

本地方法栈(Native Internal Threads)
本地方法栈是线程私有的。本地方法栈为虚拟机使用的Native方法(本地方法)提供服务,这个Native方法指得就是Java程序调用了非Java代码,算是一种引入其它语言程序的接口。和虚拟机栈类似,本地方法栈也会抛出StackOverFlowException和OutOfMemoryException异常。

方法区(Method Area)
方法区是线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。通常被开发人员成为“永久代”。这个区域的内存回收的目标就是针对常量池的回收和对类型的卸载,也是较为难处理的部分。方法区溢出时会抛出OutOfMemoryException异常。

(Heap)
堆是java虚拟机中内存中最大的一块,被所有线程共享的一块内存区域,在虚拟机创建时创建。作用就是存放对象实例,所有的对象的实例都需要在这里分配内存。几乎所有的对象实例和对象数组都需要在堆上分配。是java虚拟机内存回收的管理的重要区域,因此也被称为**“GC”堆**,可以被分为新生代老年代。新生代又由Eden空间、From Survivor空间、To Survivor空间组成。如果堆中没有内存完成实例分配,并且堆也无法扩展时,则抛出OutOfMemoryException异常。

参考
1、
面试 — 1.2 JVM组成部分以及各个部分的作用
2、JVM的主要组成部分及其作用
3、JVM运行时数据区
4、java 类加载执行的过程