java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。比如kotlin、scala等。
JVM由三个主要的子系统组成
1.类加载子系统
2.运行时数据区(内存结构)
3.执行引擎

程序需要运行肯定需要java指令运行字节码文件,这个命令执行jvm底层会作三件事情
1.通过类装载子系统把java类装载到运行时数据区
2.然后通过执行引擎字节码执行引擎执行内存中的代码
堆

一般new的对象放在堆里面,但是new出来的对象放在堆里面哪个区域,放在Eden区
当Eden区放满之后,会产生minor gc,有什么作用,说白了就是jvm执行引擎会开启一个垃圾收集的线程对Eden区无效的对象做垃圾收集,把那些无效的对象清理之后给新的对象放
等到Eden区放满,回收一些垃圾,还有一些对象是有引用的就是存活的对象,存活的对象jvm会把它挪到survivor区,等到survivor区的From区放满之后也会触发minor gc,这时候会把From区和Eden区无效的对象都清理一遍,把From区有效的对象挪到To区
对象每经过一次minor gc没有销毁的对象,对象的存活年龄或者分代年龄都会加1
分代年龄存储在对象头里面有个分代年龄,这里存储多少gc
To区放满之后也会触发minor gc,经过minor gc之后还有存活的对象,这些对象会把它挪到From区,这时候To区被清空,等到From区放满之后,会触发minor gc,把无效的对象清理后把有效的对象挪到To区,这样反反复复,等到达到15次minor gc之后,对象还没有被回收,这些对象会被挪到老年代
用VisualVM工具jvm调优的辅助工具查看一下底层内存分配和流转的过程
默认的VisualVM工具里没有visualgc插件,只需要下载一个插件安装在VisualVM即可
可以通过jvisualvm命令启动VisualVM
通过可视化就可以看到具体底层内存分配和流转的过程
什么叫无效对象
没有被引用的对象
比如main方法执行完之后,整个jvm进程还没有退出,main线程执行结束,意味着main方法对应的栈帧内存区域被出栈被销毁,也就地址引用指针都没有了,就意味着堆的内存区域还在,这些对象还存活,对象处于游离状态,没有任何的指针指向它,后面还有新的线程要运行,新的线程怎么也找不到这个对象,说白了这个对象就是个无用对象,还占用这个堆内存区域就浪费资源
gc Roots根
比如虚拟机栈的本地变量表可以是gc Roots根,当这些指针还存在的时候,有很多对象,局部变量里面有本地变量有指针指向这个对象,在线程结束之前这个本地变量表里面的math局部变量就是gc Roots根,线程结束后这个gc Root根本地变量也被销毁,这些对象就游离了,jvm垃圾收集收集的就是这些比如这些对象从它的引用去找没找到gc Roots根,那么就会把这些对象回收掉
栈
一个程序启动运行的时候,如果是多个线程的,每个线程启动运行的时候jvm都会给这个线程在jvm内存区域里面分配一块独立的栈空间,它是一块内存空间,运行时数据区(内存模型)的几块组成部分都是jvm内存组成部分一小块组成部分的组成区域
局部变量都需要一块内存存储,这些局部变量在运作过程中需要内存区域存放它的数据的,这些局部变量需要的内存区域实际上都是被jvm分配到栈内存区域
在运行过程中都有个主线程,当主线程一运行jvm会给这个线程分配在内存区域里面分配一小块栈内存区域,单独针对main线程
栈内存区域
每个线程都有自己的栈内存区域,主要放线程运行的方法过程中局部变量需要的内存区域
public int computer(){
int a = 1;
int b = 2;
int c = (a+b)*10
return c;
}
public static void main(String []args){
Math math = new Math();
math.computer();
}
栈的内部有一系列的栈帧组成
栈帧
当系统运行时jvm会给main方法分配一块独立的内存区域,这块内存区域就是main方法对应的栈帧内存区域,主要放main方法的局部变量比如math局部变量,jvm继续运行到computer方法的时候jvm给computer方法单独开辟一块内存区域,也就是computer对应自己的栈帧内存区域,这块栈帧内存区域放的就是computer方法里面的啊,a,b,c局部变量
说白了一个线程下面可以会调用很多方法,每调用一个方法都会给这个方法单独开辟一块栈帧内存区域去存放这个方法自己的局部变量,这些局部变量在方法内部有效
一个方法对应一块栈帧内存区域

这个栈是jvm内存区域一小块的组成部分,实际上内部的数据组成结构就是用栈这种数据结构来存储的
比如主方法加一个System.out.println("test");
public int computer(){
int a = 1;
int b = 2;
int c = (a+b)*10
return c;
}
public static void main(String []args){
Math math = new Math();
math.computer();
System.out.println("test");
}
当computer方法执行完之后,computer的局部变量的内存区域都被销毁了,也就是这些局部变量的内存全部被释放了,也就是computer方法对应的栈帧就出栈了,当main方法执行完之后main()方法对应的栈帧也出栈了,这样就很符合栈的数据结构

我们了解一下jvm指令码,所谓指令码也就是java文件通过编译成class文件然后反汇编可以看到一些jvm指令码
其实栈帧里面不只是有局部变量,它有很复杂的结构,里面有
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口
public int computer(){
0:incost_1
1:istore_1
2:incost_1
3:istore_2
4:iload_1
5:iload_2
6:iadd
7:bipush 10
9:imul
10:istore_3
11:iload_3
12:ireturn
}
我们来看第一行指令码incost_1:把int常量1压入操作数栈
当线程执行computer方法底层执行第一行指令码incost_1的时候,它就会让jvm类执行引擎在jvm对应方法所属的栈对应的栈帧里面那个操作数栈里面放一个常量1
接下来执行第二行jvm指令码istore_1:把int类型值存入局部变量1,下标值为1的局部变量
int a = 1;
这块代码执行完之后就是把1分配到第一个局部变量a对应的内存区域
来看一下
istore_1这个jvm指令码执行完之后jvm底层运转过程
先在局部变量表里面给第一个局部变量下标为1的局部变量a在局部变量表里面开辟一小块内存区域,用来存放这个a这个局部变量对应的数据所需要的内存区域,然后再把1这个值放到a对应的内存区域
istore_1执行完之后,a这个值变为1


