JVM_01
-
推荐阅读
- Java虚拟机规范(中文版译本)基于Java7:适合Android程序员
- 深入理解虚拟机规范:进阶
-
JVM是一种规范
- 更加强调约定的味道:只要为满足Java虚拟机规范的 .class 文件,JVM都可以执行
- 这点在JVM的语言无关性中有所体现
-
JDK包含:
- JRE:Java运行时环境,没有编译功能的,=JVM+Java类库
- JVM:虚拟机
- 某些工具:jhat.exe,这玩意儿在bin目录下有
-
Java程序执行流程
-
源程序(.java)经过编译(javac)到字节码文件(.class)
-
(满足Java虚拟机规范的)交由 JVM处理
-
JVM处理:
- 类加载器加载
- 执行(字节码解释器,JIT编译器)
- 执行引擎->硬件设备
-
-
解释执行(字节码解释器)
-
JVM是用C++编写的,JVM相当于拿到合乎规范的字节码文件后交由C++解释器进行处理
-
绝大多数代码执行,效率就比较慢
-
C++解释器:JVM帮助解释
-
实际上就是一系列的 if 语句
//可能是汇编等其他的,具体要看这个解释器 if(new){ 相应的C++代码,完成java中的new操作; }
-
-
-
JVM的执行:
- 分为客户端和服务端
-
即时编译(JIT热点代码技术)
-
当方法、代码的循环次数达到一定量(1W+):走热点编译(JIT,Hotspot)
-
此时不再经过JVM转化,直接从codecache(java程序编译的时候就比较慢了)里面拿出来就用了
-
关于JVM转换:这个还有点问题
- Java代码->字节码->(这步骤就很费时了)汇编->机器码
- 一般翻译成汇编就行了
-
-
JVM的跨平台特性
- 不同的平台都有相对应的JVM版本
- Windows,Linux,Unix,Android,Mac
-
JVM的语言无关性:语言只是将程序翻译为字节码文件
- JVM处理的原材料是符合Java虚拟机规范的字节码文件,至于它是怎么来的,不管
- JVM系语言:Java,kotlin,scala,grooxy
-
JVM的跨平台行+语言无关性决定了生态圈比较大
-
常见的JVM实现:多种实现方式
-
自己写个JVM实现
- 遵循Java虚拟机规范(底层实现无限制)
- 得到Oracle认证
- 自己再给他取个名字
-
Hotspot
- Oracle收购了Sun,从而得到了Hotspot,又收购了Bin,从而获得了Jrockit(这个东西背整合到了Hotspot中)
- 还有个ZGC(java11),这个是抄了zing的(算法借鉴嘛)
-
J9
- IBM自己的
-
LiquidVM
- Bin公司的
- 针对于硬件,直接进行操作,快得很
- 不需要安装操作系统,很快
- 这个就相当于是一个操作系统,甚至可以不再安装其他的OS
-
Zing
- 不开源,收费,还贵
- C4算法:垃圾回收停顿时间控制在1ms
-
TaobaoVM
- 阿里系的一个Hotspot定制版
-
毕昇
- 华为的 ,openJDK的一个定制版
- 针对IBM的ARM架构进行优化
- 兼容Java8,但是不能在Windows上跑,只能在Linux或者ARM上一些平台上跑
-
-
java的运行时数据区:
-
示意图:
-
运行时数据区:经过虚拟化了的,方便找对象
-
定义:Java虚拟机在执行Java程序时,将内存划分为若干个不同的区域
-
类型:
-
线程私有
- 虚拟机栈
- 本地方法栈
- 程序计数器
-
线程共享:堆,大多数空间分配都在堆上
-
方法区
- 运行时常量池
-
堆
-
-
-
-
方法区:永久代(JDK 1.7),元空间(JDK 1.8)都是Hotspot的,使用这两个实现了方法区
- 在Java虚拟机规范中明确提到了方法区的概念;
- 在Hotspot中以永久代(JDK1.7),元空间(JDK1.8)实现了方法区;因为Hotspot应用广泛,所以这两个概念,广为人知,甚至一度成为方法区的代名词
-
-
直接内存:C/C++中使用较多
- 独立于运行时数据区
-
未经过虚拟化的,也叫堆外内存;JVM在管理内存的时候将其虚拟化了,new出一个对象,拿到引用就可以操作了
-
案例:假如一个机器有8G内存,运行时数据区就占了5G,还剩3G。这3G内存,在Java中就可以通过某种方式直接进行操作,使用
-
但是,这样使用起来会很不方便(C++就很喜欢这个弄)
-
分配内存
-
分配地址
-
数据格式化
-
-
-
Java方法的运行与虚拟机栈:
-
虚拟机栈:存储当前线程运行Java方法所需的数据,指令,返回地址,一块内存(栈)
-
大小限制:
-
XSS,这个是可以设置的,默认值取决去平台
-
Windows/Linux默认值:1024KB
-
修改XSS:
- -Xss 1m 设置为一兆
- -Xss 1024 不显示指明单位时,默认为比特
- 不能使用代码进行动态调整的,设置好后,是多大就是多大
-
-
也可以查到
-
一般是1024KB(JDK1.8),这是JVM根据不同的OS与机器的具体情况设置的,它会推荐一个默认的容量
-
-
查看虚拟机栈信息
- 编写Java程序
- 命令打开JDK/lib 键入命令:
-
启动线程(运行Java程序):
-
创建一个对应的虚拟机栈
-
每一个Java方法都对应着一个虚拟机栈
-
Java实质上就是方法调方法
-
运行方法->创建一个栈帧(放在栈顶,构成了虚拟机栈,它是有一定大小的),若有方法内嵌,创建新的栈帧,每个栈帧是独立的,线程私有的
-
死递归:虚拟机爆栈 StackOverFlowError,栈溢出一般都是循环
-
当方法执行完后,栈帧出栈
public static main (String [] args){ public void A(){ A; } } -
代码示例
public static main (String [] args){ public void A(){ …… } public void B(){ …… } public void C(){ …… } }虚拟机栈:栈帧
-
-
栈帧里面到底有什么?
-
局部变量表,操作数栈,动态链接,完成出口
-
示意图:Java方法运行的内存区域
-
-
程序计数器:指向当前正在执行的字节码指令的地址(行号,这个要反汇编才能看得到的)
-
很小的一段内存单位,每个线程都有一个咩?
-
确保多线程的正常实现,保证JVM的正常运行,保证CPU可抢占且程序无异步现象(当执行到字节码文件的第X行时,调用其他的方法)
-
count:当前字节码文件的行号(有些指令可能会占据多行,所以count值不连续)
-
count:可能出现重复的值
- 程序计数器是线程私有的,那么每个方法都会对应一个栈帧,当方法执行完毕后,对应的栈帧就会出栈,在调用新的方法时,又会创建新的栈帧,程序计数器又会从零开始,所以难免会出现,count值重复现象
-
-
-
优秀博客:cloud.tencent.com/developer/article/135540
-
-
栈帧执行对于内存区域的影响:
-
示意图:
-
源代码
-
-
字节码文件:就是一个Class文件,JVM处理的对象
-
怎么样去找到这个字节码文件?
-
可以在编译后:
- 打开文件资源管理器,进入out/production/ref-jvm3/原程序包名,采用命令行打开
- 输入javp -V Person.class就有了
-
- 针对于work() 方法,行号就是字节码文件的地址,实质上是一个偏移量(拿给程序计数器),后面那个就是助记符。
-
源程序详细流程:
- 运行程序,开启线程
- 执行main方法,为其创建一个main方法栈帧,并将其压入虚拟机栈
-
当源程序执行到第8行时,调用work,触发虚拟机栈,开启新的线程,创建一个新的栈帧
-
源程序执行到第9行:
-
一行java代码(int x =1):对应两行字节码
-
对应字节码文件中的第0行,此时程序计数器的count=0,
iconst_1表示new出了一个值为1的int常量,并将这个常量1,压入操作数栈,此时该数位于栈顶
- iconst_X:将常量X压入操作数栈
- 这个X是有大小限制的
-
对应字节码文件中的第1行,此时程序计数器的count=1,
istore_1(存储命令)表示将操作数栈栈顶的数据放到局部变量表中下标为1的地方
-
在局部变量表(创建栈帧的时候就有这个表了)中,起点为第0行,默认值为this
-
为什么是this?
因为在调用的时候,如果是静态方法,那么这个地方就不要this,这个this只是指明了本方法的调用者是谁(是当前对象);如果说这个地方调用的是静态方法的话,就不需要this了,因为在这个类中,静态方法是跟类挂钩的,与具体的对象没有什么关系
-
-
存储命令(操作数栈--->局部变量表):istore_X(将操作数栈栈顶元素压入局部变量中下标为X的位置)
-
-
-
源程序执行到第10行:
- 原理类似,对应字节码文件中的第2,3行
-
当源程序执行到第11行(int z = (x+y)*10; ):对应字节码文件中的4,5,6,7,9,10,下对字节码文件进行分析
-
第4,5行(iload_1与iload_2):将局部表量表中下标为1,2的数据加载到操作数栈中(因为这两个值已经有了,不用再new了)此时程序计数器会跟着变;
- 加载命令:iload_X(将局部变量表中下标为X的数据压入操作数栈)
- 此时操作数栈中两个数据,并且2在1的上面
-
第6行(iadd):将操作数栈中的两个数据出栈(从操作数栈中取出,顺序为2,1),并相加,将得到的结果3重新压入操作数栈,且位于栈顶;
- 算术指令(iadd):运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并将结果重新存入操作数栈顶
此时,原来的两个操作数1,2就被扔掉了,那么扔到哪里去了?
-
第7行(bipush 10):将常量10,推入操作数栈
-
注意:这里是字节码指令 bipush 10,这是一条指令且占了两个地址空间,偏移量为2;实际上程序计数器记录的是字节码指令的偏移量;
-
字节码的行号是针对于本方法的偏移
-
对于int类型:
- icount_x:x只能在-1,0,1,2,3,4,5中选
- 其他的int就需要用bipush_x,进行push(凡是用了数据就要压入操作数栈)
-
-
第8行丢失:bipush占了两行
- 底层指令直接操作内存,针对于这个work() 方法,有些指令占据的空间就大
-
第9行(imul):将操作数栈的元素取出(顺序:10,3),并相乘,将结果重新压入操作数栈,此时操作数栈仅剩栈顶的30
-
第10行(istore_3):取出操作数栈栈顶元素并存入局部变量表下标为3的位置
-
第11(iload_3):将局部变量表中下标为3的元素取出,重新压入操作数栈
-
12行(ireturn):再执行 ireturn
- 方法的调用与数据类型无关,但是方法的返回指令根据返回值类型进行区分ireturn
- 将操作数栈的数据返回到main中去:执行栈帧之间的返回(栈帧之间是有嵌套的)
-
完成出口是什么?
- 当main中调用work()方法,假设此时字节码的行号为3,那么这个3就记录在完成出口中;当work()方法执行完后,程序接着从main方法的字节码行号为3的地方继续执行
-
动态链接是什么?
- 跟多态有关;
-
程序计数器中的数据可能重复,
-
因为方法之间会存在嵌套关系,那么栈帧之间也会存在嵌套关系,并且程序计数器中记录的是针对本方法的字节码偏移量;当内嵌方法执行完后,程序跳回上层方法,此时程序计数器中保存的从内嵌--->上层;就有可能出现重复;
-
但是这个是不影响的,因为:虚拟机栈同一时刻只会执行一个栈帧(就是最顶上的栈帧),执行引擎在执行代码的时候只会找最顶上的栈帧;有了新的方法调用那就压入新的栈帧就行了
-
程序计数器:确保JVM中单/多线程的正常执行,
- java中是不能自主控制线程的
- 当CPU切出去了,没有程序计数器记录当前执行到哪里了,那么CPU切回来的时候就又回重来;
- 记录每一个字节码执行的地址,也就是状态
-
-
-
操作数栈:栈帧在方法运行完了,这块内存就没有了
-
关于本地方法
- 在java虚拟机规范中指出:java虚拟机可能会使用传统的栈来支持native方法(指使用java以外的其他语言编写的方法)的执行,那么这个栈就是本地方法栈
-
Java中是不能直接操作线程的,使用本地方法(操作系统提供的库,可能是汇编等非JVM语系的代码)
-
本地方法栈结构与虚拟机栈基本类似
-
示例:public native int hashCode();
- native:这个就是本地方法,不是Java里面执行的
- 此时将栈帧压入到本地方法栈栈顶
- 此时程序计数器无法工作(程序计数器只能记录虚拟机栈,不能记录本地方法栈),count显示为空:因为本地方法中都不是这种字节码的形式了,始终为null
-
在Hotspot虚拟机中,本地方法栈与虚拟机栈使用同一块内存,不区分这个两个,调的时候看你调的什么方法(native方法就是调本地方法栈):只要合乎Java虚拟机规范就行了,对于具体细节不做深究
-
Java中的类加载机制
-
源代码:
-
ObjectAndClass类:放到方法区,类加载的时候放在方法区,
-
静态变量,常量,静态代码块,这些在类加载的时候会一并放到方法区
-
final static ObjectAndClass lobject = new ObjectAndClass ();12行的那个(改一下)
- 这段代码在类加载的时候不会执行
-
private boolean isKing;
- 成员变量在类加载的时候也不会管,它是跟随这个对象的
-
运行main方法:开启线程
- int x = 18;与long y =1;局部变量是放在栈帧里面的(操作数栈或者局部变量表,具体要结合实际情况)
-
在18行:ObjectAndClass object = new ObjectAndClass ();
-
new出一个对象:在堆中进行分配内存,所以说这个new ObjectAndClass)() 在堆
-
注意这行代码的object是局部变量(new 出的这个对象(在堆中))的引用
- 这个object是放在虚拟机栈中的局部变量表里面,并且指向堆中new出来的那个内存
-
注意:当执行完18行以后;那么就要对我new 出的那个对象调用构造方法初始化(完整类的成员变量的初始化),因为在类的成员变量里面还有一句ObjectAndClass lobject = new ObjectAndClass ();
那么,此时就会再次在堆中分配一个对象,并将其引用lobject 放到堆中的之前分配的那个对象里面去并且指向新分配的对象,这个引用lobject 不会放到虚拟机栈的局部变量表,因为他是之前new出来的 那个对象的类属性:object.lobject (就是这种关系)
-
-
直接内存:最后一行
ByteBuffer bb = ByteBuffer.allocateDirect(128*1021*1024)-
直接内存可以不释放,JVM中会不断轮询,Union引用实现内存回收
-
直接内存在源码中在Unsafe类中处理(需要反射修改权限才能玩)
-
在java9的时候打算去掉但是没有(大量的框架都用了)
-
Unsafe类:
- 直接操作对象
- 直接分配内存(绕过JVM,快,但是忘了释放导致内存泄漏,玩多线程可能造成内存覆盖),直接取地址,直接设置地址,直接 设置内存,直接cpoy内存,拿到方法区
-
-
- 注意:在此时new出一个对象后会触发构造方法,再次回到12行,再次创建一个对象,再次触发构造方法,再次回到12行,就死循环了;可以在12行前面加上public static解决这个问题?还是有点疑问
-
-
运行时数据区的其他区域
-
本地方法栈
- 为JVM使用到本地(Native)方法服务
-
方法区
- 永久代与元空间
- 运行时常量池
-
直接内存(堆外内存)
-
没有经过虚拟化
-
可以使用Unsafe 类进行操作
- 直接操作对象
- 释放内存
- 查看方法偏移量,方法域
- 找到 .class在哪里
- CAS操作
- park操作:阻塞线程
- unpark操作:唤醒线程
- 内存屏障
-
Unsafe绕过了JVM的垃圾回收,相当于是一个手动的方式
- 优点:快,但是用ZGC就差不多了
- 缺点:忘记释放内存导致内存泄漏,在处理多线程问题的时候极易造成覆盖
-
-
-
-
深入理解JVM内存区域:
-
代码执行时JVM的处理流程
-
JVM申请内存:
- 方法区:放Class,静态常量
- 堆
-
初始化运行时数据区
-
类加载
- 将Class,静态变,常量放到方法区
-
执行方法:
-
运行一个方法main
-
创建虚拟机栈
-
main方法里面放一个栈帧
- 放T1,T2(这两个只是引用,跟普通的变量基本没有区别)指向堆中的对象
- 只能说现在基本上都是32位指针,到JDK11,强制使用64位指针,此时一个引用就会占据局部变量表中两个局部变量
-
-
-
-
创建对象
- 当引用不再存在,堆中的对象被回收
-
-
-
从底层深入理解运行时数据区:就是对真实地址进行虚拟化
-
代码展示:
public class JVMObject { public final static String MAN_TYPE = "man"; // 常量 public static String WOMAN_TYPE = "woman"; // 静态变量 public static void main(String[] args)throws Exception { Teacher T1 = new Teacher();//这个对象在哪里??哪个地址 T1.setName("Mark"); T1.setSexType(MAN_TYPE); T1.setAge(36); for(int i =0 ;i<15 ;i++){ System.gc();//主动触发GC 垃圾回收 15次--- T1存活(因为仍有引用指向这个对象) T1要进入老年代 } Teacher T2 = new Teacher(); T2.setName("King"); T2.setSexType(MAN_TYPE); T2.setAge(18); Thread.sleep(Integer.MAX_VALUE);//线程休眠 T2还是在新生代 } } class Teacher{ String name; String sexType; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSexType() { return sexType; } public void setSexType(String sexType) { this.sexType = sexType; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } -
示意图:
-
新生代:蓝色的,老年代,橙色的(经过多次垃圾回收还存活)
-
特殊命令:
- java -cp 执行特定的java类,这个是要配合JHSDB来玩的
- jps:查看具体的进程
-
使用JHSDB
-
java - cp:在命令行执行,要有一个jar包
-
jps:找出现在虚拟机中正在运行的java程序的进程号
-
附着:将这个进程号穿进去
-
开始用了
-
可以去看堆空间的具体划分:这三个区域是紧凑,贴着的,可以看到具体的01代码,来划分
- Eden
- From
- To
-
-
-
内存溢出:
-
当程序实际使用的空间大于,虚拟机参数(Xms),抛出异常
- Metaspace:方法区溢出
- heap space:堆空间溢出
- stack overflow:栈溢出
- Direct buffer memory:直接分配内存,导致溢出
-
-
-
问题
-
动态链接:
- 基于多态,运行的时候才知道这个方法在哪里。
- 通过晚期绑定,使用的其他类的方法和变量在发生变化时,不会对调用他们的方法造成影响
-
操作数栈是属于执行引擎的一部分:有虚拟化
-
为什么是15次?
- 对象经历一次垃圾回收,没有被回收,age+1
- age达到15晋级老年代,因为年龄就是四位二进制记录的
-
构造方法运行时是栈帧吗?
- 就是
-
一个程序所产生的字节码文件,会在程序跑起来后,一次性加载还是按照需求来?
- 尽可能按照需求
- 内部有机制的
-
方法执行完,栈帧消失,那么操作数栈的东西是怎么返回到上一层?
- 操作数栈不会消失,这个是执行引擎的一部分
- 这个类似寄存器,所有的数据都会进入操作数栈,操作数栈可以复用
- 执行引擎 栈(类似告诉缓存) 堆空间
-
所有类会一次性加载到方法区吗?
- 按照需求来
-
Unsafe类的是真实地址吗?
- 操作系统给出的01地址,可能存在映射;
-
方法没有返回值,那么还有完成出口吗?
- 有啊,只是说你返回上一层后,不会携带数据
-
JVM处理的 .class 文件到底是什么?
-
类是静态资源,放到方法区
- 对
-
栈帧里面有什么?
-
局部变量表
- 根据程序,保存操作数栈的某些元素
- 第一行一般为this,费静态方法
-
操作数栈
- 只要处理数据,那么数据就要压入操作数栈
- iload_3:将局部变量表中下标为3的元素压入操作数栈
-
动态链接
- 跟多态相关
-
完成出口
- 记录跳出方法的地址(有点问题)
- 比如说,在执行main的过程中,当执行到main对应的第3行字节码时,执行Person person=new Person ();//当执行完成后,回到main的栈帧,接着从第三行字节码执行下去;
-
-
-
编程经验
-
内存优化:
- 问题描述:只有200m的内存,现在有500个方法需要运行(一般情况下,一个方法对应一个大小为1m的栈帧)
- 处理(调整栈帧大小):-Xss 256kb
- 一般情况下,循环几百,上千次才会出现爆栈
-
反汇编(有点问题):
- 找到.class文件,
- 进入字节码文件,javap -Person.class
-
在IDEA中如何查找 .class文件
- out/production/ref-jvm/ex
-
\