JVM总结(一)

2022-07-31

什么是JVM

JVM是可运行Java代码的假想计算机,包括一套字节码指令集,一个寄存器,一个栈,一个垃圾回收,堆,一个存储方法域

JVM的作用

想要运行一个Java代码,需要具备JRE环境。而JRE中,包括Java虚拟机及Java的核心类库。Java程序员通常安装的JDK,则已经包括了JRE,还附带了常用的开发和诊断工具。
在Java语言中,最重要的莫过于Java虚拟机。为什么需要有Java虚拟机呢?
Java 作为一门高级程序语言,它的语法非常复杂,抽象程度也很高。因此,直接在硬件上运行这种复杂的程序并不现实。所以呢,在运行 Java 程序之前,我们需要对其进行一番转换。
转换的过程为通过编译器将 Java 程序转换成该虚拟机所能识别的指令序列,也称 Java 字节码。Java虚拟机会将字节码,即class文件加载到JVM中。由JVM进行解释和执行。除了 Java 外,Scala、Clojure、Groovy,以及时下热门的 Kotlin,这些语言都可以运行在 Java 虚拟机之上
说说jdk,jvm,jre之间的关系
jdk:Java的开发工具包。JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。
其中的开发工具:编译工具(javac.exe) 打包工具(jar.exe)等。
jre:Java运行环境。包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

三者间的关系
jdk=jre+开发工具集(包括javac.exe,jar.exe)
jre=JVM+java的核心类库

由上图可以看出,JVM是运行在操作系统上的,与硬件无直接交互

常见的JVM

我们通常用到的jvm是HotSpot JVM:Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。最初由一家名为“Longview Technologies”的小公司设计,后被Sun公司收购。

JVM体系概述

从上图中可以看出JVM把运行时内存区分为五部分:方法区、堆、Java栈(Java虚拟机将栈栈细分为面向Java方法的Java方法栈和面向本地方法的本地方法栈)、本地方法区、程序计数器。
执行 Java 代码首先需要使用类加载器将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。
Java 虚拟机将栈细分为面向 Java 方法的 Java 方法栈,面向本地方法(用 C++ 写的 native 方法)的本地方法栈,以及存放各个线程执行位置的 PC 寄存器(程序计数器)。
在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧(栈的一片区域),用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

类加载器

类加载器,即ClassLoader,它负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

类加载器分类

虚拟机自带的类加载器
1、启动类加载器(Bootstrap):主要负责加载jre中的最为基础、最为重要的类。如JAVAHOME/jre/lib/rt.jar等,以及由虚拟机参数−Xbootclasspath指定的类。由于它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。2、扩展类加载器(Extension),由Java代码实现,用于加载相对次要、但又通用的类,比如存放在JRE的lib/ext目录下jar包中的类,以及由系统变量java.ext.dirs指定的类。如JAVA_HOME/jre/lib/rt.jar等,以及由虚拟机参数 -Xbootclasspath 指定的类。由于它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。
2、扩展类加载器(Extension),由Java代码实现,用于加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类,以及由系统变量 java.ext.dirs 指定的类。如
JAVA_HOME/jre/lib/ext/*.jar。
3、应用程序类加载器(AppClassLoader),由Java代码实现, 它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
除了BootStrap Class Loader,其他的类加载器,都是Java.lang.ClassLoader的子类。其他的类加载器都由加载sum.misc.Launcher类后得到。

类的加载过程

又上图可以看出分为三个阶段:
1、加载
加载,是指查找字节流,并且据此创建类的过程。
类加载器负责类的加载,除了BootStrap ClassLoader,其他的类加载器都由Java代码实现,因此需要先使用BootStrap ClassLoader将其他的类加载器加载到java虚拟机。再由这些类加载器加载其他类。类的加载需要遵守双亲委派模型

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
优势:
①这采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
②其次是考虑到安全因素,防止java核心api中定义类型不会被用户恶意替换和篡改,从而引发错误。

package java.lang;
public class String {
	public String() {
		System.out.println("自己伪造的String");
	}
	public static void main(String[] args) {
		System.out.println("自己伪造的String的main()");
	}
}

注意:在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
加载的类在JVM中创建相应的类结构,类结构会存储在元空间(1.8之前称之为方法区)。
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象,Class类的构造方法是私有的,只有JVM能够创建。因此Class对象是反射的入口,使用该对象就可以获得目标类所关联的.class文件中具体的数据结构。
类加载的最终产物就是位于堆中的Class对象,该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口。
2、链接(验证、准备、解析)
加载(将字节码文件读入到虚拟机)后的类,如果没有链接,那么是暂时不能使用的。链接可分为验证、准备以及解析三个阶段。
验证指确保被加载类能够满足Java虚拟机的约束条件,防止字节码文件损坏或者被恶意注入。
准备阶段主要是在元空间(方法区)为加载类的静态字段分配内存,设置变量的初始化值。
解析阶段则主要将符号引用解析为实际的引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载。
3、初始化
类的初始化只有一次,类的初始化是不同于对象的初始化。
类加载的最后一步是初始化,如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,该字段便会被 Java 编译器标记成常量值(ConstantValue)。初始化会为类的常量赋值,以及执行静态代码块中的方法。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
类在初始化时,会由编译器自动收集类中的所有变量的赋值语句和静态代码块,将这些合并为一个方法,由上往下执行。
Java 虚拟机会通过加锁来确保类的初始化过程(clinit)仅被执行一次。
类的初始化会在以下情况下触发:
*当虚拟机启动时,初始化用户指定的主类;
*当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
*当遇到调用静态方法的指令时,初始化该静态方法所在的类;
*当遇到访问静态字段的指令时,初始化该静态字段所在的类;
*子类的初始化会触发父类的初始化;
*如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
*使用反射 API 对某个类进行反射调用时,初始化这个类;

public class Singleton {
	private Singleton() {}
		private static class LazyHolder { 
			    static final Singleton INSTANCE = new Singleton();
		}
	public static Singleton getInstance() {
			    return LazyHolder.INSTANCE;
		}
}

/**
这是一个单例延时初始化的案例,只有调用Singleton.getInstance 时,程序才会访问 LazyHolder.INSTANCE,才会触发对 LazyHolder 的初始化(对应第 4 种情况),继而新建一个 Singleton 的实例。
由于类初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个 Singleton 实例。
**/

本文地址:https://blog.csdn.net/weixin_41731982/article/details/107695677

《JVM总结(一).doc》

下载本文的Word格式文档,以方便收藏与打印。