一,结构图我先标在这里,方便有个整体的记忆。
二,写一个简单的例子
public class Test {
//计算
public int calc(){
int a = 1; //第三处代码
int b = 2;//第四处代码
int c = a + b;//第五处代码
return c;//第六处代码
}
//主程序
public static void main(String[] args) {
Test test = new Test(); //第一处代码
test.calc(); //第二处代码
}
}
三,我们开始运行程序
当我们运行main方法时,就生成了一个main主线程,线程实际就会存入到栈里面,那栈里面有什么呢?现在我们放大Thread main线程看看
这里有点套娃, 线程 里面包含有三块: 程序计数器 ,线程栈,本地方法栈。
程序计数器:主要是用来记录当前线程代码的执行位置,用于CPU切换线程再回来时能够继续执行。所以计数据器是每个线程所独有的,假定当前已执行到第一处代码, 计数器 标记为1。
本地方栈:这个好理解主要是替我们执行Native方法。
线程栈:采用先进后出(FILO)的方式存储栈帧。那什么是栈帧呢?我们可以把类里面的每个方法就理解成栈帧。上图我只画了main方法的栈帧,接下来我们执行第二处代码,把cal()方法也压入栈。
可以看到cal栈帧里面包含四个部份:
局部变量:如cal里面定义的变量a,b,c,他们是相对于栈帧进行隔离的,存放在这个区域。
操作数栈:随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。当我们运行第三,四,五行代码的时候,这里实际就是将1,2两个数压入操作数栈中,当cpu进行相加操作完成后再将数3出栈返回给c。
我们可以在Test.class目录用 java p命令查看指令逻辑
javap -c Test.class
public int calc();
Code:
0: iconst_1 //将int类型常量1压入操作数栈
1: istore_1// 将int类型值存入局部变量1
2: iconst_2
3: istore_2
4: iload_1//从局部变量1中装载int类型值
5: iload_2
6: iadd//执行int类型的加法
7: istore_3
8: iload_3
9: ireturn//从方法中返回int类型的数据
动态链接: Java虚拟机 栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接
方法出口:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
好了说完栈,我们聊下堆,我们都知道,像我们第一行代码new Test();,实际这个对象就存储在堆中,要知道java作为高级语言,我们是有垃圾回收机制自动帮我们回收无用的对象的,也就是我们平时说的GC。这期我们不展开讲GC,只讲下堆里面的各个区间,如下图:
堆可以分为两代:年轻代和老年代
年轻代可以分为:Eden区,Survivor To,Survivor From
我们生成的对象大部份都是朝生夕死的,所以一般会存放到年轻代的Eden区,当成为垃圾对象后,gc会对其进行回收,并将余下未回收的对象,存入Survivor From区,并将对象的年龄加一,等到下次gc,会连同Eden区和Survivor From区里面的对象一起回收,并将未回收成功的对象放入Survivor To区,并继续将年龄加一,依次往复,当对象年龄超过15次(默认),会将对象移到老年代,老年代了咋整?放心老年代也会gc的,只不过方式不太一样。
最后还剩下方法区:方法区主要存放我们的类信息,常量和静态变量,所以这块区域是公有的。
总结:今天算是对JVM总体结构有一个比较深刻的认识。学习还是要知其然知其所以然,我是阿雷,一个没秃顶的程序员。