java虚拟机秘籍(JVM_1)
一、什么是JVM?
JVM是一种规范,虚构的计算机(冯诺依曼计算机结构)。跨语言的平台。
也就是编译后是 .class文件的语言,都能在JVM中运行。例如Java、Kotlin、Scala、Jruby
二、JVM要学什么?
1. 源码到类文件
2. 类文件到JVM
3. JVM中各种处理(内部结构、执行方式、垃圾回收、本地调用)
2.1 源码到类文件
2.1.1 Javac过程
XXX.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> XXX.class文件
2.1.2 class文件格式-十六进制
cafe babe 0000 0034 00a4 0900 1900 81090019 0082 0900 1900 8309 0019 0084 09001900 8509 0019 0086 0700 870a 0007 00880800 890a 0007 008a 0a00 1900 8b0a 0007008c 0800 8d0a 0019 008e 0800 8f0a 00190090 0800 910a 0019 0092 0800 930a 00190094 0800 950a 0019 0096 0800 970a 00070098 0700 990a 0019 009a 0a00 9b00 9c0a009b 009d 0a00 1e00 8807 009e 0700 9f010010 7365 7269 616c 5665 7273 696f 6e554944 0100 014a 0100 0d43 6f6e 7374 616e7456 616c 7565 0500 0000 0000 0000 01010002 6964 0100 104c 6a61 7661 2f6c 616e672f 4c6f 6e67 3b01 0019 5275 6e74 696d6556 6973 6962 6c65 416e 6e6f 7461 74696f6e 7301 0029 4c69 6f2f 7377 6167 6765722f 616e 6e6f 7461 7469 6f6e 732f 4170694d 6f64 656c 5072 6f70 6572 7479 3b010005 7661 6c75 6501 0008 e997 aee9 a2986964 0100 2d4c 636f 6d2f 6261 6f6d 69646f75 2f6d 7962 6174 6973 706c 7573 2f61......
2.1.3 ClassFile Struucture
官网地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count];}
2.1.4 简单分析
magic u4 --> cafe babe
The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.
minor_version u2 -->0000
major_version u2-->0034 十进制数52,表示 JDK8
constant_pool_count u2 --> 00a4 十进制数164 常量池的数量为164
The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.
2.1.5 javap验证
javap -v -p xxx.class 进行反编译,查看字节码信息和指令等信息
2.2 类文件到虚拟机(类加载机制)
类加载机制指虚拟机把class文件加载到内存,并对数据进行校验、转换解析和初始化。
2.2.1 装载(Load)
主要是查找和导入class文件。
step 1 -->通过一个类的全限定名获取定义此类的二进制字节流;
step 2 -->将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
step 3 -->在堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2.2.2 链接(Link)
2.2.2.1 验证(Verify)
保证被加载类的正确性!文件格式验证、元数据验证、字节码验证、符号引用验证。
2.2.2.2 准备(Prepare)
为类的静态变量分配内存,并将其初始化为默认值。
2.2.2.3 解析(Resolve)
把类中符号引用转换为直接引用
符号引用:一组符号来描述目标,可以是任何字面量。
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进 行。
2.2.3 初始化(Initialize)
对类的静态变量、静态代码块执行初始化操作
2.2.4 类加载器ClassLoader
用来装载class类
2.2.4.1 分类
Boostreap ClassLoader --->C++实现的
Extension ClassLoader ---->扩展
App ClassLoader ----->ClassPath指定路径下
Coustom ClassLoader --->自定义
2.2.4.2 加载原则(双亲委派机制)
1. 检查某个类是否已经被加载
自底向上,从Custom ClassLoader至Bootstrap ClassLoader逐层检查,只要有一个ClassLosder加载了,保证此类只被加载一次。
2. 加载顺序
自顶向下,由上层来逐层加载此类。
2.2.4.3 破坏双亲委派
1. tomact
2. SPI机制(Service Provider Interface)
Java中的SPI(Service Provider Interface)机制是一种用于实现组件化架构的技术。它允许在运行时动态地扩展或替换框架或库中的实现类。
Java提供了一些标准的SPI实现,例如JDBC、JNDI、JAXP等。开发人员也可以使用SPI机制来实现自己的插件框架。
3. OSGi
OSGi(Open Services Gateway Initiative)是一种模块化系统架构,可以使Java应用程序以模块化的方式进行开发、部署和管理。
如Eclipse、Apache Felix
2.3 运行时数据区(Run-TIme Data Areas)
2.3.1 官网概括
官网; https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
2.3.2 图解
2.3.3 初步介绍
2.3.3.1 Method Area(方法区)
1. Java虚拟机有一个在所有Java虚拟机线程之间共享的方法区域。
2. 方法区域是在虚拟机启动时创建的。
3. 方法区域在逻辑上是堆的一部分,但是它有一个别名Non-Heap(非堆),目的是和heap(堆)分开
4. 方法区域的内存不需要是连续的。
5. 如果方法区域中的内存不能用于满足分配请求,Java虚拟机将抛出OutOfMemoryError。
另外不同JDK版本Method Area真正的实现不同;
JDK 8中 是Metaspace(元空间)
JDK 8之前是Perm Space(永久代)
二者主要区别:
Meta space和Perm space的主要区别在于存储元数据的方式和管理方式。Meta space使用本地内存存储元数据,并具有自动内存管理功能,而Perm space则位于JVM的堆内存中,并且容量是固定的。
2.3.3.2 Heap(堆)
1. Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
2. Java对象实例以及数组都在堆上分配。
2.3.3.3 Java Virtual Machine Stacks(虚拟机栈)
1. 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的 运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创 建。
2. 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
void method_a(){method_b();} void method_b(){method_c();} void method_C(){}
栈帧:
官网https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6
每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。 每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
局部变量表: 方法中定义的局部变量以及方法的参数存放在这张表中 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使 用。
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用 过程中的动态连接(Dynamic Linking)。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇 见异常,并且这个异常没有在方法体内得到处理。
2.3.3.4 The pc Register(程序计数器)
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法,则这个计数器为空。
2.3.3.5 Native Method Stacks(本地方法栈)
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?
2.3.4 各种情况
2.3.4.1 栈指向堆
在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元 素指向堆中的对象。
2.3.4.2 方法区指向堆
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对 象。
private static Object obj=new Object();
2.3.4.3 堆指向方法区
方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
那么一个对象具体是怎么知道它是由那个类创建出来的呢?这就涉及到对象的内存模型了。
2.3.4.4 Java对象内存模型
2.4 JVM内存模型
2.4.1 运行时数据区
其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这 两方面展开(注意这两块区域都是线程共享的)。
对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。
2.4.2 TLAB(Thread Local Allocation Buffer)?
2.4.2.1 什么是TLAB?
从内存模型来看,在Eden区域内,JVM为每个线程分配了一个私有缓存区域。
2.4.2.2 为什么会出现TLAB?
1. 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
2. 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
3. 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
2.4.2.3 快速分配策略
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
2.4.2.4 TLAB说明
1. 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但TLAB确实是作为内存分配的首选。
2. 可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
3. TLAB空间的内存非常小,仅占有整个Eden空间的1%,可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
4.
2.4.3 具体分区
一块是非堆区,一块是堆区 堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To
2.4.4 对象内存分配过程
2.4.5 常见面试题
2.4.5.1 如何理解Minor/Major/Full GC ?
Minor GC 年轻代
Major GC 老年代
FULL GC 新生代+老年代
2.4.5.2 为什么需要Survivor区?只有Eden不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?
频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
如果对老年代的空间进行增加或者减少呢?
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一 旦发生Full GC,执行所需要的时间更长;
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生。
2.4.5.3 为什么需要两个Survivor区?
最大的好处就是解决了碎片化。假设现在只有一个Survivor区,我们可以模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些 存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,
也就导致了内存碎片化。 永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
2.4.5.4 新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是 “朝生夕死”的
2.4.5.5 堆内存中都是线程共享的区域吗?
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。 对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
2.5 Garbage Collect(垃圾回收)
2.5.1 如何确定一个对象是垃圾?
2.5.1.1 引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
缺点:出现循环引用情况,则永远不能被回收
2.5.1.2 根可达算法
通过GC Root的对象,开始向下寻找,看某个对象是否可达。
那些能作为GC Root?
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
2.5.2 什么时候会进行垃圾回收?
1. Eden区或者Servivor区空间不够
2. 老年代空间不够用
3. 方法区空间不够用
4. System.gc() 一般不用
2.5.3 垃圾收集算法
2.5.3.1 标记-清除算法(Mark-Sweep)
1. 标记:找出内存中需要回收的对象,并且把它们标记出来。
此时堆中所有区域都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。
2. 清除调被标记需要回收的对象,释放出对应的内存空间。
缺点:
1. 会产生大量不连续的内存碎片,碎片太多会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而触发另一次垃圾收集动作。
2. 耗时,效率不高
2.5.3.2 标记-复制(Mark-Copying)
将内存划分为两块相等的区域,每次只使用其中一块。当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次 清除掉。
缺点:空间利用率低
2.5.3.3 标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活 的对象都向一端移动,然后直接清理掉端边界以外的内存。
2.5.3.4 分代收集算法
Young区:复制算法(年轻代对象大部分是朝生夕死,存活对象较少,复制效率就高)
Old区:标记清除或者标记整理算法(Old区对象存活时间较长)
2.5.5 HotSpot中垃圾收集器
2.5.5.1 Serial
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一 选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更 重要的是其在进行垃圾收集的时候需要暂停其他线程。
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:JVM Client模式下的默认新生代收集器
2.5.5.2 Serial Old
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算 法",运行过程和Serial收集器一样。
2.5.5.3 ParNew
可理解为Serial收集器的多线程版本。
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在JVM Server模式下的虚拟机中首选的新生代收集器
2.5.5.4 Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集 器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的 运算任务。
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCRatio直接设置吞吐量的大小。
2.5.5.5 Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量
2.5.5.6 CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。 采用的是"标记-清除算法",整个过程分为4步
1.初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
2.并发标记 CMS concurrent mark 进行GC Roots Tracing
3.重新标记 CMS remark 修改并发标记因用户程序变动的内容
4. 并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 浮动垃圾
通过参数启用 -XX:+UseConcMarkSweepGC
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量 。一般会撘配Serial Old一起
2.5.5.7 G1(Garbage-First)
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再 是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂 如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中 设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
1.分代收集(仍然保留了分代的概念)
2.空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
3.可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消 耗在垃圾收集上的时间不得超过N毫秒)
工作过程可分为以下几步:
1.初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
2.并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
3.最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
4. .筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
2.5.5.8 ZGC
官网:https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了 会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题 只能在64位的linux上使用。
1. 可以达到10ms以内的停顿时间要求
2. 支持TB级别的内存
3. 堆内存变大后停顿时间还是在10ms以内
2.5.5.9 垃圾收集器分类
1. 串行收集器
Serial 和 Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。适用于内存比较小的嵌入式设备
2. 并行收集器(吞吐量优先)
Parallel Scanvenge,Parallel Old
多条垃圾回收线程并行工作,但此时用户线程处于等待状态。适用于科学计算、后台处理的场景
3. 并发收集器
CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时 候不会停顿用户线程的运行。
适用于相对时间有要求的场景,比如Web 服务。
2.5.5.10 常见问题
1. 吞吐量和停顿时间
停顿时间:垃圾收集器进行垃圾回收 终端应用执行响应的时间
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互 的任务。
2. 如何选择合适的垃圾收集器
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器
3. G1收集
JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。
选择G1收集器的理由:
50%以上的堆被存活对象占用 ;对象分配和晋升的速度变化非常大 ;垃圾回收时间比较长
4. G1中的Rset
全称Remembered Set,记录维护Region中对象的引用关系
在G1垃圾收集器进行新生代的垃圾收集时,也就是Minor GC,假如该对象被老年代的Region中所引 用,这时候新生代的该对象就不能被回收,怎么记录呢? 用一个类似于hash的结构,key记录region的地址,value表示引用该对象的集合,这样就能知 道该对象被哪些老年代的对象所引用,从而不能回收。
5. 如何开启需要的垃圾收集器
串行 :
-XX:+UseSerialGC -XX:+UseSerialOldGC
并行(吞吐量优先):
-XX:+UseParallelGC -XX:+UseParallelOldGC
并发收集器(响应时间优先) :
-XX:+UseConcMarkSweepGC -XX:+UseG1GC
下篇已发布:https://www.nowcoder.com/discuss/1157371
#java##JVM##java虚拟机#主要归纳JVM知识点