整理多方文档,记录真实的JVM
基础概念
什么是JVM?全称Java Virtual Machine,即Java虚拟机
不同于 C或者C++ ,是直接将代码生成机器指令,CPU去直接执行该部分指令的方式。在Java中需要先生成字节码,JVM再将字节码解码成机器码。
优势:JVM屏蔽了底层平台的差别,可以做到一次编译,再各个平台运行,比如在Windows编译,也可以在Linux运行
缺点:影响性能,这也是Java的性能一般不如C或C++的原因
JVM主要组成部分
![](https://i-blog.csdnimg.cn/blog_migrate/461a65375d8b195a26c4ebb749ac3b2f.png)
(转载百度图片)
作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
类加载器 (ClassLoader)
作用:
负责加载class文件,经过类加载器加载并初始化之后,会得到Class,实例化对象的时候会参考Class。如Person.class经过ClassLoad加载初始化后,会得到Person Class,之后每次实例化对象的时候,都会参考Person Class
加载过程:
![](https://i-blog.csdnimg.cn/blog_migrate/e5a0e3dd975b96422e80f19aac59dff7.png)
JVM类加载过程分为:加载 、链接 、初始化 、使用 、卸载 这五个阶段,其中链接阶段又包括: 验证 、 准备 、 解析 。
分类:
启动类加载器(Bootstrap):主要负责加载jre中的最为基础、最为重要的类。如$JAVA_HOME/jre/lib/rt.jar等。它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。
扩展类加载器(Extension):由Java代码实现,用于加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类。
应用程序类加载器(AppClassLoad):由Java代码实现, 它负责加载应用程序路径下的类。比如Person.java,Car.java等程序员自定义的类。
用户自定义加载器。 其中Bootstrap是Extension的父类加载器,Extension是AppClassLoad的父类加载器。
![](https://i-blog.csdnimg.cn/blog_migrate/5bafff7be270c3b27f961f524ef14c75.png)
双亲委派机制:
当一个类加载器收到了类加载的请求的时候,它不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
搜索 ClassLoader 并进入类中,找到 loadClass() 方法 找到如下源代码:
/**
* Loads the class with the specified <a href="#name">binary name</a>.
* This method searches for classes in the same manner as the {@link
* #loadClass(String, boolean)} method. It is invoked by the Java virtual
* machine to resolve class references. Invoking this method is equivalent
* to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,
* false)</tt>}.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class was not found
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// If still not found, then invoke findClass in order
// to find the class.
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) {
resolveClass(c);
}
return c;
}
}
解释:
若是一个 .class文件需要被加载的时候。
不考虑我们自定义类加载器,首先会在 Appcation ClassLoader 中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到Extension ClassLoader,然后调用Extension ClassLoader加载器的 loadClass() 方法。Extension ClassLoader中同理也会先检查自己是否已经加载过,如果没有再往上。直到到达 Bootstrap ClassLoader 之前,都是在检查是否加载过,并不会选择自己去加载。直到 Bootstrap ClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出 ClassNotFoundException。
优势:
1. 避免类的重复加载
2. 保护程序安全,防止核心API被随意篡改
缺点:
检查类是否加载的委派过程是单向的, 这个方式虽然从结构上说比较清晰,
使各个 ClassLoader 的职责非常明确, 但是同时会带来一个问题, 即顶层的
ClassLoader 无法访问底层的 ClassLoader 所加载的类
通常情况下, 启动类加载器中的类为系统核心类, 包括一些重要的系统接口,
而在应用类加载器中, 为应用类。 按照这种模式, 应用类访问系统类自然是没
有问题, 但是系统类访问应用类就会出现问题。 比如在系统类中提供了一个接
口, 该接口需要在应用类中得以实现, 该接口还绑定一个工厂方法, 用于创建该
接口的实例, 而接口和工厂方法都在启动类加载器中。 这时, 就会出现该工厂方
法无法创建由应用类加载器加载的应用实例
详解可见:https://blog.csdn.net/qq_53799434/article/details/126428392
运行时数据区 (Runtime Data Area)
![](https://i-blog.csdnimg.cn/blog_migrate/f87b4b6dd10b48de82f4b27d67233bc1.png)
程序计数器 (Program Counter Register)
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
JVM可以同时支持多个执行线程。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。
如果该方法不是native方法,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是native的,则pc寄存器的值为undefined。Java虚拟机的pc寄存器足够宽,可以容纳特定平台上的returnAddress或native指针。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError**情况的区域。
Java虚拟机栈
与程序计数器一样,是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆
所有线程共享,虚拟机启动时创建。唯一的目的是用于存放对象实例和数组,绝大部分对象实例在堆上分配内存。
方法区
用于存储被JVM加载的class的元数据信息,比如类的结构、运行时的常量池、字段、常量、方法数据、方法构造函数以及接口初始化等特殊方法。还有JIT编译器编译后的代码缓存等数据。
JDK8之前,HotSpot采用永久代的概念实现方法区,JDK8开始废弃了永久代的概念,改用在本地内存(Native Memory)中实现的元空间(Meta-space)来代替。
方法区的GC比较少出现,回收目标主要是针对常量池的回收和对类型的卸载。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
执行引擎 (Execution Engine)
概述
JVM核心支撑之一。JVM主要任务就是把字节码加载到内存中在让执行引擎进行执行。执行引擎的任务就是把字节码文件编译成操作系统可识别的的本地机器指令。
工作过程
执行引擎在执行过程中究竟需要执行什么样子的字节码指令完全依赖于PC寄存器
每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息(方法区)
本地库接口(Native Interface)
概述
一个本地方法就是一个java调用非java代码的接口,该方法的底层是由非java语言实现的,比如C语言.在定义一个本地方法时,不提供实现体,因为其实现体是由非java语言实现的.
关键字native可以与其他所有的java标识符连用,abstract除外.
作用
1.与java环境外交互
在java应用时有时会需要与java外面的环境交互,这是本地方法存在的主要原因.本地方法这种交流机制为我们提供了一个简单的接口,不需要去了解java应用之外的繁琐细节.
2.与操作系统交互
JVM毕竟不是一个完整的系统,虽然是java程序赖以生存的平台,但是也会经常依赖于一些底层系统的支持,这些底层系统就是强大的操作系统.通过使用本地方法,我们可以用java实现了jre的与底层系统的交互.还有如果需要使用一些Java语言本身没有提供的操作系统特性时,也是需要使用本地方法的.
3.Sun’s java
Sun的解释器是用C实现的,这使得它能像一些普通的C与外部交互.jre大部分是用java实现的,他也会通过一些本地方法与外界交互.举个例子:java.lang.Thread的setPriority()方法是由java实现的,但是它调用的是该类的本地方法setPriority0(),这个本地方法是C实现的。