JVM基础知识
从编译到执行
-
HelloWorld.java通过javac编译成HelloWorld.class
-
JVM中的ClassLoader类加载器把HelloWorld.class加载到JVM中
-
再通过字节码解释器的执行引擎运行在操作系统上
-
在JDK底下有一个JRE(java运行时环境),里面有很多java类库
-
在JRE底下有一个JVM,也就是java虚拟机
-
任何一个class要运行,除了走字节码解释器执行,还可以通过JIT编译器来执行
-
jvm把class这个字节码文件翻译成操作系统底层能够识别的机器码
JVM的跨平台与语言无关性
- 写一个类,编译成class
- 这个class文件可以跑在windows、linux、mac等不同操作系统上,但是前提是要在官网上下载相应操作系统的JDK
- JVM并不识别java语言,识别的是class字节码,除了java,还有scala、kotlin、groovy都有字节码,这称为语言无关性
- 奠定了很强大的java生态圈,比如kafka
常见的JVM实现
-
Hotspot
- oracle
- 在jdk1.8融合了Jrocket
-
Jrocket
- 原先属于BEA,后被oracle收购
-
J9
- IBM
- 只能用在IBM自己的产品上
-
TaobaoVM
- 淘宝定制版
- 是hotspot的深度定制版本
- 考虑了电商场景
- 进行垃圾回收时可以定制化,垃圾回收普通虚拟机是根可达,除此之外,还可以根据业务可达、seesion可达
-
LiquidVM
- BEA
- 底下没有操作系统
-
zing
- 同名公司zing
- 很贵
- 垃圾回收效率非常高,可以控制在1ms
- hotspot吸收了里面的一个垃圾回收算法,hotspot又推出了zgc
-
虚拟机本身有一套规范,唯一没按规范的是微软
JVM知识体系
- 核心:内存结构,以下的7项都跟内存结构相关
- 性能调优
- 垃圾回收
- JVM自身优化技术
- 执行引擎
- 监控工具
- 类文件结构
- 类加载
JVM的内存区域
- class其实就是jvm的指令,同时也需要模拟内存
运行时数据区域
- Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
结构
-
JVM运行时数据区
-
线程私有区(每个线程都有一个),如果启动了多个线程,就会有多个线程私有区,就会有多个虚拟机栈
(1)虚拟机栈
(2)本地方法栈
(3)程序计数器
$1.虚拟机栈和程序计数器配合使用执行java方法
$2.native方法是在c/c++中实现的,比如操作系统中有dll的动态连接库,native方法也是通过类似的手段调用了操作系统底层的某个方法,像这些native方法是在本地方法栈中执行的
##为什么一定要有本地方法栈?
虚拟机规范,只要有这个概念,并没有规定实现一定要按什么样来实现
-
线程共享区
(1)方法区
$1.方法区除了方法区,还包括运行时常量池
(2)堆
-
-
直接内存
-
假如电脑操作系统有8g内存,JVM虚拟化了3g,则直接内存为5g
虚拟机栈
- 存储当前线程运行java方法所需的数据,指令、返回地址
模拟
-
package ex1; /** * @author King老师 * JAVA方法的运行与虚拟机栈 */ public class MethodAndStack { public static void main(String[] args) { A(); } public static void A(){ B(); } public static void B(){ C(); } public static void C(){ } }
-
假设我们启动一个线程1来运行main方法
-
1.JVM要启动一个虚拟机栈,栈的特点是先进后出
-
2.会往这个虚拟机栈压入栈帧1-main()
-
3.继续往这个虚拟机栈压入栈帧2-A()
-
4.继续往虚拟机栈压入栈帧3-B()
-
5.最后往虚拟机栈压入栈帧4-C()
-
6.C方法执行完毕,栈帧4-C()出栈,然后就被销毁了
-
7.B方法执行完,栈帧3-B()出栈
-
8.A方法执行完,栈帧2-A()出栈
-
9.main方法执行完,栈帧1-main()出栈
大小限制
- –Xss
- 具体的VM命令设置见—https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- 设置线程栈大小,linux64位下默认是1M,不同操作系统是不一样的
栈异常
- 栈溢出—StackOverflowError
- 递归方法没有出口
- 国外社区StackOverflow,查很多异常信息是十分方便的,能查到很多百度查不到的,把日志贴上去很快就能找到相应问题的解决方法
栈帧
- 虚拟机栈最核心的方法是栈帧
结构
- 1.局部变量表,方法中的变量,不但包括8大基础类型变量,还包括对象的引用
- 局部变量表从第0行开始,其中存储的是this
- 2.栈帧最核心的部分-----操作数栈
- 也是一个栈
- 操作数栈是执行引擎的一个工作区
- 如果要自己造操作系统,一般是由CPU+缓存+主内存,这些东西不能是文件,主内存就是放数据的地方,CPU运算的时候先把数据从主内存放到缓存中,然后再利用缓存中的数据去运算
- 既然JVM是一个模拟版本的操作系统,在操作系统之上再架一个操作系统,对应上面的结构可以对照为 JVM执行引擎+操作数栈+(栈、堆),所以可以把操作数栈看成操作系统中的缓存
- 3.动态连接
- 是java多态动态语言的特性,必须结合class文件和执行引擎
- 4.完成出口
- 每一个栈帧有完成出口,也可以称为返回地址
- 字节码文件中iconst_1左边的数据就是字节码地址或对应的行号
- 方法出栈后,执行引擎会返回程序计数器记录的行号
模拟栈帧
java文件
-
package ex1; /** * @author King老师 * 栈帧执行对内存区域的影响 */ public class Person { public int work()throws Exception{ int x =1; int y =2; int z =(x+y)*10; return z; } public static void main(String[] args) throws Exception{ Person person = new Person();//person 栈中--、 new Person 对象是在堆 person.work(); person.hashCode(); } }
class文件
-
不能直接用notepad打开,打开是乱码,直接打开class文件所在目录的命令行,并输入javap -c Person.class,对class字节码进行了反汇编
-
Compiled from "Person.java" public class ex1.Person { public ex1.Person(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int work() throws java.lang.Exception; Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: new #2 // class ex1/Person 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method work:()I 12: pop 13: return }
-
为什么work()中没有第8行?
这只是简单的近似位,因为JVM是通过c/c++编写的,c/c++中有一个偏移量的概念,这里的数字代表字节码相对于work方法的偏移量
-
java虚拟机字节码指令集—https://cloud.tencent.com/developer/article/1333540
-
iconst表示将1个常量加入到操作数栈中
jvm中执行过程
- 1.首先启动一个线程,创建一个虚拟机栈
- 2.main()-栈帧入栈
- 3.work()-栈帧入栈
- 4.执行work方法
- 1.iconst_1— 将1压入操作数栈中
- 2.istore_1 — 从操作数栈把1取出来,并将1放入局部变量表第1行
- 3.iconst_2 — 将2压入操作数栈中
- 4.istore_2— 从操作数栈把2取出来,并将2放入局部变量表第2行
- 5.iload_1 — 将局部变量表下标为1的变量加载到操作数栈中
- 6.iload_2 — 将局部变量表下标为2的变量加载到操作数栈中
- 7.iadd — 取出操作数栈栈顶的两个元素2和1,放入执行引擎相加,得到结果3,并将结果重新放入操作数栈中
- 8.bipush 10 — 把10的常量加到操作数栈中
- 9.imul — 取出操作数栈栈顶的两个元素10和3,放入执行引擎相乘,得到结果30,并将结果重新放入操作数栈中
- 10.istore_3 — 把30存放到局部变量表下标为3的位置
- 11.iload_3 — 将局部变量表下标为3的变量加载到操作数栈中
- 12.ireturn — 方法返回指令,ireturn说明是int类型,执行引擎只能操作操作数栈,将30这个返回值返回
程序计数器
-
指向当前线程正在执行的字节码指令的地址
-
这是一块单独的区域,是一块很小很小的内存
-
比如方法转跳、进行恢复、异常处理都需要程序计数器
-
为什么需要程序计数器?
- 操作系统:CPU时间片轮转机制,1秒钟对于CPU太慢了,所以CPU对1秒钟进行切片,假设按1ms切片,切成了1000片,之前的work方法需要占用两个时间片,但是操作系统层面并没有规定一定是连续的,假设work方法执行到第3行,事件片用完了,必须挂起或阻塞,来等下一个时间片,所以必须记录当前执行到第3行,确保jvm在时间片轮转机制下正常运行
- 本质上是对操作系统的程序计数器的映射
-
比如work方法,程序执行到第7行,程序计数器里面count=7
-
程序计数器是在内存里面唯一不会内存溢出(OOM)的区域
方法区
-
类加载时是把某一个类加载进虚拟机,而方法区就是去加载相关的类信息
-
方法区是虚拟机规范中规定的,是一个逻辑划分,任何版本的虚拟机都有一块区域进行逻辑划分,叫做方法区
永久代和元空间
- jdk1.7方法区的实现叫做永久代
- jdk1.8之后方法区的实现叫做元空间
运行时常量池
-
常量池分为运行时常量池和静态常量池
-
静态常量池
-
之前反汇编是通过javap -c Person.class指令,如果换成javap -v
Person.class
-
Classfile /C:/Users/liusiping/AppData/Local/Temp/WeiyunDisk/tencent_weiyun_open_file_temp/{D9D05872-9A22-40B9-A327-39893EAC0A5A}/ref-jvm3/ref-jvm3/out/production/ref-jvm3/ex1/Person.class Last modified 2020-7-16; size 622 bytes MD5 checksum ddf1a0c6629c54ff1cc57a76de3a42af Compiled from "Person.java" public class ex1.Person minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#28 // java/lang/Object."<init>":()V #2 = Class #29 // ex1/Person #3 = Methodref #2.#28 // ex1/Person."<init>":()V #4 = Methodref #2.#30 // ex1/Person.work:()I #5 = Class #31 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 LocalVariableTable #11 = Utf8 this #12 = Utf8 Lex1/Person; #13 = Utf8 work #14 = Utf8 ()I #15 = Utf8 x #16 = Utf8 I #17 = Utf8 y #18 = Utf8 z #19 = Utf8 Exceptions #20 = Class #32 // java/lang/Exception #21 = Utf8 main #22 = Utf8 ([Ljava/lang/String;)V #23 = Utf8 args #24 = Utf8 [Ljava/lang/String; #25 = Utf8 person #26 = Utf8 SourceFile #27 = Utf8 Person.java #28 = NameAndType #6:#7 // "<init>":()V #29 = Utf8 ex1/Person #30 = NameAndType #13:#14 // work:()I #31 = Utf8 java/lang/Object #32 = Utf8 java/lang/Exception { public ex1.Person(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lex1/Person; public int work() throws java.lang.Exception; descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn LineNumberTable: line 9: 0 line 10: 2 line 11: 4 line 12: 11 LocalVariableTable: Start Length Slot Name Signature 0 13 0 this Lex1/Person; 2 11 1 x I 4 9 2 y I 11 2 3 z I Exceptions: throws java.lang.Exception public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class ex1/Person 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method work:()I 12: pop 13: return LineNumberTable: line 15: 0 line 16: 8 line 17: 13 LocalVariableTable: Start Length Slot Name Signature 0 14 0 args [Ljava/lang/String; 8 6 1 person Lex1/Person; Exceptions: throws java.lang.Exception } SourceFile: "Person.java"
-
新方法反汇编出来的文件里面就有常量池,在这个class文件里面就有一大块内容来表示常量池,包括定义的常量、方法的很多东西
-
-
运行时常量池
-
当我们把class文件加载到常量池时,它会把符号引用替换成直接引用
$符号引用
比如Tool.hashcode(),这就是一个符号引用
$直接引用
-
字面量
- Stirng a = “b”;
- b就是字面量
方法区运行步骤
- 首先进行类加载,放class文件,class文件之后要进行解析,在解析的过程把一些东西放到常量池中间来
堆
- 几乎所有的对象都在堆中分配
直接内存
- 堆外内存,JVM在运行时除了自己虚拟化的内存,还有操作系统本身剩余的内存,比如Nio中的DirectByteBuffer
- sun.misc.Unsafe类就是可以操作内存的
- EHcache、中间件大量使用了直接内存
JVM的整体内存结构
- 机器内存
- 运行时数据区
- 堆:共享的,要回收的
- 方法区:共享的,静态的,要经常使用的
- 栈区
- 直接内存/堆外内存
- 运行时数据区
总结
- 1.一个线程只有一个程序计数器
- 2.iload_3入栈后,这个数值仍然存在于局部变量表中,局部变量表的数据不会删,但是栈帧出站了,局部变量表、操作数栈就都没有了
- 3.时间片轮转,反汇编的行号为什么不连续?因为有的指令需要多个字节的操作数。
- 4.运行时常量池中符号引用变为直接引用是什么意思?
- 5.栈溢出的主要原因是不断的压栈帧
- 6.栈帧出栈后不需要回收,这块区域就没有了,跟随线程销毁
- 7.ireturn到哪里?属于一个指令,根据返回值类型进行区分返回
- 8.不是操作数栈溢出,指令集设计有问题,是虚拟机栈溢出
- 9.符号引用,不指代地址,jvm运行后当变成直接引用,就代表了具体的地址。