JAVA程序运行过程
jvm的跨平台和语言无关性
所有的java程序都需要编译成class文件的形式提交给jvm虚拟机来加载,通过图可以看到class文件经过javac编译以后进入jvm虚拟机内部首先要通过类加载器(ClassLoader)进行加载然后通过字节码解释器或者JIT编译器最后经过执行引擎来操纵硬件运行程序。
这里有一个东西是不在jvm中的就是java类库他是存在java运行时环境中的(JRE)
在这里我们可以了解到不管java在运行时不论外界到底是什么系统什么情况,他都会使用自己的jvm虚拟机,那我们是不是可以理解成不管外面是什么操作系统也好只要虚拟机搭建好了class文件是一样的运行后的效果也一样呢,没错这就是java作为编程语言的一个重要的特性,即jvm的跨平台性java不管在windows也好 linux也好mac也好他都会在上面生成jvm虚拟机来运行
那他实现的方法就是放我们下载JDK的时候会有各个操作系统的版本,不同的版本带的该操作系统的jvm虚拟机。如此就可以实现jvm的跨平台啦
同时我们又发现jvm虚拟机他好像识别的是class文件而不是我们熟悉的.java文件那我们是不是可以想到所有可以编译成class文件的编程语言也可以在jvm虚拟机上运行呢,没错!这就是我要说的jvm的语言无关性可以编译成或者说运行时需要编译成class文件的编程语言都可以在jvm上面运行比如scala,kotlin,groovy等
jvm的发展
jvm就像一个我们平时用的电脑它可以有各种版本型号如
他们都是jvm的实现方式比较常见的是Hotspot,但是平时我们敲代码也是直接运行我们也不知道我们的虚拟机是什么来实现的我们可以打开cmd窗口输入java -version来查看
其中第一个箭头指向的就是jvm的实现方式大部分都是HotSpot第二个箭头指向的是执行模式
jvm内存结构
我们学习jvm的运行情况最重要的一点是要知道jvm的内存结构,只有知道了内存结构才能知道jvm在我们的电脑上是怎么样运行的
首先我们要了解一个很重要的原理就是运行时数据区域也就是在虚拟机内部运行一个class文件的时候分配的内存情况
我们先去看虚拟机将jvm运行时内存分成两个部分一个是直接内存一个是运行时数据区这里我们首先要了解到我们直接内存直接内存就是物理机的真实内存但是物理机把这个内存区域分配给jvm虚拟机我们可以简单理解成当jvm我们的电脑的硬盘
然而最重要的是运行时数据区他内部也有分类他分为两块一个是线程私有一个线程共享区。这个可以理解成我们去吃席,你有自己的碗也有公用的盘子每个人都有一个自己的碗但是一桌子人共享桌子上的盘子
同样的在执行一个class文件的时候每一个线程都在线程私有区有一个自己的区域但是这些线程都共享可以调用线程共有区的内容
像我们吃饭每个人都有碗筷子碟子一堆用餐工具,同样的我们线程私有区的每一个线程都有该线程的虚拟机栈,本地方法栈,程序计数器这里需要注意的是每一个线程的这个三个区域都是该线程独有的,也就是来一个线程就会分配给他一套。就像我们吃席来一个人就得给他一套餐具。
容纳和线程共享区就很好理解了不管来多少线程他是不会变的线程如果有需要时来这里拿东西引用东西,就像吃席的时候一张桌子上多少菜不会以为人变多而变多的而且这些菜每个人都能吃
线程私有区-虚拟机栈
首先我们要理解线程私有区得先理解一个概念–栈栈是什么呢我们可以把它理解成一种内存类型
他也是内存的一种只是和其他内存不一样的是你可以把它看成一个死胡同
就是一堆人要去一个胡同走进去的时候发现是死胡同要走出来的时候就要先进去的后出来后进去的先出来。栈内存就是一堆堆数据压入栈内,然后我们需要操作数据的时候要先操作上面的数据。
然后来看我们的虚拟机栈虚拟机栈就是一个线程内的储存区域里面储存了运行java方法所需的数据,指令还有返回地址这样无法理解对吧我们换个方法就是我们刚才进死胡同的是一个个人那在虚拟机中的是什么呢是一个个栈帧每一个栈帧储存着一个方法`
public class dwad {
public static void main(String[] args) {
sdw(44);
}
public static void sdw(int a){
System.out.println(a);
}
}
这里我们先看到的是main()方法对吧,mian方法就像一个人他先进去栈内然后看到了我们引用了sdw方法我们再把sdw这个让他进栈那么我们的虚拟机栈内就有两个栈帧啦等处理运行的时候就是先处理sdw栈帧因为他是最后进去的对吧
最后我们一下虚拟机栈作为一个内存区域那么肯定有他的大小对吧不可逆无限大呀无限大我们电脑不久吃不消了嘛,那么这个大小多少呢,不同操作系统的虚拟机栈大小是不同的但是大部分情况下是1mb也就是1024kb超过了这个内存就会虚拟机就会报错栈溢出(java.lang.StackOverflowError)。具体的操作系统jvm虚拟机的虚拟机栈内存大小是多少如下图
当然你嫌这个大小不好自己设置也可以但是不推荐调整的指令如下
我们现在知道虚拟机栈内部是一个个栈帧组成的,那么栈帧内部是什么东西呢?我们现在看下图
可以看到栈帧也分成四个部分分别是局部变量表,操作数栈,动态链接,完成出口
操作数栈
我们首先想一下一个电脑如果要运行起来最重要的几个东西是什么一个是cpu对叭来运行程序还需要缓存来临时储存数据还需要主内存对吧他需要长期存储数据。里面的逻辑就是从主内存中取出需要操作的内容放到缓存区然后再把缓存区的内容交给cpu操作cpu是没有存储功能的所以他执行完后要立马交给缓存区,再由缓存区来存到主内存。那么jvm虽然是个虚拟机但是他是不是也要走这个流程呢?是的!jvm说到的他是模拟计算机的运行的那么操作数栈在当中充当了什么角色呢?看完下图你就明白了
操作数栈就是扮演了一个缓存的作用他是从栈堆内获取数据然后交给jvm的执行引擎来操作。
局部变量表
我们学习局部变量表的时候我们要知道重点是什么是表这个字。这一块他就是一张表,他的内容就是存储局部变量的,
这个表其实和我们数组很像他也有类似下标的数字而且开始也是从0开始的但是这个0下标存储的是什么的就是这个栈帧也就是这个方法的他的本身(this)当我们需要数据的时候从局部变量标内读取数据或者根据这个表的引用从堆里拿内存到操作数栈。为什么要从堆里拿呢因为这里本质上还是在栈里的栈的内存是大部分只有1mb我们不可能把所有的数据都存在表里只能存一个引用地址然后到时候去堆里找。
我们再回过头我们从表里取出了数据再操作数栈然后呢操作数栈他本身是没有运行的能力的所以他需要把数据提交给jvm的执行核心去运行,结束以后得到的内容先返回操作数栈然后再把它写到表内。这样子一个周期就完成了。
至于动态连接和完成出口并不是太重要只要记住我们方法运行成功的情况下返回值是通过完成出口出去的必须是运行成功的情况下如果运行失败就是别的方法。
线程私有区-程序计数器
刚才学习的是虚拟机栈的内容那我们看他还有一个程序计数器
我们首先要理解我们class文件在虚拟机栈内变成栈帧以后是怎么运行的首先我们得知道一个概念
就是时间片比如我们有一秒种但是他对于电脑来说太长了他把时间切成了很小很小的一片
比如切成1ms一片,我们这么干是为了什么呢,就是为了同时运行很多的文件比如你电脑是8核心正常来说只能同时运行8个文件对吧但是我们电脑明显不止8个,我们可以打开不同软件。这是怎么实现的就是通过这个时间片比如需要同时运行2个文件a文件和b文件首先我们把第一片时间片给a,a来运行1ms过后不管这个a运行到什么程度了sleep他下一个时间片给b然后1ms以后不论他是否运行结束sleep他
然后如此往复以后直到他们运行完成。但是我们又有问题了a文件sleep以后再唤醒他我们怎么知道他运行到哪里呢这个时候就需要程序计数器。我们class文件要被jvm虚拟机运行还需要编译成汇编代码他的内容如下
下面的代码被编译成上面的汇编语言。我们可以在class文件内打开cmd输入javap -c指令来查看。
他每一条命令前头都有计数器他来记载运行的步骤实际上他是c里面的偏移量但是篇幅不够我就不在这里展开想了解的话我以后会写关于这个的文章。
计数器就是来保证jvm运行class文件的步骤不会出错。
线程共享区-方法区![请添加图片描述](https://i-blog.csdnimg.cn/blog_migrate/9ac70b780c5364885f96b6fb6ea5d77a.png)
接下来是方法区的了解啦首先方法区只是一个概念他有他的实现方法比如永久代和元空间。我们这个文章的主旨是讲解java的内存区域没错方法区也是一个内存的区域。他用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。类信息,静态变量、JIT编译后的代码都能理解这个常量池是什么东西呢
常量池
常量池可以分为运行时常量池和静态常量池,静态常量池
在这个Constant pool 里面已经写在文件里的常量就是静态常量。
那么运行时常量池又是啥呢?我们先也去学习一个概念,我们上面静态常量是已经写在class文件里的常量,那我们是不是还有引用的常量对吧。但是我们引用一般是怎么引用的?比如a类里面需要调用b类的方法我们肯定是需要引用的。引用的时候肯定不是直接把b类的真实物理地址写上去而是写一个符号来代替他对吧,那么在jvm虚拟机里我们肯定是需要一个东西把这个符号引用转换成真实的物理地址。他就是运行时常量池的工作即:将符号引用转换成直接引用
线程共享区-堆
首先理解堆是用来干什么的,他是用来存储对象的区域。平时我们new的对象是存储在堆里的,第二呢我们还得了解一个概念字符串常量池这个常量池他在逻辑上在方法区实际上的地址是在堆里的,这样子比较绕对吧就像你逻辑上是一个人类但是你的实际是存在中国的某个地方。我们平时写的字符串也是一个对象(万物皆对象)只不过存在的的地方就是字符串常量池
我们来看下面这个经典的面试题下面的代码创建了几个对象,我们刷过面试题都知道是两个对象,但是为什么呢
String str=new String("abc")
首先我们能看见他new了一个String的对象。他存储在堆里因为堆就是用来存储对象的。然后呢我们又知道字符串也是对象我们也需要new一个“abc”的对象出来他放到字符串常量池然后String对象去引用这个String对象
但是为什么下面代码只创建了一个对象呢
String str="abc"
因为他只在常量池新建了一个“abc”的对象当我们需要使用的时候引用这个常量池的对象就行不需要通过String对象来引用我们再引用Sting对象。
这个常量池也有很有意思的特性
String str="abc";
Sting b="abc";
你们猜猜他新建了几个对象,答案是两个,执行第一行代码的时候新建一个“abc”对象把它放进字符串常量池然后第二行代码执行了然后他要继续放到常量池的时候,欸发现已经有这个对象了那么他就直接引用这个对象不再new重复的内容了。
我们这里明白了堆是存储对象的但是对于堆的内存,gc的逻辑,堆内对象是怎么样存在的都不太清除因为这一块内容很多我会写一篇专门关于堆的文章好好聊聊这个堆是啥东西我们现在只需要了解堆是用来存储对象的,需要被引用的时候就会被提取。