JVM专题-组件剖析_jvm

1.类加载器

JVM专题-组件剖析_类加载器_02
它是负责加载.class文件的,它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine 来决定

1.1.类加载器的流程(类的生命周期)

JVM专题-组件剖析_类加载器_03

从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接.

加载

1.将class文件加载到内存
2.将静态数据结构转化成方法区中运行时的数据结构
3.在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口

链接

1.验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
2.准备:为static变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
3.解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)

初始化

初始化其实就是一个赋值的操作,它会执行一个类构造器的<clinit>()方法。由编译器自动收集类中所有变量的赋值动作,此时准备阶段时的那个 static int a = 3 的例子,在这个时候就正式赋值为3

卸载

GC将无用对象从内存中卸载

6.2.类加载器的加载顺序

加载一个Class类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的

虚拟机自带的加载器
BootStrap ClassLoader:rt.jar
Extention ClassLoader: 加载扩展的jar包
App ClassLoader:指定的classpath下面的jar包

用户自定义加载器
Custom ClassLoader:自定义的类加载器

JVM专题-组件剖析_类加载器_04
JVM专题-组件剖析_jvm_05
JVM专题-组件剖析_加载_06

引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。

类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL

对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
JVM专题-组件剖析_加载_07

2.什么是沙箱

Java安全模型的核心就是Java沙箱(sandbox),沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

3.双亲委派机制

当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给父类去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,子类加载器才会自行尝试加载

这样做的好处是,加载位于rt.jar包中的类时不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样保证了使用不同的类加载器得到的都是同一个结果。

其实这个也是一个隔离的作用,避免了我们的代码影响了JDK的代码,比如我现在要来一个

public class String(){
public static void main(){sout;}
}

这种时候,我们的代码肯定会报错,因为在加载的时候其实是找到了rt.jar中的String.class,然后发现这也没有main方法

双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该首先传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
JVM专题-组件剖析_类加载器_08

4.类加载器加载流程

JVM专题-组件剖析_类加载器_09

5.执行引擎

Execution Engine执行引擎负责解释命令,提交操作系统执行。

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM锁识别的字节码指令、符号表和其他辅助信息,那么,如果想让一个Java程序运行起来、执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者.执行引擎的工作过程。

Engine执行代码时一般分为两种类型:

1.解释执行 传统方式
2.编译执行(e.g JIT),产生本地机器码,编译花费时间多,但是执行时效率和速度更高

6.1.什么是解释器( Interpreter),什么是JIT编译器?

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT (Just In Time Compiler)编译器(即时编译器):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

6.2.为什么说Java是半编译半解释型语言?

JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
JVM专题-组件剖析_类加载器_10

7.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关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。

这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

1.native 是用做java 和其他语言(如c++)进行协作时用的,也就是native 后的函数的实现不是用java写的
2.既然都不是java,那就别管它的源代码了,呵呵

8.本地方法栈

native关键字主要用于方法上

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

一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。

