定义
JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作
JVM通过执行引擎将通过类加载器加载.clsss文件的数据放在运行时数据区进行解释执行.
JVM运行时数据区
运行时数据区可划分为二个基本区: 线程私有和线程共享
线程私有: 程序计数器,虚拟机栈,本地方法栈
程序计数器: 它主要是记录线程运行到哪里,比如一个线程里代码运行到一半,忽然CPU执行权被其它线程抢走了,这时就是程序计数器就要记录下我这线程运行到这,然后下次抢到执行权又从记录的这一块开始
虚拟机栈: 虚拟机栈是由一个一个的栈桢组成,一个方法就是一个栈桢,它的模式是先进后出,先进的方法,是最后出栈的
栈桢: 由局部变量表,操作数栈,动态连接,完成出口组成
局部变量存储的是8大基本类型和类的引用 当一个方法里执行了int a = 1;时,这个时候1就会入操作数栈,然后再将a=1储存到局部变量表中
操作数栈是解释执行虚拟机数据的
动态连接这个一般是处理多态,比如一个人,它可以是男人也可以是女人,当开始创建一个男人,男人要上厕所,执行WC方法,过不多久又又上面男人的引用创建了一女人,女人这里也要上厕所,执行WC方法,动态连接这个就是用来是执行男人上厕所方法还是女人上厕所方法
完成出口方法要出栈肯定有个结束,有返回值这个时候也要将返回值储存到局部变量表中
本地方法栈: 主要是存放native方法的
线程共享: 方法区,堆
方法区:一片连续的堆空间主要存放的是静态变量,常量,即时编译后的代码
即时编译后的代码就是匿名内部类,动态代码生成的内存类
共享区域为什么要用二个区域来表示呢? 因为方法区里的内存基本是不变的, 而堆里面的内存会出现频繁回收,所以为了效率就分为了二个
而我们Androdi中的内存优化主要涉及的就是堆这一块内存
堆内存又分为几大区域,新生代,老年代 元空间(永久代)
新生代: 一般情况下新来的对象都是先来新生代报道的,新生代又分为三个区域,eden from to三个空间,为为什么要三个,主要涉及到了垃圾回收算法
老年代: 一个对象经过垃圾回收15次还没有回收掉的时候,这处存活很高的对象就会转移到老年代
元空间: 存放的是方法区里的内存,但是也会出现OOM,因为即时编译后的代码过大就会
Android应用程序是运行在Dalvik/ART虚拟机上,并且每一个应用程序都有自己独立的虚拟机.
Dalvik虚拟机也算是一个Java虚拟机,只不过android执行的是dex文件 ,而Java执行的是.class文件
Android虚拟机和Java虚拟机特性其实是差不多了,它也是基于Java虚拟机开发出来的,差别就在于二者执行的指令集不一样,andorid是基于寄存器,而java是基于操作数栈的
public class Person {
public int work(){
int x= 1;
int y = 2;
int z = (x+y)*10;
return z;
}
public static void main (String [] args){
Person person = new Person();
person.work();
}
}
上面这一段代码是一个java文件 ,它要运行的话会通过javac把Person.java文件编译成Person.class文件
通过
javap -c Person.class命令会将这个class文件编译成字节码汇编码,我们的代码是会执行的,也就是说它会像指针一样走到哪里就指到哪里
public class com.vanke.lib.Person {
public com.vanke.lib.Person();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int work();
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[]);
Code:
0: new #2 // class com/vanke/lib/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
}
Code:这个行号,它是针对方法体的偏移量,大体上可以解释为是程序计数器记录字节码的地址
虚拟机栈(好比一个子弹夹) 一个个子弹为一个个栈帧(一个栈帧就是一个方法)
栈帧:
栈帧:包括:局部变量表 操作数栈, 动态连接,完成出口 大小是受限制的可以通过-Xss来修改
局部变量表:存储局部变量的,比如work()方法里的int x; int y; 只能存储8大基本类型加一个引用. 引用就是上面说的Person person = new Person();这行代码里红色标记的东西
操作数栈: 方法的操作(比如打枪的方法,用手打,用脚打,各种方式)
就比如上面的例子
一个线程有一个虚拟机栈,当执行到main()方法里,main()方法就是一个栈帧,这个时候main()方法入栈
在main()方法里走着走着,它会调用work()方法,这个时候main()它会被压下去,然后work()方法入栈
现在我们主要来分析一下work()方法,上面我们通过一行命令将.class文件翻译成了机器码如下
public class com.vanke.lib.Person {
public com.vanke.lib.Person();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int work();
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
}
当执行到iconst_1 将int值为1 入操作数栈
当执行到istore_1时 将操作数栈栈顶的int型数值存入到局部变量表,存在局部变量表下标为1的位置,在局部变量表中的第0位置存的基本是this,如果是静态方法则不是this.因为非静态方法要通过this才能调用其它方法
当执行到iconst_2 将int值为2 入操作数栈
当执行到istore_2时 将操作数栈栈顶的int型数值存入到局部变量表,存在局部变量表下对应位置
iload_1 将x拿出来压入操作数栈
iload_2 将y拿出来压入操作数栈
iadd 先将2出栈 再将1出栈再执行相加得到3 再把3压入操作数栈
bipush 10 将10的值拓展为int压入操作数栈
imul 10出栈 3出栈 然后相乘 得到30压入操作数栈
istore_3 将操作数栈的30存入局部变量表
iload_3 因为要返回所以要将30这个值压入操作数栈
ireturn 返回
java 解释执行基于操作数栈 c是基于寄存器
优缺点: 寄存器快,寄存器是基于硬件,缺点移植性差. 操作数栈兼容性好,速度偏低一点点
动态连接 (java里有多态,表态分派,动态分派)
Women extends Person
Man extends Preson
Person ancely = new Women();
ancely.work();
ancely = new Man();
ancely.work();
如上面一段代码 work()方法JVM不知道执行谁的work方法,所以就有一个动态连接来确定
完成出口: (返回地址,正常会调用程序计算器中的地址进行返回)
本地方法栈
本是方法栈是保存navite方法的信息
当jvm创建的线程调用native方法后,jvm不再为其在虚拟机栈中创建栈桢,jvm只是简单的动态链接并直接调用native方法, 程序计数器是不会记录的
HotSpot直接把虚拟机栈和本地方法栈合并
方法区(<=JDK 1.7叫永久代 >=1.8叫元空间)
存放类信息 常量 静态变量,即时编译后的代码
永久代:受制于堆内存大小
元空间(tenured):它可以使用机器内存,默认不受限制,方便拓展,但是它会挤压堆空间
Oracle 收构了二家公司 Hotspot JRocket Hotspot有元空间和永久代 JRocket没有永久代
堆(-Xmx 堆区可分配的最大上限 -Xms 堆区内初始分配的大小)
存放对象实例和数组
共享区为什么要用二个区来呢?
原因: 因为堆存放的是对数和数组,这些东西都是会频繁回收(GC),而类信息 常量 静态变量非常难回收的.所以就有动态分离,便于回收;
直接内存(堆外内存)
如果使用NIO的话这块区域会频繁使用,在java堆内可以直接引用directByteBuffer对象直接引用并操作,这块内存不会java堆大小限制,可以通过MaxDirectMemortSize来设置,默认大小跟堆内存一样大
下面详细分析一下JVM运行时内存的分布
先写一段java代码
/*
* @项目名: My Application
* @文件名: JVMObject
* @创建者: fanlelong
* @创建时间: 2021/5/12 10:29 PM
* @描述: -Xmx30m -Xms30m -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops -Xss1m
*
*/
public class JVMObject {
public static final String MAN_TYPE = "man";//常量
public static String WOMEN_TYPE = "women";//静态变量
public static void main (String [] args) throws InterruptedException {//栈桢
Teacher T1 = new Teacher();//堆中 T1是局部变量
T1.setName("Anceyl");
T1.setSexType(MAN_TYPE);
T1.setAge(28);
for (int i = 0; i < 15; i++) {//进行15次垃圾回收
System.gc();
}
Teacher T2 = new Teacher();
T2.setName("Lelong");
T2.setSexType(MAN_TYPE);
T2.setAge(18);
Thread.currentThread().sleep(Integer.MAX_VALUE);
}
public static class Teacher {
String name;
String sexType;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
}
}
1: 程序启动时,要向操作系统申请内存(栈是多大,堆是多大,方法区多大)
2: 类加载:通过类加载器,会把JVMObject.class Teacher.class放入我们方法区
3: 方法区解析方法时,发现有常量和静态亦是,这时就把 MAN_TYPE 和 WOMEN_TYPE放入到方法区
4:运行代码了,main()方法运行进来,这是虚拟机栈创建起来并运行main(),这时就会压入一个main()方法的栈桢
5: 栈桢中的方法执行了: 执行到 Teacher T1 = new Teacher();时,会在堆中分配一块内存,然后把把T1这个引用压到main()方法栈桢的局部变量表中
6:中间执行了一些方法,这些方法就不断的压入虚拟机栈
7: 执行循环15次 ..................T1这块内存进入到老年代
8 Teacher T2 = new Teacher(); 堆中eden中分配内存,T2这个引用压到main()方法栈桢的局部变量表中
内存可视化工具HSDB(java -cp.\sa-jdi.jar sum.jvm.hotspot.HSDB)
java -cp "C:\Program Files\Android\Android Studio\jre\lib\sa-jdi.jar" sun.jvm.hotspot.HSDB
jps 查看进程 ps -ef|grep java-->查看linuxjava进程