Jvm(14.1),运行时数据---jvm和操作系统的关系
Posted qingruihappy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jvm(14.1),运行时数据---jvm和操作系统的关系相关的知识,希望对你有一定的参考价值。
知识点三:java虚拟机的体系结构(无奈,我怀着悲痛心情告诉你,我们必须来一些概念,别急,咱有图)
在了解jvm的结构之前,我们有必要先来了解一下操作系统的内存基本结构,这段可不能跳过,它会有助于消化上面的那个图哦!好先来看图
操作系统内存布局:
那么jvm在操作系统中如何表示的呢?
操作系统中的jvm
为什么jvm的内存是分布在操作系统的堆中呢??因为操作系统的栈是操作系统管理的,它随时会被回收,所以如果jvm放在栈中,那java的一个null对象就很难确定会被谁回收了,那gc的存在就一点意义都莫有了,而要对栈做到自动释放也是jvm需要考虑的,所以放在堆中就最合适不过了。
操作系统+jvm的内存简单布局
其实在这里jvm的运行就是放在操作系统的堆中的从上图中,你有没有发现什么规律,jvm的内存结构居然和操作系统的结构惊人的一致,你能不能给他们对号入座?还不能,没关系,再来看一个图,我帮你对号入座。看我下面红色的标注
从这个图,你应该不难发现,原来jvm的设计的模型其实就是操作系统的模型,基于
操作系统的角度,jvm就是个该死的java.exe/javaw.exe,也就是一个应用,而基于class文件来说,jvm就是个操作系统,而jvm的方法区,也就相当于操作系统的硬盘区,所以你知道我为什么喜欢叫他permanent区吗,因为这个单词是永久的意思,也就是永久区,我们的磁盘就是不断电的永久区嘛,是一样的意思啊,多好对应啊。而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,至于堆嘛,虽然概念上一致目标也一致,分配内存的方式也一直(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异,gc的回收算法,估计是jvm里面的经典啊,后面我们也会一点点的学习的,不要着急。
其实区别就相当于在wid的操作系统总,我们假如要回收内存的话把某个软件关闭掉就行了,这是我们手动的处理的,但是在jvm中则是GC自动回收额。
有没有突然自信的感觉?如果你对我的文章有自信,我们再继续,还是以图解的方式,我还是那一句,对于概念我绝对有信心让它在你脑子里根深蒂固。
看下面的图。
将这个图和上面的图对比多了什么?没错,多了一个pc寄存器,我为什么要画出来,主要是要告诉你,所谓pc寄存器,无论是在虚拟机中还是在我们虚拟机所寄宿的操作系统中功能目的是一致的,计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,(这一点对于学过汇编的同学应该很容易理解,有很多的寄存器eax,esp之类的32 位寄存器,jvm里的寄存器就相当于汇编里的esp寄存器),计算机用pc寄存器来存放"伪指令"或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以你现在应该很明确的知道,虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。
再对上面的图扩展,这一次,我们会稍微的深入一点,放心啦,不会很深入,我们的目标是浅显易懂,好学易记嘛!看下面的图。
多了什么?没错多了一个classLoader,其实这个图是要告诉你,当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm 的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader,如下图。
那么方法区中的字节码内存块,除了记录一个class自己的class对象引用和一个加载自己的
ClassLoader引用之外,还记录了什么信息呢??我们还是看图,然后我会讲给你听,听过一遍之后一辈子都不会忘记。
你仔细将这个字节码和我们的类对应,是不是和一个基本的java类惊人的一致?下面你看我贴出的一个类的基本结构。
[java]view plaincopy
- package test?import java.io.Serializable?public final class ClassStruct ex tends Object implements Serializable {//1.类信息
- //2.对象字段信息
- private String name?
- private int id?
- //4.常量池
- public final int CONST_INT=0?
- public final String CONST_STR="CONST_STR"?
- //5.类变量区
- public static String static_str="static_str"?
- //3.方法信息
- public static final String getStatic_str ()throws Exception{
- return ClassStruct.static_str?
- }}
你将上面的代码注解和上面的那个字节码码内存块按标号对应一下,有没有发现,其实内存的字节码块就是完整的把你整个类装到了内存而已。
所以各个信息段记录的信息可以从我们的类结构中得到,不需要你硬背,你认真的看过我下面的描述一遍估计就不可能会忘记了:
1.类信息:修饰符(public final)
是类还是接口(class,interface)
类的全限定名(Test/ClassStruct.class)
直接父类的全限定名(java/lang/Object.class)
直接父接口的权限定名数组(java/io/Serializable)
也就是 public final class ClassStruct extends Object implements Serializable这段描述的
信息提取
2.字段信息:修饰符(pirvate)
字段类型(java/lang/String.class)
字段名(name)
也就是类似private String name;这段描述信息的提取
3.方法信息:修饰符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
参数需要用到的局部变量的大小还有操作数栈大小(操作数栈我们后面会讲)
方法体的字节码(就是花括号里的内容)
异常表(throws Exception)
也就是对方法public static final String getStatic_str ()throws Exception的字节码的提取
4.常量池:
4.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String
CONST_STR="CONST_STR";
1.3CONSTANT_DOUBLE_INFO浮点型直接常量池
等等各种基本数据类型基础常量池(待会我们会反编译一个类,来查看它的
常量池等。)
4.2.方法名、方法描述符、类名、字段名,字段描述符的符号引用
也就是所以编译器能够被确定,能够被快速查找的内容都存放在这里,它像数组一样通过索引访问,就是专门用来做查找的。
编译时就能确定数值的常量类型都会复制它的所有常量到自己的常量池中,或者嵌入到它的字节码流中。作为常量池或者字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量作为他们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存
5.类变量:
就是静态字段( public static String static_str="static_str";)
虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。
6.一个到classLoader的引用,通过this.getClass().getClassLoader()来取得为什么要先经过class
呢?思考一下,然后看第七点的解释,再回来思考
7.一个到class对象的引用,这个对象存储了所有这个字节码内存块的相关信息。所以你能够看到
的区域,比如:类信息,你可以通过this.getClass().getName()取得
所有的方法信息,可以通过this.getClass().getDeclaredMethods(),字段信息可以通过
this.getClass().getDeclaredFields(),等等,所以在字节码中你想得到的,调用的,通过class这
个引用基本都能够帮你完成。因为他就是字节码在内存块在堆中的一个对象
8.方法表,如果学习c++的人应该都知道c++的对象内存模型有一个叫虚表的东西,java本来的名字就叫c++- -,它的方法表其实说白了就是c++的虚表,它的内容就是这个类的所有实例可能被调用的所有实例方法的直接引用。也是为了动态绑定的快速定位而做的一个类似缓存的查找表,它以数组的形式存在于内存中。不过这个表不是必须存在的,取决于虚拟机的设计者,以及运行虚拟机的机器是否有足够的内存
--------------------------------------------------------------------------------------忽略下面虚线间的废话----------------------------------------------------------------------------------------
---------------------------------
好了,还剩这么多没讲过。不过不要急,我一向提倡,学到哪里讲到哪里,看到哪里。所以没有学到的概念,让他随风去。
但是我还是会来串一下思路滴:
首先,当一个程序启动之前,它的class会被类装载器装入方法区(不好听,其实这个区我喜欢叫做 Permanent区),执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统APIi等等。
这里的本地方法栈就是native的方法了。
可不可以这样理解,方法区中的数据被加载进来了,但是还没运行,而堆中的就是从方法区中加载进来开始跑的数据呢。
现在自己把思路捋一下:
一:当操作系统开始启动java代码的时候,这个时候通过执行引擎把代码的class字节码加载到方法区中,注意这个时候是边执行边加载的。当加载到main的方法,静态,常量等的时候会把这些统统的都放到方法区中
假如现在执行到new对象的时候会把new的对象放到堆内存中去,而把new对象的引用test等放到栈内存中去,接着会继续往方法区中加载东西。
这种说法可能只支持hotspot的虚拟机,原因详见对象的访问定位。hotsopt没有通过句柄池,而是直接定位的方式来的。
二,我们现在再来看看栈的运行情况。这个时候栈里面存储的是对堆得引用,这个时候通过计数器在栈空间中的作用会把方法一步步的往下执行,当执行到native的方法的时候,这个时候就会映射成本地的方法在操作系统中的实现,这部分代码是没有开源的,甚至有可能不是java代码实现的,可能是c语言,也可能是汇编语言,也有可能是二进制的来实现的,因为这个时候jvm就是在操作系统的堆空间中的,所以调用native方法就是执行操作系统的一系列的方法。
三,先抛出一个疑问,回头再解决。
按照上面的逻辑,方法区存储的是class的一些信息,堆中储存的是new的对象,而栈中存储的是对象的引用。那堆中这个对象到底有什么呢,class的相关字节码对象不是都在方法区吗。
解释详见18,HotSpot的内存布局
好吧,你听晕了,我知道,先记住这段话的位置,等某年某月我提醒你回来看,你就焕然大悟了, 现在你只需要走马观花咯!!!
--------------------------------------------------------------------------------------忽略下面虚线间的废话----------------------------------------------------------------------------------------
---------------------------------
好了,这一节的内容实在够多了,所以我打算把它拆解一下,剩下的内容放到下一节,下一节我们会来学习虚拟机的堆栈,和堆。
以上是关于Jvm(14.1),运行时数据---jvm和操作系统的关系的主要内容,如果未能解决你的问题,请参考以下文章