在定义一个native方法时,并不提供实现体(比较像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的

本地方法栈的特点

1.Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
2.本地方法栈,也是线程私有的。

允许被实现成固定或者是可动态扩展的内存大小(在内存溢出方面和虚拟机栈相同)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。

3.如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
4.本地方法一般是使用C语言实现的。
5.它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

JVM专题-组件剖析_加载_11

9. PC寄存器

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

10.方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中

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

方法区就用来存储了每一个类的结构信息,不同的虚拟机实现是不一样的,有些叫永久代,有些称为元空间

方法区中的内容:

类型信息

类型的全限定名
超类的全限定名
直接超接口的全限定名
类型标志(该类是类类型还是接口类型)
类的访问描述符(public、private、default、abstract、final、static)

类型常量池
Jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string, integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。 因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。

字段信息

字段修饰符(public、protect、private、default) 
字段的类型
字段名称

方法信息

方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表

类变量(静态变量)
就是类的静态变量,它只与类相关,所以称为类变量 。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。

指向类加载器的引用
jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。

类加载器加载完某个类后,将这个类的一些信息保存在方法区,并将这个类加载器的一个引用作为类型信息的一部分保存在方法区中

指向Class实例的引用

jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

例如,假如你有一个java.lang.Integer的对象引用,可以激活getClass()得到对应的类引用类加载的过程中(通过new 或者Class.forName方式加载某个类),虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象

方法表
运行时常量池

11.Stack栈是什么

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

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

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

11.2.栈运行原理:

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

遵循“先进后出”/“后进先出”原则。

JVM专题-组件剖析_类加载器_12
图示在一个栈中有两个栈帧:

栈帧 2是最先被调用的方法,先入栈,然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,线程结束,栈释放。

每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕 后会自动将此栈帧出栈。

11.3.虚拟机栈有什么作用

1.主管Java程序的运行 程序中的方法与局部变量 部分结果
2. 参与方法的调用与返回

JVM专题-组件剖析_jvm_13

11.4.栈的优点

1.栈是一种快速有效的分配存储方式 访问速度仅次于程序计数器
2.JVM直接对栈的操作只有两个每个方法的执行会伴随进栈(入栈 压栈),执行结束后会出栈(弹栈)
3.对于栈来说没有垃圾回收的问题(不存在GC有可能存在OOM)

11.5.虚拟机常见异常

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常;

如果Java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常;

package com.it.test;

/**
* @BelongsProject: Tmall
* @BelongsPackage: com.it.test
* @CreateTime: 2020-11-15 17:27
* @Description: TODO
*/
public class TestStack {

public static int i=1;

public static void main(String[] args) {
//我的电脑默认测试JVM栈的大小为9750左右
//添加JVM命令行参数:-Xss1024k 之后为9777左右
//添加JVM命令行参数:-Xss1m 之后为9789左右
System.out.println(i++);
main(args);
}
}

JVM专题-组件剖析_java_14

11.6.设置栈内存大小

我们可以使用虚拟机参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度;

-Xss size

设置线程堆栈大小(以字节为单位)。附加字母k或K表示KB,m或M表示MB,和g或G表示GB。默认值取决于平台:

Linux / x64(64位):1024 KB
macOS(64位):1024 KB
Oracle Solaris / x64(64位):1024 KB
Windows:默认值取决于虚拟内存

下面的示例以不同的单位将线程堆栈大小设置为1024 KB:

-Xss1m (1mb)
-Xss1024k (1024kb)
-Xss1048576

设置方式如下图所示:
JVM专题-组件剖析_加载_15

深度调用后,撑爆了栈,是一个错误,不是异常
Exception in thread java.lang.StackOverflowError

java.lang.Object
——java.lang.Throwable
——java.lang.Error
——java.lang.VirtualMachineError
——java.lang.StackOverflowError

11.7.栈+堆+方法区的交互关系

JVM专题-组件剖析_jvm_16
HotSpot是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址

JVM专题-组件剖析_jvm_17

12.常见的JVM种类

HotSpot VM(Sun公司)

HotSpot VM是绝对的主流。大家用它的时候很可能就没想过还有别的选择,或者是为了迁就依赖了Oracle/Sun JDK某些具体实现的烂代码而选择用HotSpot VM省点心。Oracle / Sun JDK、OpenJDK的各种变种(例如IcedTea、Zulu),用的都是相同核心的HotSpot VM。
当大家说起“Java性能如何如何”、“Java有多少种GC”、“JVM如何调优”云云,经常默认说的就是特指HotSpot VM。可见其“主流性”。

JDK8的HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,也就是传说中的“HotRockit”,只是产品里名字还是叫HotSpot VM。

这个合并并不是要把JRockit的部分代码插进HotSpot里,而是把前者一些有价值的功能在后者里重新实现一遍。移除PermGen、Java Flight Recorder、jcmd等都属于合并项目的一部分不过要留意的是,这里的HotSpot VM特指“正常配置”版,而不包括“Zero / Shark”版。

Wikipedia那个页面上把后者称为“Zero Port”。用这个版本的人应该相当少,很多时候它的release版都build不成功。

J9 VM(IBM)
J9是IBM开发的一个高度模块化的JVM。在许多平台上,IBM J9 VM都只能跟IBM产品一起使用。这不是技术限制,而是许可证限制。
例如说在Windows上IBM JDK不是免费公开的,而是要跟IBM其它产品一起捆绑发布的;
使用IBM Rational、IBM WebSphere的话都有机会用到J9 VM(也可以自己选择配置使用别的Java SE JVM)。根据许可证,这种捆绑在产品里的J9 VM不应该用于运行别的Java程序…大家有没有自己“偷偷的”拿来跑别的程序IBM也没力气管
(咳咳而在一些IBM的硬件平台上,很少客户是只买硬件不买配套软件的,IBM给一整套解决方案,里面可能就包括了IBM JDK。
这样自然而然就用上了J9 VM。
所以J9 VM得算在主流里,虽然很少是大家主动选择的首选。
J9 VM的性能水平大致跟HotSpot VM是一个档次的。有时HotSpot快些,有时J9快些。
不过J9 VM有一些HotSpot VM在JDK8还不支持的功能,最显著的一个就是J9支持AOT编译和更强大的class data sharing

JRockit(BEA公司的)
JRockit以前Java SE的主流JVM中还有JRockit,跟HotSpot与J9一起并称三大主流JVM。
这三家的性能水平基本都在一个水平上,竞争很激烈。
自从Oracle把BEA和Sun都收购了之后,Java SE JVM只能二选一,JRockit就炮灰了。
JRockit最后发布的大版本是R28,只到JDK6;原本在开发中的R29及JDK7的对应功能都没来得及完成项目就被终止了。