jvm运行时数据区

来源:互联网 发布:ppt播放软件下载 编辑:程序博客网 时间:2024/06/03 12:13

对spring多线程情况下的运行比较迷茫, 于是看一看深入理解jvm, 手抄了一些我觉得关键点的段落, 供日后回顾


当java虚拟机运行一个程序时, 它需要内存来存储许多东西, 例如, 字节码, 从已装载的class文件中得到的其他信息, 程序创建的对象, 传递给方法的参数, 返回值, 局部变量, 以及运算的中间结果等. java虚拟机把这些东西都组织到几个"运行时数据区"中, 以便于管理.
尽管这些"运行时数据区"都会以某种形式存在于每一个java虚拟机实现中, 但是规范对它们的描述却是相当抽象的. 这些运行时数据区结构上的细节, 大多数都由具体实现的设计者决定.
不同的虚拟机实现可能具有很不同的内存限制, 有的实现可能有大量的内存可用, 有的可能只有很少. 有的实现可以利用虚拟内存, 有的则不能. 规范本身对"运行时数据区"只有抽象的描述, 这就使得java虚拟机可以很容易地在各种计算机和设备上实现.
某些运行时数据区是由程序中所有线程共享的, 还有一些则只能由一个线程拥有. 每个java虚拟机实例都有一个方法区及一个堆, 由该虚拟机实例中的所有线程共享. 当虚拟机装载一个class文件时, 它会从这个class文件包含的二进制数据中解析类型信息, 然后把这些类型信息放到方法区中. 当程序运行时, 虚拟机会把所有该程序在运行时候创建的对象都放到堆中. 请看图5-2中对这些内存区域的描绘.


当每一个新线被创建时, 它都将得到它自己的pc寄存器(程序计数器)以及一个java栈. 如果线程正在执行的是一个java方法(非本地方法), 那么pc寄存器的值将总是指示下一条将被执行的指令, 而它的java栈则总是存储该线程中java方法调用的状态--包括它的局部变量, 被调用时传进来的参数, 它的返回值, 以及运算的中间结果等. 而本地方法制胜的状态, 则是以某种依赖于具体实现的方式存储在本地方法栈中, 也可能是在寄存器或者其他某些与特定实现相关的内存区中.
java栈是由许多栈帧(stack frame)或者帧(frame)组成的, 一个栈帧包含一个java方法调用的状态. 当线程调用一个java方法时, 虚拟机压入一个新的栈帧到该线程的java栈中; 当该方法返回时, 这个栈帧被从java栈中弹出并抛弃.
栈: 线程私有, 存放局部基本类型的变量/部分的返回结果以及栈帧, 非基本类型的对象在jvm栈上仅存放一个指向堆的引用.
栈帧: 栈帧由三个部分组成, 局部变量区, 操作数栈和帧数据区. 栈帧是用于支付虚拟机进行方法调用和方法执行的数据结构, 每一个方法从调用到执行完成的过程都可以看作是一个栈帧在虚拟机栈中从入栈到出栈的过程.


java虚拟机没有寄存器, 其指令集使用java栈来存储中间数据. 这样设计的原因是为了保持java虚拟机的指令集尽量紧凑, 同时也便于java虚拟机在那些只有很少通用寄存器的平台上实现. 另外, java虚拟机的这种基于栈的体系结构, 也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化.


java数据类型可以分为两种: 基本类型和引用类型. 引用类型被统称为"引用"(reference), 有三种引用类型: 类类型, 接口类型, 数组类型. 它们的值都是对动态创建对象的引用. 类类型的值是对类实例的引用; 数组类型的值是对数组对象的引用, 在java虚拟机中, 数组是个真正的对象; 而接口类型的值, 则是对实现了该接口的某个类实例的引用. 还有一种特殊的引用值是null, 它表示该引用变量没有引用任何对象.
java虚拟机中还有一个只在内部使用的基本类型: returnAddress, java程序员不能使用这个类型, 这个基本类型被用来实现java程序中的finally子句.


方法区: 在java虚拟机中, 关于被装载类型的信息存储在一个逻辑上被称为"方法区"的内存中. 当虚拟机装载某个类型时, 它使用类装载器定位相应的class文件, 然后读入这个class文件--一个二进制数据流--然后将它传输到虚拟机中, 紧接着虚拟机提取其中的类型信息, 并将这些信息存储到方法区. 该类型中的类(静态)变量同样也是存储在方法区中. 由于所有线程共享方法区, 因此它们对方法区数据的访问必须设计成线程安全的. 比如, 假设同时有两个线程都企图访问一个名为Lava的类, 而这个类还没有被装入虚拟机, 那么, 这时只应该有一个线程去装载它, 而另一个线程则只能等待.
方法区的大小不必是固定的, 虚拟机可以根据应用的需要动态调整. 同样, 方法区也不必是连续的, 方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配. 另外, 虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最大尺寸等.
方法区也可以被垃圾收集, 因为虚拟机允许通过用户定义的类装载器来动态扩展java程序, 一些类也会成为程序不再引用的类. 当某个类变为不再被引用的类时, java虚拟机可以卸载这个类(垃圾收集), 从而使方法区占据的内存保持最小.


类型信息. 对每个装载的类型, 虚拟机都会在方法区中存储以下类型信息:
1.这个类型的全限定名
2.这个类型的直接超类的全限定名(除非这个类型是java.lang.Object, 它没有超类)
3.这个类型是类类型还是接口类型
4.这个类型的访问修饰符
5.任何直接超接口的全限定名的有序列表
除了上面列出的基本类型信息外, 虚拟机还得为每个被装载的类型存储以下信息:
1.该类型的常量池
2.字段信息
3.方法信息
4.除了常量以外的所有类(静态)变量
5.一个到类ClassLoader的引用
6.一个到Class类的引用
在下面的小节将会描述这些数据.


常量池 虚拟机必须为每个被装载的类型维护一个常量池. 常量池就是该类型所用常量的一个有序集合, 包括直接常量(string, integer和floating point常量)和对其他类型/字段和方法的符号引用. 池中的数据项就像数组一样是通过索引访问的. 因为常量池存储了相应类型所用到的所有类型/字段和方法的符号引用, 所以它在java程序的动态连接中起着核心的作用. 常量池在本意后面的第6章中会详细讨论.
字段信息 对于类型中声明的每一个字段, 方法区中必须保存下面的信息. 险些之外, 这些字段在类或者接口中的声明顺序也必须保存. 下面是字段信息的清单:
1.字段名
2.字段的类型
3.字段的修饰符(public/private/protected/static/final/volatile/transient的某个子集)
方法信息 对于类型中声明的每一个方法, 方法区中必须保存下面的信息. 和字段一样, 这些方法在类或者接口中的声明顺序也必须保存. 下面是方法信息的清单:
1.方法名
2.方法的返回类型
3.方法参数的数量和类型(按声明顺序)
4.方法的修饰符(public/private/protected/static/final/synchronized/native/abstract的某个子集)
除上面清单中列出的条目之外, 如果某个方法不是抽象的和本地的, 它还必须保存下列信息:
5.方法的字节码(bytecodes)
6.操作数栈和该方法的栈帧中的局部变量区的大小
7.异常表
类(静态)变量 类变量是由所有类实例共享的, 但是即使没有任何类实例, 它也可以被访问. 这些变量只与类而不是类实例有关, 因此它们总是作为类型信息的一而存储在方法区. 除了在类中声明的编译时常量外, 虚拟机在使用某个类之前, 必须在方法区中为这些类变量分配空间.
而编译时常量(就是那些使用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同, 每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中, 或嵌入到它的字节码流中. 作为常量池或字节码流的一部分, 编译时常量保存在方法区中--就和一般的类变量一样. 但是当一般的类变量作为声明它们的类型的一部分数据而保存的时候, 编译时常量作为使用它们的类型的一部分而保存. 这种特殊处理方式在第6章中更详细地讨论.
指向类ClassLoader的引用 -
指向Class类的引用 -
方法表 为了尽可能提高访问效率 -


堆: java程序在运行时创建的所有类实例或数组都放在同一个堆中. 而一个java虚拟机实例中只存在一个堆空间, 因此所有线程都将共享这个堆. 又由于一个java程序独占一个虚拟机实例, 因而每个java程序都有它自己的堆空间. 但是同一个java程序的多个线程却共享着同一个堆空间, 在这种情况下, 就得考虑多线程访问对象(堆数据)的同步问题了.
java虚拟机机有一条在堆中分配新对象的指令, 却没有释放内存的指令. 正如你无法用java代码去明确释放一个对象一样, 字节码指令也没有对应的功能. 虚拟机自己决定如何及何时释放不再被运行的程序引用的对象所占据的内存.
和方法区一样, 堆空间也不必是连续的内存区. 在程序运行时, 它可以动态扩展或收缩. 事实上, 一个实现的方法区可以在堆顶实现. 换句话说, 就是当虚拟机需要为一个新装载的类分配内存时, 类型信息和实际对象可以都在同一个堆上. 因此, 负责回收无用对象的垃圾收集器可能也要负责无用类的释放. 另外, 某些实现可能也允许用户或程序员指定堆的初始大小/最大最小值等等.


栈: 每当启动一个新线程时, java虚拟机都会为它分配一个java栈. 前面我们曾经提到, java栈以帧为单位保存线程的运行状态. 虚拟机只会直接对java栈执行两种操作, 以帧为单位的压栈或出栈.
某个线程正在执行的方法被称为该线程的当前方法, 当前方法使用的栈帧称为当前帧, 当前方法所属的类称为当前类, 当前类的常量池称为当前常量池. 在线程执行一个方法时, 它会跟踪当前类和当前常量池. 此外, 当虚拟机遇到栈内操作指令时, 它对当前帧内数据执行操作.
每当线程调用一个java方法时, 虚拟机都会在该线程的java栈中压入一个新帧. 而这个新帧自然就成为了当前帧. 在执行这个方法时, 它使用这个帧来存储参数/局部变量/中间运算结果等等数据.
java方法可以以两种方式完成. 一种通过return返回, 称为正常返回. 一种通过抛出异常而中止. 不管以哪种方式返回, 都会将当前帧弹出java栈然后释放掉, 这样上一个方法的帖就成为当前帧了.
java栈上的所有数据都是此线程私有的. 任何线程都不能访问另一个线程的栈数据, 因此我们不需要考虑多线程情况下栈数据的访问同步问题. 当一个线程调用一个方法时, 方法的局部变量保存在调用线程java栈的帧中. 只有一个线程能访问那些局部变量, 即调用方法的线程.
像方法区和堆一样, java栈和帧在内存中也不必是连续的.帧可以分布在连续的帧里, 也可以分布在堆里, 或者二者兼而有之. 表示java栈和栈帧的实际数据结构由虚拟机的实现者决定, 某些实现允许用户指定java栈的初始大小和最大最小值.


栈帧: 栈帧由三个部分组成, 局部变量区, 操作数栈和帧数据区. 局部变量区和操作数据栈的大小要视对应的方法而定, 它们是按字长计算的. 编译器在编译时就确定了这些值并放在class文件中. 而帧数据区的大小依赖于具体实现.
当虚拟机调用一个java方法时, 它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小, 并据此分配栈帧内存, 然后压入java栈中.
局部变量区 java栈帧的局部变量区被组织为一个以字长为单位, 从0开始计数的数组. 字节码指令通过从0开始的索引来使用其中的数据. 类型为int/float/reference和returnAddress的值在数组中只占据一项, 而类型为byte/short和char的值在存入数组前都将被转换成int值, 因而同样占据一项. 但是类型为long和double的值在数组中占据连续的两项.
局部变量区包含对应方法的参数和局部变量. 编译器首先按声明的顺序把这些参数放入局部变量数组. 图5-9描绘了下面两个方法的局部变量区.
操作数栈 和局部变量区一样, 操作数栈也是被组织成一个以字长为单位的数组. 但是和前者不同的是, 它不是通过索引来访问, 而是通过标准的栈操作.
不同于程序计数器, java虚拟机没有寄存器, 程序计数器也无法被程序指令直接访问. java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的, 因此它的运行方式是基于栈的而不是基于寄存器的. 虽然指令也可以从其他地方取得操作数, 比如从字节码流中跟随在操作码(代表指令的字节)之后的字节中或者从常量池中, 但是主要还是从操作数栈中获得操作数.
虚拟机把操作数栈作为它的工作区--大多数指令都要从这里弹出数据, 执行运算, 然后把结果压回操作数栈. 比如, iadd指令就要从操作数栈中弹出两个整数, 执行加法运算, 然后把结果压回操作数栈中. 
栈数据区 除了局部变量区和操作数栈外, java栈帧还需要一些数据来支持常量池解析/正常方法返回/以及异常派发机制. 这些信息都保存在java栈帧的帧数据区中.
java虚拟机中的大多数指令都涉及到常量池入口. 有些指令仅仅是从常量池中取出数据然后压入java栈(这些数据的类型包括int/long/float/double和String). 还有些指令使用常量池的数据来指示要实例化的类或数组/要访问的字段/要调用的方法. 还有些指令需要常量池中的数据才能确定某个对象是否属于某个类或实现了某个接口.
每当虚拟机要执行某个需要用到常量池数据的指令时, 它都会通过帧数据区中指向常量池的指针来访问它.
常量池中对类型/字段和方法的引用在开始时都是符号. 当虚拟机在常量池中搜索的时候, 如果遇到指向类/接中/字段或者方法的入口, 假期它们仍然是符号, 虚拟机那时候才会进行解析.
除了用于常量池的解析外, 帧数据区还要帮助虚拟机处理java方法的正常结束或异常中止. 如果是通过return正常结束, 虚拟机必须恢复发起调用的方法的栈帧, 包括设置pc寄存器指向发起调用的方法中的指令--即紧跟着调用了完成方法的指令的下一个指令. 假如方法有有返回值, 虚拟机必须将它压入到发起调用的方法操作数栈.
为了处理java方法执行期间的异常退出情况, 帧数据区还必须保存一个对此方法异常表的引用. 异常表会在第17章深入描述, 它定义了在这个方法的字节码中受catch子句保护的范围, 异常表中的每一项都有一个被catch子句保护的代码的起始和结束位置(译者注: 即try子句内部的代码), 可能被catch的异常类在常量池中的索引值, 以及catch子句内的代码开始的位置.
当某个方法抛出异常时, 虚拟机根据帧数据区对应的异常表来决定如何处理. 如果在异常表中找到了匹配的catch子句, 就会把控制权转交给catch子句内的代码. 如果没有发现, 方法会立即白跑中止, 然后虚拟机使用帧数据区的信息恢复发起调用的方法的帧, 并在发起调用的方法的上下文中重新抛出相同的异常.
除了上述信息(支持常量池解析/正常方法返回/以及异常派发的数据)外, 虚拟机的实现者也可以将其他信息放入帧数据区, 如用于调试的数据等.


本地方法栈: -


引用 <<深入理解jvm虚拟机>>原书第2版第5章

原创粉丝点击