JVM
虚拟机
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。他是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机(如WMware)和程序虚拟机(如Java虚拟机)
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中
Java虚拟机
Java虚拟机是一台执行Java字节码的虚拟机计算机,他拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
JVM平台的各种语言可以共享Java虚拟机带来的跨平台性,优秀的垃圾回收器,以及可靠的即时编译器。
Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点
- 一次编译,到处运行
- 内存自动管理
- 自动垃圾回收功能
JVM的位置
JVM的整体结构
- HotSpot VM是目前市面上高性能虚拟机的代表作之一。
- 它采用解释器与即时编译器并存的架构
- Java程序的运行性能已经达到了可以和C/C++程序一较高下的地步了
Java执行流程
Java代码执行流程
Java语言概念图
JVM的架构模型(栈、寄存器)
栈的指令集架构和寄存器指令集架构的区别
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
一条指令由地址码和操作数组成,零地址即没有地址只有操作数,多地址表示有多个地址表示一个操作数
栈:跨平台性、指令集小,指令多,执行性能不如寄存器架构
基于栈式架构的特点:
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题;使用零地址指令分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好的跨平台性;
基于寄存器架构的特点:
- 典型的应用的是X86的二进制指令集(如传统的PC以及Android的Davlik虚拟机);
- 指令集架构完全依赖硬件,可移植性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作
- 大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
总结
由于跨平台性的设置,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为寄存器的。优点在于跨平台性、指令集小、编译容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
JVM的生命周期
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类(Initial Class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
- 程序开始执行时它才开始运行,程序结束时它就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
虚拟机的退出
情况如下:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
- 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况
类加载子系统
作用
- 类加载器子系统负责从文件或者系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
- 加载的类信息存放于一块称为方法区的内存空间,出了类的信息外,方法区中还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色
class file 加载到JVM中,被称为DNA元数据模板,放在方法区。
- 将class文件加载到JVM中最终成为元数据模板,此过程需要一个运输工具即ClassLoader(类加载器)
类的加载器分类
以下关系是包含关系,不是继承关系,也不是上下级的关系
引导类、扩展类、系统加载器
虚拟机自带的加载器
启动类加载器(引导类加载器 Bootstrap ClassLoader):
- 这个类加载器使用C/C++语言实现,嵌套在JVM内部。
- 启动类加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路劲下的内容),用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父类加载器。
- 加载扩展类和应用程序类加载器,并指定为它们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader):
- Java语言编写,由sun.misc.launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java,ext.dirs系统属性所指定的目录中加载库类,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会由自动扩展类加载器加载。
应用程序类加载器(系统类加载器 AppClassLoader):
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
- 该类加载器是程序中的默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
用户自定义加载器:
为什么要自定义类加载器?
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄露
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会对它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成加载任务,就成功返回,若不能子加载器才会尝试自己去加载,这就是双亲委派模式。
寄存器
CPU只有把数据加载到寄存器中才能运行。
作用:PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
OOM(Out Of MemoryError):内存不足,栈溢出
PC寄存器介绍
1、每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
2、任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的TVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned) 。
栈
**栈是运行时的单位,而堆是存储单位。**即栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
栈的内部结构
Java虚拟机栈帧
Java虚拟机栈是什么?
Java虚拟机栈(Java virtual Machine stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧( Stack Frame) ,对应着一次次的Java方法调用。
- 是线程私有的
生命周期:
- 生命周期和线程保持一致
作用:
- 主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- 没有垃圾回收问题
栈的操作:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈工作
栈中存储什么?
每个方法都对应着栈帧,方法运行完毕后出栈。
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame) 的格式存在。
- 在这个线程.上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
局部变量表
局部变量(Local variables)指在程序中只在特定过程或函数中可以访问的变量。局部变量是相对于全局变量而言的。
操作数栈
- 操作数栈的深度为2
- byte、short、char、boolean都以int型来保存
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接:
- 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接:
- 如果被调用的方法在编译器无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称为动态链接。
方法的调用:早期绑定与晚期绑定
绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定:
- 即指被调用的目标方法如果在编译器可知,且运行期保持不变时,可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
方法的调用:虚方法和非虚方法
非虚方法:
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
- 静态方法、私有方法、final方法、构造器实例都是非虚方法。
- 其他方法称为虚方法
虚方法和非虚方法:调用指令
前四条指令(普通指令)固化在JVM内部中,方法的调用指令不可人为的干预,invokedynamic指令则支持由用户确定方法版本。invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本。
- invokespecial:调用
方法,私有及父类方法,解析阶段确定唯一方法版本。 - invokevirtual:调用所有虚方法。
- invokeinterface:调用接口方法。
动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行。
invokeddynamic指令的使用
动态类型语言和静态类型语言
动态类型语言和静态类型语言的区别就在于对类型的检查是在编译器还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。简单来说静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的重要特征。
方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在过程结束;如果不通过类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java.lang.IllegalAccessError异常。
- 否则,按继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果还没有找到适合的方法,则抛出java.lang.AbstractMethodError异常。
java.lang.IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
方法返回地址
- 存放调用该方法的PC寄存器的值。
- 一个方法的结束,有两种方式:
- 正常执行结束
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法调用的位置,方法正常退出时,调用者的PC计数器的值的值作为返回地址,即调用该方法的指令的下一条指令地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法(C语言)的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 本地方法是用C语言实现的
- native方法执行事加载本地方法库
堆
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
堆的概述:
堆的内存空间大小可以调节 -Xms10m -Xmx10m -XX:PrintGCDetails
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间(堆的内存空间大小可以调节)。
- 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
- 所有的线程共享Java堆,在这里还可以划分私有线程的缓冲区(Thread Local Allocation Buffer, TLAB)。
堆的核心概述:
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
堆的核心概述:内存细分
现代垃圾收集器大部分都基于分代理论设计,堆空间细分为:新生区(新生代/年轻代)+养老区(老年区/代)+元空间(永久区/代)
堆空间大小的设置:
-Xms和Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
- -Xms:用于表示堆区的起始内存,等价于-XX:InitialHeapSize
- -Xmx:则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。另一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
- Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
- 其中年轻代又可以分为Eden空间,Survivor0和Survivor1空间(有时也叫from区、to区)
- 几乎所有的Java对象都是在Eden区被new出来的。
- 绝大部分的Java对象的销毁都在新生代进行了。
对象的分配过程
对象的分配过程:概述
-
new的对象先放伊甸园区。此区有大小限制。
-
当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
-
然后将伊甸园中的剩余对象移动到幸存者0区。
-
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
-
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
-
啥时候能去养老区呢?可以设置次数。默认是15次。
参数设置:-XX:MaxTenuringThreshold=次数 进行设置
对象的分配过程:总结
- 针对幸存者s0, s1 区的总结:复制之后有交换,谁空谁是to。
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
Minor GC、Major GC、Full GC的对比
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是指新生代。针对Hotspot VM的实现,他里面的GC按照回收又分为两大种类型:一种是部分收集(Partail GC),一种是整堆收集(Full GC)
部分收集:
- 新生代收集(Minor GC/ Young GC):只是新生代(Eden、S0、S1)的垃圾收集。
- 老年代收集(Major GC/ Old Gc):只是老年代的垃圾收集。
- 目前,只有CMS GC会有单独收集老年代的行为。
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 整堆回收(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
整堆收集(Full GC):
收集整个Java堆和方法区的垃圾收集。
老年代GC(Major GC/Full GC)触发机制:
STW(Stop一the一World)指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。.
- 指发生在老年代的GC,对象从老年代消失时,就是“Major GC” 或“Full GC”发生了
- 出现了Major GC经常会伴随至少一次的Minor GC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
- 也就是老年代不足时,会尝试触发Minor GC。如果之后空间还不足,则触发Major Gc.
- Major GC的速度一般会比Minor GC慢10倍以上,SWT的时间更长。
- 如果Major GC后,内存还不足,就报OOM了。
- Major GC的速度一般会比Minor GC慢10倍以上。
堆空间的分代思想:
- 为什么需要把Java堆分代?不分代就不能正常工作了吗?
- 不分代完全可以,分代的唯一理由就是优化GC性能。
JDK8分代
内存分配策略(或对象提升(Promotion)规则):
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden区
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保
- -XX:HandlePromotionFailure
对象分配过程:TLAB
什么是TLAB?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区域。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此可以将这种内存分配方式称为快速分配策略。
- 大多数从OpenJDK衍生出来的JVM都提供了TLAB设计。
为什么有TLAB(Thread Loacl Allocation Buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
对象分配过程:TLAB
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
- 通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
- 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,也可以通过“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空降中分配内存。
对象的分配空间:TLAB
未完待续...
Java基本命令
Java:执行Java程序(运行.class文件)
Javac:编译.java文件为.class文件
javap:反编译java程序
参考文档
The Java® Virtual Machine Specification
Start :May 1, 2021 End:
Q.E.D.