谈谈对 JVM 的理解
JVM 的体系结构概述
什么是类加载器?
负责加载 class 文件,class 文件在 文件开头有特定的文件标示,将 class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且 ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定
三大类加载器
- 根加载器(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());
}
}
输出为:
Object 为什么是 null 呢?其实这里的 null 指的就是 Bootstrap(启动类加载器),而 Object 又是 Java 自带的,所以这里输出为 null 是因为 Object 无法获取本身(类加载器)
而 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 对象
沙箱安全机制
我们建一个 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 方法,为什么呢?
说明我们虽然编写了 String 这么个类,但是程序并没有运行这个 String 类。按道理来说我们自定义的的类是由应用程序类加载器(AppClassLoader)去加载的,而我们刚刚学过双亲委派机制,当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给自己的父类去完成
所以运行 String 这个类的时候首先就是由启动类加载器去加载(Bootstrap),而启动类加载器(Bootstrap)会先去加载 rt.jar 下的 java.lang.String 这个类,这个类是 Java 自带的,为了不让我们去修改这个源代码,所以就设置了一个沙箱安全机制(保证对代码的有效隔离,防止对本地系统造成破坏)
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 中永久区改为了元空间
转载:https://blog.csdn.net/Woo_home/article/details/104919149