每一行jvm指令码前面对应的都有数字,这个数字实际上就是jvm指令码对应的行号,可以把这个数字当做jvm指令码内存映射的地址
程序计数器
程序计数器标识的就是马上要执行的下一行或者说正在执行的指令码对应的行号的位置
是每一个线程独有的

java应用系统中还有其它的线程,每一个线程运行过程中,jvm都会给这个线程分配一块独有的栈内存区域,程序计数器也一样,每个线程都会有独有的程序计数器
其实每运行一行jvm指令码jvm执行引擎都会动态的更新程序计数器
接着往下执行
iload_1:从局部变量1中装载int类型值
这一行jvm指令码就是把局部变量1也就是a的值load出来装载到操作数栈中
iload_2:从局部变量2中装载int类型值
这一行jvm指令码就是把局部变量2也就是b的值load出来装载到操作数栈中
接着往下执行
iadd:执行int类型的加法
jvm执行引擎开始执行加减乘除四则运算
jvm执行引擎会从操作栈栈顶盘出两个元素做运算,比如这两个元素做加法,比如说2+1=3,它会把3从新压回到操作数栈
接下来往下走
bipush 10
bipush:将一个8位带符号整数压入栈
哪个整数就是这个10,将操作数栈压入10这个常量
接下来往下走
imul:执行int类型的乘法
一旦jvm执行引擎在执行四则运算,它就会从栈帧所属的操作数栈栈顶判处两个元素做四则运算,做乘法运算运算结果出来后把这个结果压入操作数栈里
以前说过前面这个行号对应jvm指令对应的内存地址
7:bipush 10
看到jvm指令会发现8哪去了,7对应的是bipush后面还有一个10这个指令,这个也是一个执行,它也占用内存区域,你可以把8那个行号那个内存地址认为是10栈掉了
接下来往下走
istore_3:将int类型值存入局部变量3
也就是存入第三个局部变量里面,说白了就是把算出来30的结果存入局部变量c里面
istore_3执行过程jvm执行引擎做这些事情
computer方法对应的栈帧的局部标量表里面给局部变量c分配一块内存区域
为什么要分配一块内存区域,因为我要把30放到这块内存区域里面
接着往下走
11:iload_3
iload_3从局部变量3中装载int类型的值
这一行jvm指令码就是把局部变量3也就是c的值load出来装载到操作数栈中
因为马上要把这个值return
接下来往下走
12:ireturn
ireturn从操作数栈栈顶把30的值拿出来return掉
什么叫操作数栈
在程序运作过程中临时的操作数临时中转的内存存放区域
方法出口
一个方法return之后,方法出口里面放到实际是这个方法return那个主方法哪个位置上去
说白了这个方法return之后,当时调用方法的时候那个主方法对应程序计数器的值是多少实际上就是存到这个方法对应的方法出口里面去,回头根据方法出口里面的值告诉jvm执行引擎应该帮我返回main方法对应的哪一行jvm指令上去
看一下main方法
public static void main(String []args){
Math math = new Math();
math.computer();
System.out.println("test");
}
我们都知道new出的对象是放在堆里面的
main方法里面的这个math局部变量实际上是这个对象对应的一个地址指针一个引用
这边存储的是对象的地址指针的一个引用,在堆内存里面的地址指针一个引用

new的对象是存储在堆里面的,局部变量是存储在栈里面的,其实堆和栈有联系的,堆里存放的是对象的数据,对象的内存地址是存放在栈里面的
说白了栈里面有一些对象类型的引用,那些引用都是由指针指向堆里面的对象
方法区
class字节码文件都加载到方法区,底层都放一些常量,静态变量,类元信息
比如说静态变量是对象类型的
public static Object obj = new Object();
这个Object对象new出来的也是放在堆里面,静态变量实际是放在方法区,静态变量在方法区那边又有指针指向堆,这就是堆和方法区建立了联系
obj放在方法区,因为它是一个静态变量,Object对象放在堆里面
严格来说方法区并不算是jvm整个内存区域的一块,方法区也是一块内存区域,只不过它使用的不是jvm整个内存区域里面的内存,它使用的是操作系统的直接内存
类元信息:就是类的组成部分
比如Math.class 最终这个字节码文件被加载到方法区,需要通过类加载验证、解析一系列的过程,最终加载完之后这个字节码文件会被解析成一系列的类元信息
说白了就是类的组成部分,这个类的常量,静态变量,方法都是类的组成部分
对象头
对象头有一些对象的描述信息,还有一部分重要的类型指针,就是对象对应的类的类元数据的指针
当new一个Math对象生成一个math对象,对象数据会放在堆里面,实际上在生成这个对象的时候jvm会生成一个对象头,对象头有一个非常重要的指针,这个指针指向的就是这个对象对应的类的类元信息
动态链接
比如Math.class有很多方法,不同的方法对应jvm指令码的地址
动态链接里面存的就是这个方法在类元信息在方法区的地址
本地方法栈
来存放那些被native关键字标记的方法的局部变量,在Execution Engine执行时加载本地方法库