0%

runtime/java/jvm/jvm

JVM JAVA 虚拟机探秘

Java 虚拟机规范官方白皮书下载地址:https://docs.oracle.com/javase/specs/

相关书籍:深入理解JAVA虚拟机,自己动手写JAVA虚拟机

字节码:

java代码由编译器编译成的.class文件就是字节码文件。

虚拟机:

  1. 古老SUN Classic VM 虚拟机

    只提供了解析器,没有提供JIT,如果需要JIT需要外挂JIT。Hostopt内置了此虚拟机

  2. Java HotSpot Virtual Machine (openJdk)

  3. JRockit

    专注于服务器端应用。JRockit内部不包含解析器执行,全部代码都靠JIT编译器编译执行。

    是世界上最快的JVM

  4. J9(IBM)

  5. TaobaoJVM

    基于openJdk 的定制版本的虚拟机,深度优化。

    将生命周期较长的JAVA对象移除到Heap外,并且GC不能管理,已达到降低GC的回收频率和提高GC的回收效率。

    严重依赖intel的CPU。

垃圾回收器:

  1. G1
  2. ZGC
  3. CMS
  4. Shenandoah GC (RedHad)

Java 虚拟机就是字节码的运行环境,负责装载字节码到内部,解释编译为对应平台上的机器码指令执行。
![image-20210607145353023](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210607145353023.png)

Hotspot 虚拟机整体结构

是基于栈的指令集架构,还有一种基于寄存器的指令集架构。

![image-20210607150301724](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210607150301724.png)

![image-20210607150333881](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210607150333881.png)

JVM的生命周期

虚拟机的启动

通过引导类加载器创建的一个初始类来完成的。

虚拟器的执行

执行一个java程序的时候,执行的是一个叫做java虚拟机的进程

虚拟器的退出

内存结构概述

类加载器子系统作用:

类加载器子系统只负责从文件或者网络中加载class文件,只负责class的加载。至于他是否可以运行,则是由Execution Engine决定的。

加载的类型信息存放在方法区的内存空间上,除了类的信息外,还有运行时常量池等。

类的加载过程:

加载-> 验证-> 准备-> 解析-> 初始化,验证、准备、解析统一为链接过程。

  1. 加载

    通过一个类的全限定名获取定义此类的二进制字节流。

    将这个字节流锁diamante的静态存储结构转化为方法区的运行时数据结构。

    在内存中生成一个代码这个类的java.lang.Class 对象 ,作为方法区这个类的各种数据的访问入口

  2. 链接-> 验证 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求

    文件格式验证、元数据验证、字节码验证、符号引用验证

  3. 链接-> 准备 为类变量分配内存并且设置该变量的默认初始值,0

    这里不包含final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化

  4. 链接-> 解析 将常量池内的符号引用转为直接引用的过程。

  5. 初始化

    执行类构造器方法<client>()的过程,这个方法已经被定义好了,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块的语句合并而来。

    子类的<client>()执行前,需要保证父类的<client>()执行完毕。

类加载器

​ JVM支持两种类型的类加载器,引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader).

​ 所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

类加载器的分类

  1. 启动类加载器(引导类加载器,Bootstrap ClassLoader)

    是使用的C/C++语言实现的,嵌套在JVM内部

    它是用来加载JAVA的核心库,用于提供JVM自身需要的类

    并不继承自java.lang.ClassLoader,没有父加载器

    加载扩展类和应用程序类加载器,并制定为他们的父类加载器

    处于安全考虑,Bootstrap 启动类加载器只加载包名为java、javax、sun等开头的类

  2. 扩展类加载器(Extension ClassLoader)

    java语言编写,由sun.misc.Launcher$ExtClassLoader实现,派生于ClassLoader抽象类

    父类加载器为启动类加载器

    从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录

  3. 应用程序类加载器(系统类加载器,AppClassLoader)

    java语言编写,由sun.misc.Launcher$AppClassLoader实现,派生于ClassLoader抽象类

    父类加载器为扩展类加载器

    他负责加载环境变量classpath 或系统属性java.class.path路径下的类库

    程序中的默认类加载器

    通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

  4. 用户自定义类加载器

    隔离加载类、修改类加载的方式、扩展加源码、防止源码泄露

