少年帮|JVM内幕:Java虚拟机详解( 二 )


栈的限制栈可以是动态分配也可以固定大小 。 如果线程请求一个超过允许范围的空间 , 就会抛出一个StackOverflowError 。 如果线程需要一个新的栈帧 , 但是没有足够的内存可以分配 , 就会抛出一个 OutOfMemoryError 。
栈帧(Frame)每次方法调用都会新建一个新的栈帧并把它压栈到栈顶 。 当方法正常返回或者调用过程中抛出未捕获的异常时 , 栈帧将出栈 。 更多关于异常处理的细节 , 可以参考下面的异常信息表章节 。
每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用
局部变量数组局部变量数组包含了方法执行过程中的所有变量 , 包括 this 引用、所有方法参数、其他局部变量 。 对于类方法(也就是静态方法) , 方法参数从下标 0 开始 , 对于对象方法 , 位置0保留为 this 。
有下面这些局部变量:
  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress
除了 long 和 double 类型以外 , 所有的变量类型都占用局部变量数组的一个位置 。 long 和 double 需要占用局部变量数组两个连续的位置 , 因为它们是 64 位双精度 , 其它类型都是 32 位单精度 。
操作数栈操作数栈在执行字节码指令过程中被用到 , 这种方式类似于原生 CPU 寄存器 。 大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作 。 因此 , 局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行 。 比如 , 一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码 。
int i;被编译成下面的字节码:
0:iconst_0// Push 0 to top of the operand stack1:istore_1// Pop value from top of operand stack and store as local variable 1更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息 , 可以在类文件结构部分找到 。
动态链接每个栈帧都有一个运行时常量池的引用 。 这个引用指向栈帧当前运行方法所在类的常量池 。 通过这个引用支持动态链接(dynamic linking) 。
C/C++ 代码一般被编译成对象文件 , 然后多个对象文件被链接到一起产生可执行文件或者 dll 。 在链接阶段 , 每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址 。 在 Java中 , 链接阶段是运行时动态完成的 。
当 Java 类文件编译时 , 所有变量和方法的引用都被当做符号引用存储在这个类的常量池中 。 符号引用是一个逻辑引用 , 实际上并不指向物理内存地址 。 JVM 可以选择符号引用解析的时机 , 一种是当类文件加载并校验通过后 , 这种解析方式被称为饥饿方式 。 另外一种是符号引用在第一次使用的时候被解析 , 这种解析方式称为惰性方式 。 无论如何, JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误 。 绑定是将对象域、方法、类的符号引用替换为直接引用的过程 。 绑定只会发生一次 。 一旦绑定 , 符号引用会被完全替换 。 如果一个类的符号引用还没有被解析 , 那么就会载入这个类 。 每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量 。
线程间共享堆堆被用来在运行时分配类实例、数组 。 不能在栈上存储数组和对象 。 因为栈帧被设计为创建以后无法调整大小 。 栈帧只存储指向堆中对象或数组的引用 。 与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同 , 对象总是存储在堆上以便在方法结束时不会被移除 。 对象只能由垃圾回收器移除 。
为了支持垃圾回收机制 , 堆被分为了下面三个区域: