JVM虚拟机原理与调优详解
文章目录
JVM java虚拟机
JVM从软件的层面屏蔽了底层硬件指令层面的细节
JVM是什么
- java源码文件——>.class文件
问题:操作系统底层对应的都是机器码010101
不同的操作系统底层对应的机器码、指令是不同的
JVM来解决这个问题 从软件层面屏蔽了底层硬件指令层面的细节
在下载的时候就将操作系统进行了分类 linux与java
- c c++跨平台性
- redis
- JDK JRE JVM
整个的java知识体系就是JDK
JRE叫做java运行时环境 (jdbc lang util等等)
JVM 分类:
1.Java Hotspot Client VM
2.Java Hotspot Sever Vm
3.Selaris linux Windows other
JDK>JRE>JVM
JDK与JRE之间的差别
JDK比JRE多了Tools&Tool APIS
Tools&Tool APIS是java自带的工具 java javac javadoc等等
源码文件到.class文件 是JDK编译时环境
java的跨平台:JVM跨平台 属于JRE运行时环境
c与c++的跨平台是在编译时环境进行的跨平台 分操作系统的类库
JVM底层的原理
- Java虚拟机底层的构成
- 内容
- 类加载子系统
- 执行引擎
- JVM运行时数据区 在计算机内存中
源码文件 生产字节码文件.class 存储在本地计算机硬盘中
java HelloWorld.class 执行字节码文件
类加载子系统将.class加载到JVM运行时数据区
JVM运行时数据区在我们计算机的内存中
执行引擎去执行它 也就是CPU分配时间调度去执行它
即类加载子系统--JVM运行时数据区--执行引擎
执行引擎去执行代码
硬件上来说执行引擎是cpu 软件角度上说是线程
JVM运行时数据区
- 内容:
- 堆
- 栈
- 本地方法栈
- 方法区(元空间)
- 程序计数器
其中 堆与方法区属于线程共享数据
栈 本地方法栈 程序计数器器 属于线程私有数据
处理高并发的项目的时候 并发问题 :就是当用户在同一时间访问同一共享数据区域 得到的结果会与理想结果存在差异 例如:100个线程执行i++ 结果有可能不是100
代码 由main线程执行
main线程里包含了栈 本地方法栈 程序计数器这三块线程私有数据
每个线程有它独立的栈 本地方法栈 程序计数器空间
栈
- 栈(先进后出)–数据结构–存储内容
- 栈来存储方法的结果 根据方法来隔离
- 例如 栈里面存储着main栈帧与add栈帧
public int add(){
int d=100;
return 100;
}
public static void main(String[] args){
Onject o=new Object();
o.add;
}
执行过程:
- main栈帧进入栈
- add栈帧进入栈
- add栈帧在上面 main栈帧在下面
- add栈帧结束后出栈
- main栈帧结束后出栈
- 符合先进后出原则
栈帧中的内容
- 栈帧
局部变量表
操作数栈
方法出口
附加信息
...
即上述add栈帧与main栈帧都有这些内容
javap命令可以对字节码文件进行反汇编
字节码文件运行在JVM虚拟机 虚拟机依赖于操作系统 操作系统底层是机器码 JVM在这个过程中充当了 字节码与机器码之间的转换角色
- int a=1;
iconst-1 将int数据1 压入操作数栈
istore-1 将局部变量a存入局部变量表 并且 操作数栈中的1赋值给a
即局部变量表存储着a=1;
- int b=2;
iconst-2 将int数据2 压入操作数栈
istore-2 将局部变量b存入局部变量表 并且 操作数栈中的2赋值给b
即局部变量表存储着b=2;
程序计数器 与 行号
- 程序计数器:指向当前线程所执行的字节码指令的地址行号
- 前面的数字就是行号
- 程序计数器修改字节码指令的地址行号 执行引擎通过行号来执行操作
- iload-1 从局部变量表的a=1装载int数据1 装载到操作数栈
- iload-2 从局部变量表的b=2装载int数据2 装载到操作数栈
操作数栈也是栈 先进后出 所以 2在1的上面
- iadd 执行int类型的解法 2与1出栈 执行2+1=3 然后 2与1小事 只保留3
- bipush 将8位带符号整数压入栈
7为什么直接到9
7其实包含两个指令
7 bipush8位带符号整数压入栈
8 8位带符号整数对应100
也就是将100 压入操作数栈
-
imul 执行int类型的乘法 100*3=300 100 与3消失 300存入操作数栈
-
istore-3 将操作数栈中的int类型 存入局部变量表中的变量c
-
iload-3 将局部变量表中的c的值加载进来 将300加载到操作数栈
- ireturn 从add方法栈帧中返回int数据 从方法出口将300传递到main栈帧中的局部变量表 即result=300
- HelloWorld app=new HelloWorld();
app存储在JVM运行时数据区的堆中 通过main栈帧的局部变量表中的app指向堆中的app
本地方法栈
- Thread.sleep();
Sleep()被native修饰的方法 java没有实现 调用底层的c与其他实现
元空间(方法区)
- 堆中存储的类的实例化对象obj 指向方法区的类class
堆(重要)
垃圾调优
性能调优
- JVM运行数据区的堆底层划分
假设600m内存
堆中老年代占比三分之二 400m
堆中新生代占比三分之一 200m
新生代中Eden占比 五分之四 160m
新生代中from占比十分之一 20m
新生代中to占比十分之一 20m
新生代 老年代
Eden 包含from to
98%的对象在新生代的Eden伊甸园区创建
垃圾回收机制
新生代的minor gc
当创建的对象占满了堆内存中新生代的Eden时 会触发java的垃圾回收机制 minor gc
- 第一次触发:当触发了minor gc时 会触发gc root 根的可达性判定
对当前创建的所有对象都进行判定
判定方式为:是否被其他地方所调用
- 如果此对象没有被调用 gc root会给此对象标记 标记为游离状态 认为是可回收的
- 如果此对象有被调用 该对象就是从Eden离开进入from区 并且该对象one会增加一条age=1;
- 第二次触发:当Eden区的对象又满了的时候 再次触发minor gc 进行gc root 根的可达性判定 判定方式同上
如果two对象被调用 将他传入from区 并增加age=1;
这时 如果之前的one对象还是处于被调用的状态 那么他的age变为2 age=2;
并且 将one与two对象拷贝到to区
to区变为from区 froom区变为to区
- 第三次触发:
当Eden区的对象又满了的时候 再次触发minor gc 进行gc root 根的可达性判定 判定方式同上
如果three对象被调用 将他传入from区 并增加age=1;
这时 如果之前的one对象还是处于被调用的状态 那么他的age变为3 age=3;
如果two对象不被调用了 two对象被移除
然后 将one与three对象拷贝到to区
to区变为from区 froom区变为to区
…
当minor gc进行15次之后 即one的age=15的时候
JVM会认为one对象是“老不死的对象”
此时one对象会进行晋升 晋升到老年代
此种方式 称为 担保机制
老年代的 full gc
- 晋升
- age=15 老年代
- 当前创建对象内存占比50% 会晋升老年代
假如from内存为40m 当from区中的对象占比20m时 这20m对象都会晋升 成为老年代
触发full gc会出现 STW现象(停顿现象)
JVM分配内存在进行压缩的时候 会占用一些资源 导致程序会出现不能响应的问题 即STW
STW Stop-The-World
是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起
例如 双十一 不能出现停顿
Java VisualVM 自带的监控整个对象内存分配情况
问题:为什么JAVA需要进行性能调优
当我们创建的对象越来越多的时候 堆所使用的内存越来越多 JVM分配的内存也会越来越大
当JVM分配的内存达到临界值的时候 会进行JVM分配内存的压缩 进行性能调优 也就是垃圾回收
进行性能调优的原因 :
在有限的空间做更多的事情
问题:为什么JAVA需要采用分代回收的策略思想
对象在新生代存活15次 尽量在新生代就被回收
实在不行 再晋升老年代
JAVA采用分代回收思想的原因: