JVM2.1-运行时数据区:方法区、堆、运行时常量池、Java虚拟机栈、程序计数器、本地方法发栈;栈和栈帧;运行时数据区模块与线程私有共有关系;new String创建了几个对象

类装载生命周期的第一步装载的过程中间,内存当中就会有方法区以及堆两块内容, 那么内存当中是否还会有其它的区域呢?
而JVM是抽象的计算机模型,因此必然要遵守冯诺依曼计算机体系模型。

请添加图片描述
因此,需要有输入设备、输出设备、存储器、运算器、控制器,也就是说输入进来需要有存储数据的地方,还要有执行数据的地方,还有输出的地方,而输入就是类加载器帮你将文件加载进来,输出就是将它输出成对应平台的机器码去进行run,数据文件加载进来,总要有地方去存储数据、区队数据进行执行和流转,所以JVM肯定不是一个只有内存的结构就不管了,它必然是会将JVM划分成不同的的区域。

为什么要用内存?
如果说CPU频繁的交互不交给内存,而是交给磁盘,那么随着CPU的不断发展,CPU的运转速度会越来越快,磁盘的读写性能必定会跟不上CPU的读写速度,即使是SSD(固态硬盘)也跟不上,它相比普通硬盘也仅是减少了寻道时间,或者说是加快了找数据的时间,那么才会在此基础上设置了内存这个东西,用来解决单次IO时间过长导致CPU的等待成本过大的问题。

CPU多核心数的发展由来
那么随着CPU处理速度的的飞速发展,哪怕是使用内存也跟不上CPU的一个读写速度,因此个时候CPU厂商就想了一个办法,在每一个CPU上都增加一个高速缓冲区,用来加快读写速度。

请添加图片描述

摩尔定理:
有两种说法,一种是说对于芯片,即ic芯片每个18个月呢个够容纳的晶体管会翻倍;另一种说法是一个叫摩尔的人的经验之谈,他说CPU的性能每隔18个月性能会翻倍。

根据摩尔定理,即使性能翻倍,CPU的个数也不可能无限制的增长,那么这个时候单核的CPU它的主频必然会有一个性能瓶颈,随着时间的增长,单核的CPU顶不住了,如果想要提升性能,能不能将加多个运算核心呢?这个时候,随着时间的增长,CPU多核的时代就来临了。

基于高速缓存的存储,可以很好解决处理器以及内存之间的矛盾,但是也引入了新的问题,内存缓存一致性问题。

CPU缓存一致性问题
如果说有多个运算核心的话,每个运算核心都会有自己的高速缓存,那么这个时候问题就是:当多个处理器运算都涉及到同一块内存区域的时候,那么就可能产生所谓的缓存不一致问题。

为了解决这个问题,我们需要各个所谓的CPU处理器运行时、包括各个运算核心都必须遵循一些协议,这个协议的作用,就是用来保证数据一致性的。

运行时数据区结构大致设计如下:

请添加图片描述
可以看大,在CPU中间有运算核心,每个运算核心都对应了一个高速缓冲区,这个时候,中间的这个协议保证了缓存的一致性,然后再到同一块主存。

Java多线程缓存一致性
在编程语言Java中使用的是多线程的机制,同样的也会有多个任务同时执行,其实就是类比了CPU的运算核心,那么必然会有一块区域或者说是一种操作能够保证我们数据的一致性,那么JVM内存当中,数据存放的部分必然是有一部分区域是所有线程同时获取到的,那么我们称那块区域为线程共享,每一个线程都会有自己单独的工作内存,当我们的线程运行操作的时候,我们的数据肯定会从JVM的一个主存拷贝到我们线程自己的工作空间去,那么这个实际上也是参照了CPU缓存一致性的设计去做的。

当我们的线程进行运作的时候,我们的数据肯定会有独有的线程工作空间,这个独有的线程工作空间我们称之为线程私有

上述内容,可以了解到在软件的设计上,很多方面是借鉴和参照了计算机原理以及计算机硬件模型,这也是每个软件开发人员必备的知识。

运行时数据区:Run-Time Data Areas

官网介绍地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

在这里插入图片描述
翻译一下,2.5就是运行时数据区

在这里插入图片描述

方法区:Method Area

先了解下方法区
在这里插入图片描述
上述规范简而言之,方法区是一个线程共享模型,是堆的一部分,有个别名,叫做“非堆”,目的是为了跟我们真正的Java堆区分开来。虽然方法区的规范是这样的,但是方法区的实现其实是有区别的,它会改名的,每个版本叫法可能并不一样。

因为JVM的数据区仅仅只是一个规范,而真正的实现,方法区在jdk8中是叫做元空间,而在jdk6是叫做永久代,而在jdk7中是做了一部分去永久代的操作(这块先做了解,先知道即可)。

pare space使用的是JVM自己的内存,而max space使用的直存(直接内存,也就是我们系统的内存)。

方法区在内存不足的情况下,会抛出OOM。

请添加图片描述

堆:Heap

再了解下堆
在这里插入图片描述
堆内存实际上是Java虚拟机管理的内存当中最大的一块,同时它会在JVM启动时创建,并且被所有的线程共享。

可以确定堆是一个线程共享模型,因为它的进程跟JVM进行绑定的,既然是进程相关的,那么按照模型来推论,它就必然是线程共享的。

运行时常量池:Run-Time Constant Pool

在这里插入图片描述
运行时常量池是在虚拟机完成所谓的一个类装载的过程之后,会将所谓的class文件中的常量池载入到内存中间,并且它会保存在方法区当中。

我们常说的常量池指的最多的就是方法区中的运行时常量池。

打开一个反编译字节码文件
在这里插入图片描述

可以看到class文件中的常量池Constant pool下面有一堆常量用来记录符号引用和字面量信息,它占用了class文件内容中非常大的比重,这种常量池也一般称作为静态常量池,我们会将这些常量汇总,并在内存中间专门开辟出一块专门为它服务的区域,用以对这些数据进行存储。

静态常量池也会被加载到内存当中,因为如果说静态常量池不被加载进我们的内存当中,除非说静态常量池它的数据是在JVM的外部完成了一系列的解析,或者说已经在外部转变成了我们想要的数据,不然就必须进到所谓的JVM的内存当中去,显然在进入JVM当中之前,它并没有做这样的一件事情,而JVM方法区中,实际上它储存了class文件的信息以及我们的运行时常量池,那么这个时候,实际上我们的class文件的信息实际上就包含有两部分(而常量池②类信息),一部分就是所谓的类信息:包括魔术开头、版本号信息、以及后面的类信息,我们可以将类信息描述成一个框架,一个规范,而这个规范中具体的内容,就是填充的所谓的常量池,内容由常量池来进行储存。

运行时常量池参照官方文档的定义,就是,每个类,每个接口在JVM进行run的过程中间,在我们的内存开辟出一块用来储存我们静态常量池一部分数据的一个特殊区域。并且我们会在运行时常量池所需要内存不足的情况下,抛出OOM,可以将这个区域看作是方法区的一个细分部分。

运行时常量池如何动态添加数据?

比如Sting中的intern()就可以将值动态的添加到字符串常量池中。

字符串常量池也叫做String常量池,它通常是包含在运行时常量池中间的。
包括运行时常量池里面其实还包括了一类,叫Java的基本类型的封装类的常量池,比如Integer、这Boolean些,但是不包括浮点数,比如float、double。

这里就又衍生出一个问题,Java1.8当中,它对于方法区的实现做了一些改变,也就是说1.7以前的版本,我们用到的方法区在虚拟机中,是虚拟机的内存,再大不会超过JVM内存的上限,而在jdk1.8中,方法区的实现元空间使用的是直接内存,那就意味着可以超出虚拟机的内存范围之外。如果说元空间超出了虚拟机的内存范围之外,那么这个时候,因为方法区的实现元空间使用的是直接节内存,那么各个常量池到底是如何放的呢?是否用到的都是直接内存呢?

请添加图片描述

上图可以看到,运行时常量池中的字符串常量池和基本类型包装类常量池都使用的是堆内存,也就是隶属于JVM内存,而运行时常量池中的其它常量池是可以延伸到直接内存中的。

那么设计这么多种类的常量池意义何在?
Java本身是一门面向对象的语言,那么必然会不断的产生对象。

而常量池当中的内容一般情况下来说是使用的比较频繁的数据,并且一般来说它的声明周期不会太短,那么如果说JVM不设计常量池这个东西,JVM就需要频繁的创建和销毁对象,从而会影响系统的性能。那么如何设计这样一个东西?就需要考虑到能否实现对象的共享,也就是一个类的缓存操作(但其实不是真正的缓存操作)。

比如说字符串常量池,在编译阶段就将所有相同的字符串常量合并,只占用一个空间,这样可以大幅度的减少运行时间。

举个例子,比如比较字符串的时候,“==”和“equals()”进行对比,“==”必然是是比“equals()”快的。
“==”在Java中只是运算符号。“==”只会判断两个引用是否相等,比较的是他们在内存中的存放地址,也就是堆内存地址。
equals()是Java中的一个方法。“equals()”用来比较的是两个对象的内容是否相等,是判断两个变量或实例所指向的内存空间的值是不是相同。由于所有的类都是继承自java.lang.Object类的,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,Object中的equals方法返回的是“==”的判断。这是一种另类的资源共享逻辑,线程池的设计就是参照了这样的思想进行设计的。

String s1 = new String (“aaa”);整个生命周期创建了几个对象?
类加载阶段: 实际上new String (“aaa”)在类加载的阶段,就已经需要对这个对象进行创建,并且类只会加载一次,这个时候这个字符串"aaa"会放到全局共享的字符串常量池当中,这是类加载阶段干的事情。这个时候实际上就已经创建了一个对象在常量池中。
运行阶段: 会对常量池进行寻找,找到"aaa",而这里由于是new String,new对象时,都会在堆中创建对象,因此需要将常量池这个"aaa"拷贝一份放到实际的堆中,并将这个对象的引用交给s1。

综上所述:
String s1 = new String (“aaa”)在整个生命周期创建了两个对象,而且每调用一次就会产生一个对象。
一个是在常量池中,常量池的为"aaa",
一个是在堆内存中,堆内存中为new String(“aaa”),
而String s1 = new String(“aaa”),“=”将这个对象的引用交给s1。

流程如下图:
请添加图片描述

String s2 = “bbb”;整个生命周期创建了几个对象?
String s=“bbb”,这种赋值形式在java中叫直接量,是Java中唯一不需要new 就可以产生对象的途径,它是在常量池中而不是像new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"bbb"的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个"bbb",下一次如果有String s3 = "bbb"又会将s3指向"bbb"这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。

名词解释:栈和栈帧

请添加图片描述

Java是一个进程,进程跑起来之后,里面会有非常多的线程,线程回去执行我们的方法,整个Java呢个够跑起来的核心一定是你当前
的进程当中有很多的线程帮你执行这些方法,如果说现在有一个线程,然后它要用来执行方法(执行的英语:invoke),那么这个时候,每个线程当中一定会有一个执行方法的数据结构,代表当线程执行方法的数据结构,那么这个结构选用的就是

而Java中的方法又会分为几类,Java方法、native方法,native方法是JVM本身会使用到的,需要调用到类库,及调用本地方法的时候才会用到native方法,对于那些执行Java的方法的我们称之为Java虚拟机栈,对于执行native方法的我们称之为native方法栈,翻译成文字的意思就是本地方法栈。

那么当前线程中间就会有一个Java虚拟机栈专门用来表示当前的一个线程,包括如果说调用各种各样的方法的话,他就会往当前的栈中去储存我们的数据,那么这个时候,一个线程当中肯定会有非常多的方法,那么必然会有一个最小的存储单位,这个结构叫做栈帧

说白了,就是代表一个方法的invoke,也就是说代表一个方法的执行,有多少个方法,我就往当前的这个栈中压多少个栈帧

Java虚拟机栈:Java Virtual Machine Stacks

在这里插入图片描述
Java虚拟机栈是一个线程执行的区域,我们保存着一个线程中方法的调用,换句话说,我们一个Java线程的运行状态会由一个虚拟机栈来进行保存,所以说虚拟机栈必然是线程私有的,它随着线程的创建而创建,每一个被线程执行的方法就对应一个栈帧,调一个方法,就会向栈压一个栈帧,一个方法调用完成之后,就会将这个栈帧从该栈中进行一个弹出,这也是栈的特性,先进后出。

官方也说明了,栈中不能进行无限制的栈帧压入,如果对栈进行无限制的压入的话,会出现throws a StackOverflowError,栈溢出。

思考一个问题:
一个JVM进程当中,会有多个线程执行,那么这个时候,线程中的内容想要拥有CPU的执行权,是靠抢占CPU的时间片,这个时候比如A线程执行到栈帧3执行到一半,这个时候下一个线程线程B抢到CPU时间片,那么A线程就失去了CPU的调度权,那么等到B执行完,A抢到了CPU的执行权,切换回A线程,那么这个时候怎么判断栈帧执行到哪了呢?
所以一个线程肯定需要在线程中维护一个变量,记录我们所谓的线程执行到的位置,这个就叫程序计数器,它需要让我们的栈中的内容按照顺序一步一步执行下去。

程序计数器:The pc Register

在这里插入图片描述

程序计数器,也叫PC寄存器。
一个JVM支持一次性执行多个执行线程,每个JVM线程都有自己的程序计数器,也就是程序计数器是每个线程私有的。如果说程序计数器它所执行的是一个Java方法,那么它记录的是JVM的字节码指令的地址,如果说执行的是native方法,那么计数器为空。Java层面是不会记录native层面的东西的。

本地方法发栈:Native Method Stacks

在这里插入图片描述

本地方法栈它执行的是native方法,比如说Object中就有很多这正发发,如hashCode(),通常会在创建每个线程的时候,为每个线程分配它自己的本地方法栈,也就是说,本地方法栈也是线程私有的。

运行时数据区模块与线程私有共有关系

堆和方法区是线程共有的,程序计数器,本地方法栈,Java虚拟机栈是线程私有的。
请添加图片描述
如果想要更加细致的了解运行时数据区这一块内容,就是栈帧,因为Java实际上很大的一部分是用来存东西,并且用一个特定的算法进行执行,算法就是我们自己定义的方法,那么最关键的就是每一个方法的调用,而每个方法的调用代表的就是一个栈帧,那么这个时候栈帧中间到底有什么内容,就是我们需要关心的。

猜你喜欢

转载自blog.csdn.net/qq_41929714/article/details/131115931
今日推荐