深入理解JVM(一)——Java内存区域

一、Java的技术体系

  1. 现如今已经有600多万软件开发者依赖于Java的技术体系,而Java的设备已经超过了45亿,其中包括8亿多台个人计算机、21亿部移动电话及其他手持设备、35亿个智能卡,以及大量机顶盒、导航系统等其他设备。Java能获得如此广泛的认可,很大一部分原因是因为它的跨平台可移植性,即“一次编写,到处运行”;
  2. 它提供了一种相对安全的内存管理和访问机制,避免了绝大部分内存泄漏和指针越界问题;它实现了热点代码检测和运行时编译即优化,这使得Java应用能够随着运行时间的增长而获得更高的性能。它有一套完整的应用程序接口,还有无数的第三方库来帮助用户实现各种各样的功能。
  3. 从广义上讲,Kotlin、Groovy等运行于Java虚拟机上的编程语言及相关程序都属于Java技术体系中的一员。如果仅从传统意义上来看,JCP官方所定义的Java技术体系包括了一下几个组成部分:
    (1)Java程序设计语言
    (2)各种硬件平台上的Java虚拟机实现
    (3)所有Class文件格式的文件
    (4)Java类库API
    (5)来自商业机构和开源社区的第三方Java类库
  4. Java技术体系所包括的内容(图片来源于《深入理解Java虚拟机》):

二、Java虚拟机家族

  • Sun Classic/Exact VM:Classic虚拟机是世界上第一款商用虚拟机,被称为虚拟机始祖,而由于初期解释器和编译器不能配合工作,所以执行效率很低。Java很慢的印象也是这时候建立起来的。后期Sun的虚拟机团队也努力去解决过这个问题,发布了Exact VM,它的编译系统已经有了现代编译器的雏型,不过很快就被HotSpot VM取代,寿命十分短暂。
  • HotSpot VM:JVM世界的王者,也是OracleJDK和OpenJDK中的默认Java虚拟机,也是目前使用范围最广的JVM.
    • HotSpot VM 即继承了之前两款商用虚拟机的优点,也有许多自己的新技术优势,如它名称中的HotSpot指的就是它的热点代码检测技术,热点代码探测技术可以通过执行计数器找出最有编译价值的代码,然后通知即时编译器以方法为单位进行编译。如果一个方法被多次调用,或者方法中的有效循环次数很多,就会触发即时编译和栈上替换编译行为。
    • 通过编译器和解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无需等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更复杂的代码优化技术,输出更高质量的代码。
  • Mobile/Embedded VM:Sun/Oracle公司除了研发Sun Classic和Exact VM之外,对于移动端和嵌入式市场也有专门的虚拟机产品——Java ME。
    • 因为Java ME的产品线不如SE那么成功,所以ME的Java虚拟机要比SE的虚拟机低调的多。现在大多数的ME的主要移动端市场环境就是安卓,原本ME是想做安卓手机上的跨平台,但是很少有人会在ios上安装Java虚拟机
    • 而在嵌入式设备上ME又面临着自家产品SE的直接竞争,ME经过多年的扩充,核心部分已经与Java SE十分接近,能用SE的地方,大家都用SE。所以在嵌入式设备上,Java ME现在也在一个很尴尬的位置上。
除了上文中介绍的之外,还有很多虚拟机产品,因为太占篇幅所以没有介绍,但是下面还是隆重介绍一下有可能成为未来王者的虚拟机——Graal VM

Graal VM的口号是“Run Programs Faster Anywhere”,这个一个在HotSpot虚拟机基础上增强而成的跨语言 全栈 虚拟机,这是一种跨语言平台的,真正意义上跟物理计算机对接的虚拟机,理由是他和物理硬件的指令集一样,做到了只和机器的特性相关而不和某种高级语言特性相关。随着GraalVM1.0的发布,我们已经可以证明拥有高性能的多语言虚拟机是可能的。其发展潜力非常值得期待。

三、Java内存区域与内存溢出异常

1. Java运行时的数据区

如图:深入理解JVM(一)——Java内存区域_第1张图片

2. 程序计数器:

