JVM运行时数据区

JVM运行时数据区域

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。——周志明《深入理解Java虚拟机》

当我刚开始编写java代码时,特别是重构旧代码时,常常被各种奇怪的报错困扰,为何Handler会导致内存泄漏,为何会报illegal forward error? 一个对象何时出生,又何时死去?对象的内部,各种数据域之间又有何区别?遇到单例模式时,又困惑于为何静态类的写法可以保证只有一个单例。 要了解这些问题,写代码时不再畏首畏尾,仅仅知道Java的语法是不够的,需要了解JVM。 了解JVM,先从Java虚拟机如何划分其运行时数据区域开始。

JVM的运行时数据区域,主要分成两个大部分,即进程共享的(由所有线程共享),和线程共享的。

一、 程序计数器(Program Counter Register)

记得以前学微机原理时,有个PC指针,用来指示下一条指令的位置。程序计数器,是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。程序控制流依赖程序计数器来完成分支、循环、跳转、异常处理、线程恢复等基础功能。 Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,每个线程都有一个程序计数器。因此程序计数器是线程私有的。
此外,如果线程正在执行的是一个Java方法,程序计数器记录的就是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法(Native),那么这个计数器的值应该是为空(Undefined)。程序计数器线程私有,仅仅用来记录字节码指令的行号,因此不会发生内春溢出,这个内存区域是《Java虚拟机规范》里为一没规定任何OutOfMemoryError情况的区域。

二、Java虚拟机栈(JVM Stack)

Java虚拟机栈描述的是方法执行的线程内存模型。从这句话我们可以看出,java虚拟机栈和程序计数器一样,也是线程私有的。那“方法执行”的意思呢?每个方法在执行的时候,java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。此外,既然是“栈”,就会用到出栈、入栈的操作,这对应着方法调用到执行完毕的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
注:
对象引用和句柄:

reference类型,它并不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置——周志明《深入理解Java虚拟机》>

句柄,这个周志明的书中也有介绍,可以看作一个当作中转站的数据结构,是对象引用去定位和访问堆中对象的一种方式。对象引用中存储的是句柄地址,而句柄中包含了对象的实例数据和类型数据各自的地址。使用句柄不够直接,但是胜在稳定,在对象被移动时,只用修改句柄中的实例数据指针即可,reference本身不用修改。

三、本地方法栈(Native Method Stack)

与JVM Stack作用类似,区别是虚拟机栈时为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地方法服务。

以上的程序计数器、栈(虚拟机栈、本地方法栈)都是线程私有的,随线程而生,随线程而死,栈深度溢出时会报StackOverflowError异常,而栈扩展(如果可以扩展的话)失败时会抛出OutOfMemoryError异常。

四、Java堆(Java Heap)

堆十分重要,当我们谈论对象、谈论垃圾回收时,我们就是在谈论Java堆,因为:

所有的对象实例以及数组都应该放在堆上分配—— 《Java虚拟机规范》

尽管周志明书中说java对象实例都分配在堆上渐渐变得不是那么绝对了,但我在遇到具体的例子之前,姑且这么认为吧。
垃圾收集器的经典分代设计:将java堆分成新生代、老年代、永久代这样的划分设计。这些并不是《Java虚拟机规范》里对Java堆的进一步划分,而是业界各种虚拟机所采用的各种设计风格中的一种,当然这一种影响力巨大,是为主流。

五、方法区(Method Area)

方法区用于存储已经被虚拟机加载过的类型信息常量静态变量、即时编译器编译后的代码缓存等数据。这些数据是在类的加载过程中,从class文件里获取的。其实简单来说,就是把class文件的内容加载到内存里,这个内存被成为Method Area方法区。方法区是线程共享的。

运行时常量池 (Runtime Constant Pool)

每一个Class文件中,都维护着一个常量池表(Constant Pool Table, 保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;JVM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string, integer, 和floating point常量)和对类型,域和方法的符号引用(不同于我们常说的引用,它们是对类型,域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用;存在这里面的数据,类似于保存在数组中,外部根据索引来获得它们。这部分在学习过class文件结构后比较好理解)。池中的数据项象数组项一样,是通过索引访问的。 因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。
并非只有在编译器才能产生常量,也即并不是只有预置到Class文件中常量池表的的内容才能进入运行时常量池,运行期也可以加入将新的常量放到池中。看android源码中的Intent类,可以看到一个String 的intern()方法.

类变量

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。

Java堆和非堆(Not-Heap,方法区的别名),都是线程共享的,当内存紧缺无法申请到内存时,都会抛出OutOfMemoryError异常。

我们已经简单的了解了JVM运行时数据区,这是后面学习类加载机制、垃圾收集器等内容的基础。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值