Java运行时数据区域

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙里面的人想出来。

                         ------------------------------------------------------------摘自《深入理解Java虚拟机》周志明

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

在jdk1.8之前和jdk1.8之后略有不同,主要表现在线程共享的方法区上,jdk1.8之后将之前的方法区改名为元空间(Metapace)并移动到与直接内存(Diect Memory)一块。

 程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

为了线程切换后能恢复到正确的执行位子,每条线程都需要一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称之这类内存区域为“线程私有“的内存。

如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Ubdefined)。

且程序计数器的内存区域是唯一一个不存在OutOfMemoryError 的内存区域,它的⽣命周期随着线 程的创建⽽创建,随着线程的结束⽽死亡。

Java 虚拟机栈

与程序计数器一样,Java虚拟机栈也是私有的,它的生命周期与线程相同。Java虚拟机栈描述的是Java方法执行执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java内存可以粗糙的划分为堆内存(Heap)和栈内存(Stack)。

局部向量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、 long、double)、对象引⽤(reference 类型,它不同于对象本身,可能是⼀个指向对象起始地址 的引⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

这些数据类型在局部变量表中的存储空间可以以局部变量槽(slot)表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只会占用一个。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里的大小是指变量槽的数量,虚拟机真正使用多大的内存空间(即一个变量槽占用32比特、64比特)来实现一个变量槽,这完全由具体的虚拟机实现来自行决定。

Java 虚拟机栈会出现两种错误: StackOverFlowError 和 OutOfMemoryError

  • StackOverFlowError : 若 Java 虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈 的深度超过当前 Java 虚拟机栈的最⼤深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError : 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也⽆法提供更多 内存的话。就会抛出 OutOfMemoryError 错误。

本地方法栈

本地方法栈和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节 码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。

本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、 操作数栈、动态链接、出⼝信息。

⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误

Java 堆

Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启 动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分 配内存。

Java世界中“⼏乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐 成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化,所有的对象都分配到堆上也渐渐 变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被 返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃 圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新 ⽣代和⽼年代:再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的 ⽬的是更好地回收内存,或者更快地分配内存。

Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,但对于大对象(如:数组对象),多数虚拟机实现出于简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现为固定大小,也可以是扩展的,当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。

在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下⾯三部分:

1. 新⽣代内存(Young Generation)

2. ⽼⽣代(Old Generation)

3. 永⽣代(Permanent Generation)

 JDK 8 版本之后⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代 之是元空间,元空间使⽤的是直接内存。

 上图所示的 Eden 区、两个 Survivor 区都属于新⽣代(为了区分,这两个 Survivor 区域按照顺 序被命名为 from 和 to),中间⼀层属于⽼年代。

⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则 会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1), 当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年 龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

堆这⾥最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有⼏ 种,⽐如:

1. OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执⾏垃圾回收并且 只能回收很少的堆空间时,就会发⽣此错误。

2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不⾜ 以存放新创建的对象, 就会引发 java.lang.OutOfMemoryError: Java heap space 错误。(和本机物 理内存⽆关,和你配置的内存⼤⼩有关!)

⽅法区

⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把⽅法区描述为堆的⼀ 个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。

⽅法区也被称为永久代,但两者并不完全等价。

《Java 虚拟机规范》只是规定了有⽅法区这么个概念和它的作⽤,并没有规定如何去实现 它。那么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像 Java 中接⼝和类的关系,类实现了接⼝,⽽永久代就是 HotSpot 虚拟机对虚拟机规范中⽅ 法区的⼀种实现⽅式。 也就是说,永久代是 HotSpot 的概念,⽅法区是 Java 虚拟机规范中 的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并 没有永久代这⼀说法。

 JDK 1.8 之前永久代还没被彻底移除的时候通常通过下⾯这些参数来调节⽅法区⼤⼩

-XX:PermSize=N //⽅法区 (永久代) 初始⼤⼩

-XX:MaxPermSize=N //⽅法区 (永久代) 最⼤⼤⼩,超过这个值将会抛出 OutOfMemoryError 异 常:java.lang.OutOfMemoryError: PermGen

相对⽽⾔,垃圾收集⾏为在这个区域是⽐᫾少出现的,但并⾮数据进⼊⽅法区后就“永久存在” 了。

JDK 1.8 的时候,⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代 之是元空间,元空间使⽤的是直接内存。

下⾯是⼀些常⽤参数:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最⼩⼤⼩)

-XX:MaxMetaspaceSize=N //设置 Metaspace 的最⼤⼤⼩

与永久代很⼤的不同就是,如果不指定⼤⼩的话,随着更多类的创建,虚拟机会耗尽所有可⽤的 系统内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  • 整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,⽆法进⾏调整,⽽元空间使⽤的是直接内 存,受本机可⽤内存的限制,虽然元空间仍旧可能溢出,但是⽐原来出现的⼏率会更⼩。

-XX MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着 它只受系统内存的限制。

-XX MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标 志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。

  • 元空间⾥⾯存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, ⽽由系统的实际可⽤空间来控制,这样能加载的类就更多了。
  • 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东⻄, 合并 之后就没有必要额外的设置这么⼀个永久代的地⽅了。

运⾏时常量池

运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息 外,还有常量池表(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)。这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,Java语言并不要求常量一定只有编译期才能产生,即并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。

既然运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存 时会抛出 OutOfMemoryError 错误。

直接内存

直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部 分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 错误出现

JDK1.4 中新加⼊的 NIO(New Input/Output) 类,引⼊了⼀种基于通道(Channel) 与缓存区 (Buffer) 的 I/O ⽅式,它可以直接使⽤ Native 函数库直接分配堆外内存,然后通过⼀个存储 在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样就能在⼀些场景中显著提⾼性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤⼩以及 处理器寻址空间的限制。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值