类加载器双亲委派机制:

当加载某一个类的时候,当前的类加载器并不会真正加载它,而是向上传递,询问父类加载器是否可以加载。如果父类可以加载就直接放回成功。否则最后才是当前类加载器加载类。

运行时数据区

不同的JVM对于内存的划分方式和管理机制存在着部分差异。

![image-20210607173534300](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210607173534300.png)

每个JVM 对应一个Runtime实例,也就是运行时数据区

![image-20210607172815624](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210607172815624.png)

PC寄存器

JVM的PC寄存器是对物理的PC寄存器的一种物理抽象。

线程私有的,用于存储指向下一条指令的地址,也就是即将要执行的代码。

它是一块很小的内存空间,运行速度很快。每个线程都有它自己的PC寄存器,生命周期与线程生命周期一致。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的jvm指令地址。如果执行的是natvie方法,则是undefned

虚拟机栈(java栈)

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的java方法调用。

线程私有的,生命周期和线程一致。

主要管java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

栈的存储单位

栈中的数据都是以栈帧的格式存在,方法与栈帧一一对应

栈运行原理

分为出栈入栈,不同线程不允许互相引用对方的栈帧。
如果当前方法调用了其他方法,方法返回的时候,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机丢弃当前栈帧,前一个栈帧称为当前栈帧。
Java方法有两种返回函数的方式,一种是return,一种是抛出异常,都会导致栈帧被弹出

栈帧的内部结构

![image-20210608153821275](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210608153821275.png)

局部变量表、方法返回地址、操作数栈、动态链接(指向运行时常量池的方法引用)、其他附加信息

局部变量表

也叫局部变量数组,定义为一个数组,主要用于存储方法参数和定义在方法体内的局部变量(各类基本数据类型、对象引用、returnAddress类型)
因为线程私有,不存在数据安全问题
局部变量表的大小是在编译器确定下来的,保存在方法的code属性maximum local variables数据中。方法运行期间不会修改局部变量表的大小

Slot

局部变量表中最基本的存储单元是Slot(变量槽)
32位以内的类型只占用一个Slot。64位的类型占用两个Slot.

![image-20210608155457746](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\image-20210608155457746.png)

操作数栈

在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈出栈。

主要用于保存计算过程的中间结果,同事作为计算过程中变量临时的存储空间。

操作数栈的深度在编译器就决定了,保存在Code属性中,为max_stack的值。

栈顶缓存技术(Top of Stack Cashing)

完成一次操作需要更多的入栈出栈操作,这也就需要更多的读写。

所以Hostpot设计了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,一次降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接

  • 每一个栈帧内部都包含一个指向运行时常量池中改栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。

  • 在java源文件编译时,所有的变量和方法引用做为符号引用保存在class文件的常量池里。

  • 动态链接就是为了将这些符号引用转换为调用方法的直接引用。

方法的调用

静态链接:当一个字节码被装载jvm中,如果被调用的方法在编译器可知,且运行期不变,这种情况下将方法调用的符号引用转换为直接引用的过程称为静态链接。
动态链接:如果被调用的方法在编译器无法确定,这种被称为动态链接。

对应方法的绑定机制为:早期绑定和晚期绑定。

虚方法表

在面向对象的编程中,会很频繁的使用到动态分派,如果每次动态分派的过程中都需要重新在类的方法元数据上搜索到合适的目标会影响到执行效率。所以,为了提高性能,在方法区建立了一个虚方法表。

每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

虚方法表是在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

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

如果采用固定大小的栈,每个线程的java虚拟机栈容量在线程创建的时候独立选定,如果线程请求分配的栈容量超过java虚拟机栈允许的大小,则抛出StackOverflowError

如果采用动态扩展的栈,尝试扩展时无法申请到足够的内存,则会抛出OutOfMemoryError

方法返回地址

存放调用该方法PC寄存器的值,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。

其他附加信息

栈帧中还运行携带与JAVA虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息

设置栈的大小