(1)占用内存空间比较小,可以看作是当前线程执行的字节码行号指示器,Java模型中,就是靠着这个指令来控制程序的指示器,完成程序的分支、循环、跳转、异常处理、线程恢复等操作。
(2)由于多线程是由一个处理器切换不同线程实现的,所以为了保证切换线程之后的准确性,每个线程都要有一个独立的程序计数器。如果一个线程正在执行的是一个Java方法那么这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,那么程序计数器此时应该为空(Undefined)。
(3)程序计数器是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

3. Java虚拟机栈:

(1)与程序计数器一样,虚拟机栈也是线程私有的。它会在每个方法被执行的时候,创建一个栈帧。每个方法自执行到执行结束,对应的就是一个栈帧从入栈到出栈的过程。
(2)每个栈帧中存储了局部变量——Java虚拟机基本数据类型、对象引用(可能是对象起始地址的引用指针,也可能是一个代表对象的句柄或者其他与此对象相关的位置),和returnAddress类型(指向一条字节码指令的地址)。
(3)这些数据类型在局部变量表中以局部变量槽(Slot)来表示,除了64位长度的long和double会占用两个变量槽,其余局部变量都占用一个变量槽。局部变量表所需内存在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的内存空间完全是确定的。在方法运行期间不会改变局部变量表的槽数量。
(4)在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况;如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOvefrFlow异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,就会抛出OOM。

4. 本地方法栈

与Java虚拟机栈一样,本地方法栈的作用也是用栈帧存储局部变量表,而Java虚拟机执行的是Java字节码,而本地方法栈执行的是native方法。有些虚拟机(如hotspot)就直接把虚拟机栈和本地方法栈合二为一。本地方法栈也会抛出StackOverFlow和OOM异常,触发条件与Java虚拟机相同。

5. Java堆

(1) 对Java应用程序来说,Java堆是Java所管理的内存中最大的一块。Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里几乎所有对象都在这里创建,这里说几乎是因为,即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象都是分配在堆上也不是那么绝对了。
(2) Java堆是垃圾回收器管理的内存区域,由于大部分垃圾回收器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”、“老年代”、“永久代”等名词,然而,这只是一个方便区分的统称,并不要求统一。而到了今天,hotspot里面也出现了不采用分代回收的垃圾处理器。所以按照分代区域命名堆中的各区域的说法也值得商榷了。

6. 方法区

(1)方法区和Java堆一样是被线程共享的一块区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法去描述为堆的一个逻辑部分,但是它在运行逻辑上是要与Java堆区分开来的。
(2)人们在似乎更喜欢把方法区称为永久代,但是对于某些虚拟机软件来说,是不存在永久代的概念的。其实用永久代去定义方法区并不是一个好主意,这样会更容易发生内存溢出的事故,所以oracle在收购BEA获得JRockit的所有权之后,在JDK1.7中,将原本放在永久代的字符串常量池、静态变量等移出,而到了JDK1.8,终于完全废弃了永久代的概念,将方法区定义的所有数据都移到元空间中。
(3)《Java虚拟机规范》对方法区的约束时非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域的确是比较少出现,这个区域的垃圾回收主要是针对常量池的回收与和对类型的卸载,一般来说在这个区域的回收结果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分的回收有时又确实有必要。以前Sun公司的Bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄露的。

7. 运行时常量池

(1)运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。
(2)Java虚拟机对于Class文件每一部分的格式都有严格规定。比如每一个字节用于存储哪种数据都必须符合Java虚拟机规范,才会被加载和执行。但是对于运行常量池,《Java虚拟机规范》并没有规定具体的细节要求。
(3) 运行时常量池相对于Class文件常量池的另一个重要的特征就是具备动态性,Java语言并不要求常量一定只有在编译期才会产生。也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以放入新的常量进入池中,这种特性用的比较多的是String类的intern()方法。

8. 直接内存

(1)直接内存并不是虚拟机运行时的数据区一部分,也不是《Java虚拟机规范》中定义的内存区域。但这部分内存也被频繁的使用,而且也可能导致OOM。
(2)在JDK1.4中新加入了NIO类,引入了一种基于通道与缓存区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

你可能感兴趣的:(Java,java,jvm)