天天看点

深入理解Java虚拟机(一)Java内存区域与内存溢出异常

前言

1.Java代码为什么可以跨平台?

  因为Java程序编译之后的代码不是能被计算系统直接运行的代码,而是一种“中间码”-----字节码。这种字节码不是纯二进制的字节码,而是基于Unicode的字节码,它不依赖于特定的计算机硬件架构而存在。然后不同的硬件系统装有不同的Java虚拟机(JVM),有JVM把字节码“翻译”成所对应硬件平台能够执行的代码。总的来说,Java之所以能跨平台运行,是因为JVM可以跨平台安装。

深入理解Java虚拟机(一)Java内存区域与内存溢出异常
2. Java中是如何管理内存的?

Java的内存管理就是对象内存的分配和释放问题。

分配: 内存的分配是由程序完成的,我们需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都是在堆中分配空间的。

释放: 对象的释放时由JVM中的**垃圾回收机制(GC)**决定和执行的。这样的操作虽然简化了人类的工作,然后加重了JVM的工作。因为为了能够正确的释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、复制等,GC都需要进行监控。

3. 总结

  通过上面的两个问题,我们知道了Java跨平台的原因就是因为Java虚拟机可以跨平台安装,Java源文件经过编译生成的.class字节码文件,字节码文件由Java虚拟机解释运行。在这里体现了Java虚拟机的重要性。

  在Java开发中,我们不像在C/C++,需要自己写free/delete来释放申请的内存空间,在Java中内存是交给虚拟机管理的,Java虚拟机提供了我们的便利性,但是也是由于我们把内存的控制权交给了虚拟机,如果我们写的程序如果出现了内存泄漏和或一出问题,应该怎么解决?这将成为一个极大的困难,这就让我们必须了解虚拟机的内存管理。

1. JVM概念

JVM: Java Virtual Machine,意为Java虚拟机。(在全文中为了统一,我会统一将Java虚拟机简称为JVM)。

虚拟机: 通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。(我的理解:通过软件模拟完整的硬件环境。)

常见的虚拟机: JVM、VMware、Virtual Box

JVM与其他两个虚拟机的区别:

  • VMware与Virtual Box是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器。
  • JVM是通过软件模拟Java字节码的指令集,JVM主要保留了PC寄存器,其他寄存器都进行了裁剪,JVM是一台被定制过的现实当中不存在的计算机。

2. 运行时数据区域

  JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建于与销毁。JVM所管理的内存将会包括一下几个运行时数据区域。

深入理解Java虚拟机(一)Java内存区域与内存溢出异常

通过上图我们可以发现JVM所管理的内存包含了一下区域:

线程私有区域:

程序计数器 Java虚拟机栈 本地方法栈

线程共享区域:

Java堆 方法区 运行时常量池

 在这里我们先点名一下,运行时常量池在JDK1.7时在方法区中,JDK1.7后移动到堆中。

2.1 程序计数器(线程私有)

1. 概述

程序计数器时一块比较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。

2. 作用

通过改变计数器的值选取下一条需要执行的字节码指令。(分支、循环、跳转、异常处理、线程恢复等)基础功能都依赖与其完成。但这个仅仅只是概念模型,虚拟机可能会通过一些更高效的方法去实现。

3. 特点
  • 无内存溢出: 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(Out Of Memory Error)情况的区域。
  • 线程私有:
    由于JVM多线程是通过线程轮流切换并分配处理器执行时间的方法来实现,因此在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,每条线程之间的计数器互不影响,独立存储。

2.2 Java虚拟机栈(线程私有)

1. 概述与存储

虚拟机栈描述的是Java方法执行的内存模型。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。

  当线程执行一个方法是,就会随着创建一个对应的栈帧,并将建立的栈帧压栈;当方法执行完成之后,便会将栈帧出栈;因此,线程当前执行的方法所对应的栈帧必将定位于Java栈的顶部。

深入理解Java虚拟机(一)Java内存区域与内存溢出异常
2. 作用

  存储局部变量表、操作数栈、动态链接、方法返回地址 和 一些额外的附加信息。

我们之前一直将的栈区域实际上就是这里的虚拟机栈中的局部变量表部分。

3. 局部变量表

  用于存储方法中的局部变量。对于基本数据类型的变量,直接存储它的值;对于引用类型的变量,则存的是指向它对象的引用。局部变量表的大小在编译时就确定好了,在执行期间不会改变局部变量表的大小。

4. 特点
  • 线程私有
  • 生命周期与线程相同
5. 产生的异常

此区域一共会产生以下两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverFlowError异常。例如:递归无出口。
  • 虚拟机在动态扩展是无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常。

2.3 本地方法栈(线程私有)

  本地方法栈与虚拟机栈的作用完全一样,它俩的区别就是本地方法栈为虚拟机使用的Native方法服务,虚拟机栈为JVM执行的Java方法服务。

  作用与虚拟机栈完全一样,异常也与本地虚拟机栈一样,抛出StackOverFlowError和OutOfMemoryError异常。

  在虚拟机规范中,对本地方法栈中使用的语言,使用方法的数据结构没有强制规定,因此虚拟机可以自由的实现它,甚至在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。

2.4 Java堆(线程共享)

1. 概念与存储

   Java堆是JVM所管理的最大内存区域。 Java堆这块内存区域存放的都是对象实例。JVM规范中说到:“所有对象的实例以及数组都要在堆上分配。”

  Java堆时垃圾回收管理的主要区域,因此很多时候称之为“GC堆”。根据JVM规范规定的内容,Java堆处于物理上不连续的内存空间中,只要逻辑上时连续的就可以。

2. 作用

此区域唯一的目的就是存放对象实例。

3. 特点
  1. 所有线程共享的一块区域。
  2. 在JVM虚拟机启动时创建。
4. 异常
  • 如果堆中没有足够的内存完成实例分配并且堆也无法扩展时,将会抛出OOM异常。

2.5 方法区(线程共享)

1. 概述

  方法区于Java堆一样,是各个线程共享的内存区域。Java虚拟机规范将方法区描述为堆的一个逻辑部分,他有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开。方法也被称为“永久代”(JDK8已经被元空间取代)。

  永久代并不意味着数据进入方法区就永久存在,此区域的内存回收主要是针对常量池的回收以及对类型的卸载。

2. 作用

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3. 特点

线程共享。

4. 异常

JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。

2.6 运行时常量池

 cc运行时的常量池时方法区的一部分,存放字面量与符号引用。

 Java 语言并不要求常量池一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容后才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中,这种特性用的比较广泛的便是 String 类的 intern() 方法。

字面量: 字符串(JDK1.7后移动到堆中)、final常量、基本数据类型的值。

符号引用: 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。