JVM

大兔子大兔子 提交于 2019-12-07 12:52:28

一、JVM一些基本概念

1、JVM和普通虚拟机

JVM:Java Virtual Machine,程序自己独立的运行环境;堆栈、寄存器、字节码指令;可以运行多种语言:Java、Scala、Grovvy;
普通虚拟机:能完整提供虚拟主机的PC,必须安装操作系统,以CPU指令运行。例如VMWare、Visual Box

2、JVM/JDK/JRE关系

JVM :(Java Virtual Machine),只认识 class文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心。
JRE :(Java Runtime Environment),Java 运行时环境。它主要包含 jvm 的标准实现和 Java 的一些基本类库。JRE = JVM + Java 类库。
JDK :(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。

显然,这三者的关系是:一层层的嵌套关系  JDK>JRE>JVM 

3、JVM产品有哪些

HotSpot、Jrockit、J9

4、为什么会出现JVM

Java程序编译后的文件是*.class文件,*.class文件是按照Java标准编译的文件,JVM是实现了Java制定的标准,因此JVM是可以运行Java程序的,而JVM是一个虚拟出来的机器,通过自定义的执行引擎、接口等实现方式与实际机器各种交互,使得Java程序在运行过程与实际机器无耦合,从而实现跨平台。

二、JVM结构

  • 类加载器
  • 执行引擎
  • 运行时数据区
  • 本地接口

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

三、类加载器以及双亲委派模型

类加载器:将Java字节码文件(遵循双亲委派模型)加载到运行时数据区

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器

双亲委派模型的工作过程是:

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。

  • 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。

  • 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

双亲委派模型并不是继承关系,注意这个坑。

作用

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系

例如类java.lang.Object,它由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并用自定义的类加载器加载,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

结构

系统提供的类加载器

在双亲委派模型的定义中提到了“启动类加载器”。包括启动类加载器,绝大部分Java程序都会使用到以下3种系统提供的类加载器:

  • 启动类加载器(Bootstrap ClassLoader)

负责将存放在<JAVA_HOME>/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机按照文件名识别的(如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

JDK中的常用类大都由启动类加载器加载,如java.lang.String、java.util.List等。需要特别说明的是,启动类Main class也由启动类加载器加载。

  • 扩展类加载器(Extension ClassLoader)

sun.misc.Launcher$ExtClassLoader实现。

负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader)

sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。

它负责加载用户类路径ClassPath上所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

启动类Main class、其他如工程中编写的类、maven引用的类,都会被放置在类路径下。Main class由启动类加载器加载,其他类由应用程序类加载器加载。

自定义的类加载器

JVM建议用户将应用程序类加载器作为自定义类加载器的父类加载器。则类加载的双亲委派模型如图:

实现原理

实现双亲委派的代码都集中在ClassLoader#loadClass()方法之中:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查目标类是否已在当前类加载器的命名空间中加载
            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) {
                    // 如果仍然加载失败,则自己加载
                    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) {
                // 默认resolve取false,不需要解析,直接返回。
                resolveClass(c);
            }
            return c;
        }
    }
  • 首先,检查目标类是否已在当前类加载器的命名空间中加载。

  • 如果没有找到,则尝试将请求委托给父类加载器(如果指定父类加载器为null,则将启动类加载器作为父类加载器;如果没有指定父类加载器,则将应用程序类加载器作为父类加载器),最终所有类都会委托到启动类加载器。

  • 如果父类加载器加载失败,则自己加载。

  • 默认resolve取false,不需要解析,直接返回。

 四、类加载过程

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现这个类进行初始化。

类加载过程只是一个类生命周期的一部分,在之前就有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;

1、加载

是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。

(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。

(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。

(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。

2、连接

(1)验证阶段:确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。

(2)准备阶段:负责为类的静态成员分配内存,并设置默认初始值。

(3)解析阶段:将类的二进制数据中的符号引用替换为直接引用。

3、初始化

初始化 -- 则是执行静态变量、静态代码块

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块<可写不可读>,则按照自上而下的顺序依次执行。

五、运行时数据区

1、程序计数器

  • 简述:

  这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  如果线程正在执行的是一个Java方法这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined。

  • 作用:

1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
2、在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。

  • 特点:

1、是一块较小的内存空间。
2、线程私有,每条线程都有自己的程序计数器。
3、生命周期:随着线程的创建而创建,随着线程的结束而销毁。
4、是唯一一个不会出现 OutOfMemoryError的内存区域。

  • 异常规定:
      如果线程正在执行Java中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是Native方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域。

2、方法区

  • 简述:
      Java 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息:
      已经被虚拟机加载的类信息常量静态变量即时编译器编译后的代码

  • 特点:

1、线程共享。 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
2、永久代。 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。HotSpot中,方法区≈永久代。不过JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念了,会采用Native Memory来实现方法区的规划了。
3、内存回收效率低。 方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。
4、Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。

  • 异常规定:OutOfMemoryError

3、 虚拟机栈

  • 简述:Java 虚拟机栈是描述 Java 方法运行过程的内存模型。它的生命周期与线程相同。

  Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

  • 压栈出栈过程

  当方法运行过程中需要创建局部变量时,就将局部变量的值存入栈帧中的局部变量表中。Java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。

  • 特点:

1、局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
2、Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
3、StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
4、OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
5、Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。

  • 异常规定:StackOverflowError、OutOfMemoryError

1、如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出StackOverflowError异常。
2、如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!