-Xss256k or -Xss1m or -Xss102400

本地方法栈

本地方法栈用于管理本地方法的调用,本地方法栈也是线程私有的,也允许被设定为固定或动态扩展的内存大小

Hotspot将本地方法栈和java方法栈合二为一。

本地方法接口

本地方法

一个Native Method 就是一个Java调用非Java代码的接口。

一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域。
Java堆在JVM启动的时候就别创建了,空间大小也就确定了。堆得内存大小是可以调节的。
《JAVA虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上应该是连续的。

![image-20210609155854380](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\jvm\image-20210609155854380.png)

堆的细分内存

现代垃圾回收器大部分都是基于分代收集理论设置,堆空间细分为:

![image-20210609160959678](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\jvm\image-20210609160959678.png)

Java 7 之前内存分为三个部分。

  • Young Generation Space
    • Eden区
    • Survivor区
  • Tenure Generation Space
  • Permanent Space

Java 8 之后逻辑上也分为三个部分

  • Young Generation Space
    • Eden区
    • Survivor区
  • Tenure Generation Space
  • Meta Space

年轻代老年代

java堆细分可以为:年轻代和老年代
其中年轻代又可以分为:Eden空间,Survivor0和Survivor1(有时候也叫作from区 和 to区)

![image-20210609162526913](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\jvm\image-20210609162526913.png)

通过 -XX:NewRetio=x 表示老年代与新生代的比例x, 则新生代为1 老年代为x , 默认值为2
通过 -XX:SurvivorRetio=x调整Eden与另外两个Survivor空间的占比,默认为8。 则 8:1:1
通过 -Xmn100m设置新生代的空间大小(一般不设置)

分配过程

  1. new的对象先放入到Eden区。
  2. 当Eden区数据放满的时候,此时要new对象,会进行垃圾回收(Minor GC)。将不可达的数据销毁
  3. 将Eden区剩余的对象,放入Survivor0 区 (Survivor0和Survivor1的数据会互相导入,保证分配的时候有一个区域是空的)
  4. 当对象被放到Survivor区的时候,会给对象打上标签1,每次在Survivor区挪移的时候会+1,直到15, 达到15的时候会将对象放入Old区 (-XX:MaxTenuringThreshold=x可以进行次数的限制)
  5. Old区内存不足时,会进行Major GC,如果Old区进行GC后内存依然不足,则会抛出OMM异常

![image-20210609183757781](D:\Git Repertory\MySelf\blog-hexo\src\source_posts\runtime\java\jvm\jvm\image-20210609183757781.png)

TLAB

因为堆区是所有线程共享的,所以在多个线程同时分配空间的时候,会造成安全问题,因此需要加锁。但是加锁又会引想到性能问题。所以采用了TLAB技术

JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

-XX:UseTLAB 可以设置开启TLAB空间,默认为开启。

默认情况下,TLAB的空间内存非常小,只占用整个Eden空间的1%。可以通过-XX:TLABWasteTagrgetPercent设置占比大小。

一旦TLAB空间分配内存失败,则会通过使用加锁机制在Eden空间中分配内存。

Minor GC 、Major GC 、Full GC

Minor GC 新生代收集
Major GC 老年代收集
Full GC 整堆收集,java堆和方法区

Minor GC触发条件:

  1. Eden空间不足时,触发。Survivor空间满时不会触发
  2. Minor GC 非常频繁,回收速度快
  3. Monor GC 会引发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程回复运行

Major GC 触发收集:

  1. 出现了Major GC,经常会伴有一次MinorGC, 也就是Old空间不足,会先尝试进行Major GC的策略选择过程触发Minor GC。如果之后空间还不足,则触发 Major GC。
  2. Major GC 会比MinorGC慢10倍以上,STW时间更长。

Full GC 触发条件:

  1. 调用System.gc()
  2. 老年代空间不足
  3. 方法区空间不足

堆空间常用的参数

最小-Xms10m
最大-Xmx10m

默认大小

物理内存大小/4 和 为物理内存大小/64

通常将最大和最小值设置为一致,其目的是为了GC后不需要在计算堆大小