Java基础-Java运行时数据区
Java虚拟机定义了若干种程序运行时使用到的运行时数据区
- 第一种是随虚拟机的启动而创建,随虚拟机的退出而销毁
- 第二种是与线程一一对应,随线程的开始和结束而创建和销毁。
结构图
组成部分
程序计数器(Program Counter Register)
也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。在虚拟机的概念模型里(不同虚拟机中可能存在更高效的实现方式),字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能依赖这个计数器来完成。 为了线程切换之后能回到正确的执行位置,每条线程都需要有一个独立的程序计数器,因此程序计数器是私有的。 每一条JVM线程都有自己的PC寄存器,在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)
- 如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址
- 如果该方法是native,那PC寄存器的值是undefined
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈(Java Virtual Machine Stack)
与PC寄存器一样,java虚拟机栈(Java Virtual Machine Stack)也是线程私有的。每一个JVM线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。(可以简单认为一个栈帧就是一个方法,进行压栈) JVM stack 可以被实现成固定大小,也可以根据计算动态扩展。
- 如果采用固定大小的JVM stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVM Stack初始容量的手段。
- 如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。
JVM Stack 异常情况:
- StackOverflowError:当线程请求分配的栈容量超过JVM允许的最大容量时抛出(线程请求的栈深度大于虚拟机允许的栈深度)
- OutOfMemoryError:如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出。
本地方法栈(Native Method Stack)
Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack) 如果JVM不支持native方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。 如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配。 异常情况:
- StackOverflowError:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时抛出
- OutOfMemoryError:如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
方法区(Method Area)
方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法
- 方法区在虚拟机启动的时候创建。
- 方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
- 方法区在实际内存空间中可以是不连续的。
Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。 Java 方法区异常:
- OutOfMemoryError: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常。
相对而言,垃圾收集行为在这个区域是比较少出现的(所以常量和静态变量的定义要多注意)。方法区的内存收集还是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。 一般来说方法区的内存回收比较难以令人满意。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。
Java堆(heap)
在JVM中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。 Java堆载虚拟机启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。
- Java堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。
- Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。
- JVM实现应当提供给程序员调节Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。
- Java堆是垃圾收集器管理的主要区域
Java 堆异常:
- OutOfMemoryError:如果实际所需的堆超过了自动内存管理系统能提供的最大容量时抛出。
几乎所有的对象实例都在这里分配,不过随着JIT编译器的发展和逃逸技术的成熟,栈上分配和标量替换技术使得这种情况发生着微妙的变化,对上分配正变得不那么绝对。
附:在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。
运行时常量池(Runtime Constant Pool)
运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。 运行时常量池是方法区的一部分。每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到JVM后,对应的运行时常量池就被创建。 在创建类和接口的运行时常量池时,可能会遇到的异常:
- OutOfMemoryError:当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内存空间后就会抛出OutOfMemoryError
对象访问
创建过程
对象访问在Java语言中无处不在,即使是最简单的访问,也会涉及到Java栈,java堆,方法区这三个最重要的内存区域之间的关联关系。如下面的代码:
Object obj = new Object();
假设这段代码出现在方法体中,那么Object obj
部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型的数据存在。而new Object();
部分的语义将会反应到Java堆中,形成一块存储Object类型所有实例数据值(Instance Data)的结构化内存,根据具体类型以及虚拟机实现的对象分布的不同,这块内存的长度是不固定的。另外,在JAVA堆中还必须包含能查找到此对象内存数据的地址信息,这些类型数据则存储在方法区中。
由于reference类型在Java虚拟机中之规定了指向对象的引用,并没有规定这个引用要通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此虚拟机实现的对象访问方式会有所不同。
访问方式
主流的访问方式有两种:句柄访问方式和直接指针。
- 如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
- 如果通过直接指针方式访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象的地址。
两种方式各有优势,句柄访问方式最大的好处是reference中存放的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。而指针访问的最大优势是速度快,它节省了一次指针定位的开销,由于对象访问在Java中非常频繁,一次这类开销积少成多后也是一项非常可观的成本。
具体的访问方式都是有虚拟机指定的,虚拟机Sun HotSpot使用的是直接指针方式,不过从整个软件开发的范围来看,各种语言和框架使用句柄访问方式的情况十分常见。
其他:
Q1:内存如何存取数据?
-
内存最小单位是一些类似于二极管这样的东西,它能存储一个电状态,高或低,可表示1或0
-
这些单元经过组织起来保存数据,组织的方法是8位编成一个字节,4个字节一个字,每组数据都可以读写
-
这些单元按照顺序排放后用地址编号,按照地址可访问其中的任一个字、字节
-
这些电路访问时由两组数据连线:地址线和数据线,比如都是32位的,地址线描述要访问的具体单元,数据线存放要给这个单元赋值的数据(写访问)或读出的数据(读访问)
(
继续问:4G内存有多少根地址线?32根
) -
这些单元上电时才能保持状态,所以内存你一掉电(关机),其中的数据就丢失了
Q2:字节码如何查看?
最新版的IDEA支持Show ByteCode