谈谈对 JVM 的理解

谈谈对 JVM 的理解

JVM 的体系结构概述

在这里插入图片描述
在这里插入图片描述

什么是类加载器?

    负责加载 class 文件,class 文件在 文件开头有特定的文件标示,将 class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构而且 ClassLoader 只负责 class 文件的加载,至于它是否能够运行,则由 Execution Engine 决定
在这里插入图片描述java

三大类加载器

  • 根加载器(Bootstrap)
  • 扩展类加载器(Extension)
  • 应用程序类加载器(AppClassLoader)
    在这里插入图片描述
package jvm;

/** * @author Woo_home * @create by 2020/3/18 */
public class HelloDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        HelloDemo hello = new HelloDemo();
        // 获取当前类加载器
        System.out.println(obj.getClass().getClassLoader());
        // 获取当前类加载器
        System.out.println(hello.getClass().getClassLoader());
        // 获取当前类加载器的父加载器
        System.out.println(hello.getClass().getClassLoader().getParent());
    }
}

输出为:
null
    Object 为何是 null 呢?其实这里的 null 指的就是 Bootstrap(启动类加载器),而 Object 又是 Java 自带的,因此这里输出为 null 是由于 Object 没法获取自己(类加载器)web

    而 HelloDemo 是本身建立的,因此获取到的类加载器是 AppClassLoader,由于 AppClassLoader 的父加载器是 ExtentionClassLoader,因此 hello.getClass().getClassLoader().getParent() 的时候就会输出 ExtClassLoader编程

sun.misc.Launcher

    咱们从输出的结果能够发现类加载器前面带着一个 sun.misc.Launcher,这是个什么东东呢?咱们都知道 Java 的 main 方法是一切程序的入口方法,而 Launcher 则是一个 Java 虚拟机的入口应用安全

双亲委派

    当一个类收到了类加载请求,它首先不会尝试本身去加载这个类,而是把这个请求委派给本身的父类去完成,每个层次类加载器都是如此,所以全部的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈本身没法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试本身去加载数据结构

采用双亲委派有什么好处?

    采用双亲委派的一个好处是好比加载位于 rt.jar 包中的类 java.lang.Object 时,不论是哪一个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不一样的类加载器最终获得的都是一样一个 Object 对象jvm

沙箱安全机制

咱们建一个 java.lang 这么一个包,而后在这个包下新建一个 String 类,代码以下:编程语言

package java.lang;

/** * @author Woo_home * @create by 2020/3/18 */
public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

这个代码很简单,运行该程序
在这里插入图片描述
从输出结果能够发现,报错说咱们没有编写 main 方法,可是咱们的代码明明编写了 main 方法,为何呢?svg

    说明咱们虽然编写了 String 这么个类,可是程序并无运行这个 String 类。按道理来讲咱们自定义的的类是由应用程序类加载器(AppClassLoader)去加载的,而咱们刚刚学过双亲委派机制,当一个类收到了类加载请求,它首先不会尝试本身去加载这个类,而是把这个请求委派给本身的父类去完成函数

    因此运行 String 这个类的时候首先就是由启动类加载器去加载(Bootstrap),而启动类加载器(Bootstrap)会先去加载 rt.jar 下的 java.lang.String 这个类,这个类是 Java 自带的,为了避免让咱们去修改这个源代码,因此就设置了一个沙箱安全机制(保证对代码的有效隔离,防止对本地系统形成破坏spa

Execution Engine

如执行命令

javac Hello.java
java Hello

Native Interface 本地接口

    本地接口的做用是融合不一样的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序,Java 诞生的时候是 C/C++ 横行的时候,要想立足,必需要调用 C/C++ 程序,因而就在内存中专门开辟了一块区域处理标记为 native 代码,它的具体作法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行是加载 native libraies
    目前该方法使用的愈来愈少了,除非是与硬件有关的应用,好比经过 Java 程序驱动打印机或者 Java 系统管理生产设备,在企业级应用中已经比较少见。由于如今的异构领域间的通讯很发达,好比可使用 Socket 通讯,也可使用 Web Service 等等

Native Method Stack 本地方法栈

    它的具体作法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库

Method Area 方法区

    供各线程共享的运行时内存区域。它存储了每个类的结构信息,例如运行时的常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。方法区是个规范,方法区在不一样的虚拟机里实现是不同的,最典型的就是永久代(PermGen space)和元空间(Metaspace)

实例变量存在堆内存中,和方法区无关

程序计数器

    每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个很是小的内存空间

    这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器经过改变这个计数器的值来选取下一条须要执行的字节码指令。

    若是执行的是一个 Native 方法,那这个计数器是空的

    用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误

Java 栈

    栈也叫栈内存,主管 Java 程序的运行,是在线程建立时建立,它的生命周期是跟随线程的声明周期,线程结束栈内存也就释放了,对于栈来讲不存在垃圾回收问题,只要线程一结束该栈就 Over,生命周期和线程一致,是线程私有的。8 种基本类型的变量 + 对象的应用变量 + 实例方法都是在函数的栈内存中分配

栈存储什么?

栈帧中主要保存 3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量
  • 栈操做(Operand Stack):记录出栈、入栈的操做
  • 栈帧数据(Frame Data):包括类文件、方法等

Heap 堆

    一个 JVM 实例只存在一个堆内存,堆内存的大小是能够调节的。类加载器读取了类文件以后,须要把类、方法、常变量放到堆内存中,保存全部引用类型的真实信息,以方便执行器执行,堆内存分为三个部分

  • Young Generation Space 新生区 Young / New
  • Tenure Generation Space 老年区 Old / Tenure
  • Permanent Space 永久区 Perm

在 Java 8 中永久区改成了元空间