阅读本文大概需要5分钟

作者:AI乔治出处:https://my.oschina.net/u/3611782/blog/4530512

1 讨论背景

周志明老师写的《深入理解Java虚拟机》应该很多程序员都读过,第二章中阐述了Java虚拟机在执行Java程序的过程中是如何管理内存的,以及这些内存是如何被划分成更细的逻辑区域的。如下图所示,按照书中的论述JVM运行时数据区域包含以下几个数据区[1]。

按照《Java虚拟机规范(Java SE 7版)》,各区域的功能简要介绍如下:

  • 程序计数器:各线程私有。用于记录每个线程下一条待执行的字节码指令以及相关信息。这是唯一的不会抛出OOM异常的区域。

  • Java虚拟机栈:各线程私有。虚拟机栈由一个个的栈帧组成,每个栈帧包含了对应方法执行所需要的信息,具体包括:局部变量表、操作数栈(类似于编译型语言体系下的数据寄存器)、动态链接(某些接口符号可能会动态的指向不同的目标方法)、函数返回地址以及其他一些相关信息。理论上当函数调用链超过栈的深度时就会触发StackOverflow,当该区域设置为动态扩展时,虚拟机无法为栈申请到更多内存时就会触发OOM。事实中基本上不管哪种情况,结果都很可能会是StackOverflow,因为栈容量和栈帧的大小决定了栈的深度(栈帧大小*深度<=栈容量),所以当OOM时,栈深度一定也已经不够用了,所以抛出StackOverflow异常也无可厚非。可以通过“-Xss”来配置虚拟机栈固定大小。

  • Java堆:各线程公有。虚拟机工作的主要内存区域(大部分情况下也是最大的),绝大部分对象实例的内存分配都在这里进行。Java 7和之前的Java堆细分为:新生代(伊甸区、存活区0、存活区1)、年老代和永久代。Java 8去除了永久代,替换以Metaspace。在JVM的运行中,大部分情况下,GC主要就发生在堆区域,

  • 方法区:各线程公有。用于存放类定义、常量池、静态变量(static修饰)、编译后的字节码等。方法区实际上是从堆上划分出来的一块区域,但是其GC机制是单独的,与堆不同,所以为了区分方法区和堆,通常又把方法区叫做“非堆”。方法区对应了堆中的永久代。因此在Java8以及之后版本中,永久代被抹除了,方法区也移到了元数据空间(metaspace)中。

  • 运行时常量池:各线程公有。用于存放类信息中的常量(字面量、符号引用等),每个类编译后的信息中的都有一个常量池,可以通过javap -vebose xxxx.class命令来查看。

  • 直接内存:进程间公有。直接内存不属于Java虚拟机运行时数据区的一部分,它是指操作系统分配给虚拟机以及其他进程所运行的那块内存区域,之所以这么说,是因为很多服务器都是虚拟机(操作系统级别),对于物理机来说,这块内存就是指操作系统所管控的物理内存。通过在堆中创建一个DirectByteBuffer实例来对直接内存进行访问。

很多读者了解完这些后还是云里雾里,各论坛还是会出现各种没有定论的问题,比如

  1. 字符串常量池属于哪个数据区?书中对字符串常量池和运行时常量池描述的相当晦涩和模糊。

  2. Java6、Java7和Java8的运行时内存数据区域到底有何不一样?

  3. 什么是字面量,什么又是字符串常量?

  4. 什么是本地内存?他和直接内存相同嘛?什么又是堆外内存?

下面我们围绕这几个问题做一些讨论和引申,从而帮助我们更好的理解运行时数据区域划分。

2 字符串常量池

我们先来回答第一和第二个问题。

2.1 字符串常量池在哪

在不同的Java版本中,规范规定的字符串常量池的位置也不一样。以下三张图分别代表了Java6、Java7和Java8体系下的Java虚拟机与运行时数据区域划分,哪些是线程私有,哪些是线程公有,哪些又是进程间公有都比较清晰了。

2.1.1 Java 6 虚拟机运行数据区

当我们听到“字符串常量池也是方法区的一部分”的时候,我们要知道他大概暗指的是Java 6或者之前的版本。如上图所示,在Java 6虚拟机规范中,字符串常量池确实是方法区的一部分,受永久代内存区大小的限制。当频繁使用Spring.intern()时,可能会引发OOM(PermGen space)。

2.1.2 Java 7 虚拟机运行数据区

从Java 7 开始,规范将字符串常量池迁移到了Java堆中,受Java堆大小的限制。当频繁大量使用String.intern()时,可能会引发OOM(Java heap space)。

2.1.3 Java 8 虚拟机运行数据区

Java 8 虚拟机规范彻底移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的则是元空间(Metaspace)。字符串常量池仍然在Java堆中,但方法区已经迁移到了元空间中。这时候由于滥用 String.intern()引发的OOM依旧在Java堆中。

2.2 字符串常量池是啥

那么字符串常量池的数据结构是怎么实现的呢?答案是HashMap,每个字符串常量池对应了一个StringTable的数据结构,其本质并不是Table,而是一个HashMap。这个HashMap的容量是固定的(默认1009),可以通过-XX:StringTableSize来设置,注意这个值是指哈希表中桶的数量,不是占用内存的大小。所以这个值最好是一个质数,并且要大于默认的1009[2]。

3 字面量和字符串常量

如以下代码:

String str = "123";

其中”123”就是我们经常看到的“字面量”。字面量是随着Class信息等在类被加载完毕后一起进入运行时常量池的。而

String str2 = str.intern();

这句代码则尝试将str的值放入字符串常量池,然而”123”已经在类信息的常量池中了,所以StringTable实际记录的是类信息常量池中该字符串的引用。

对于语句:

String str = new StringBuilder("hello").append(" world").toString().intern();

这会将新创建的“hello world”的堆内对象引用(str)放入到字符串常量池中,因为这是第一次出现,没有其他地方存在该值的引用。

4 本地内存和直接内存

首先需要说明的是,本地内存(Native Memory)和堆外内存(Off-heap Memory)的含义是一样的。而关于直接内存和本地内存的关系,StackOverflow上也没有说清楚的帖子,第二部分中的三张图已经可以很好的说明直接内存和本地内存的关系了,所谓的本地内存是操作系统分配给JVM虚拟机(作为一个进程)使用的内存块中除去堆的那一部分。而直接内存则是所有进程共享的操作系统所控制的内存。所以可以这么说:本地内存和直接内存的关系就像“苹果”和“水果”的关系,苹果属于水果,是水果更具体的限定。Java8中的元空间就属于本地内存空间,而他们都是直接内存的一部分。通过DirectByteBuffer分配的内存区域一定在本地内存中,它也受直接内存大小的限制。本地内存的大小也有限制,比如Window中对每个程序运行所需的内存大小做了2G的默认限制,这只时候其上运行的JVM的本地内存大小≈2G-JVM堆内存大小。

5 字符串常量池所属数据区的具体说明

下面我们举2个例子讨论下在Java6和Java7(含之后版本)下字符串常量池迁移带来的变化

5.1 例子1

请给出以下代码抛出异常的类型:

import java.util.ArrayList;import java.util.List;

public class Test {  public static void main(String[] args){          Listlist = new ArrayList();int i = 0;while(true) {list.add( String.valueOf(i++).intern());        }     }}

然后启动参数中我们加上:

-XX:PermSize=10M -XX:MaxPermSize=10M

分析下这个代码,其意图在于不断的产生新的字符串,并且放入字符串常量池中,试图撑爆永久代。然而这只会在Java 6 中发生,对于Java7和Java8来说,字符串常量池已经迁移到了Java堆中,如果这时候我们添加以下虚拟机参数:

-Xms10M -Xmx10M

则会引发:java.lang.OutOfMemoryError: GC overhead limit exceeded 这样的错误,这个异常的本质与 OOM(Heap space)一直,都是堆内存溢出。

5.2 例子2

以下代码在Java6和Java7中输出也不相同:

public class TestStringConstantPool {

public static String hello = "Hello Java";

public static void main(String[] args) {

String str1 = new StringBuilder("Hello ").append("World").toString();       System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("Hello ").append("Java").toString();     System.out.println(str2.intern() == str2);  }}

在Java6中会输出:

falsefalse

在Java7中则输出:

 truefalse

首先我们分析下Java6中的场景,Java6中字符串常量池还是运行时常量池的一部分,所以使用String.intern()时,会把堆中的字符串复制到方法区中,返回的是方法区中的对象引用。所以不管如何,堆中对象和方法区中对象应用都不会想等。而在Java7中,这个情况发生了变化,字符串常量池转移到了堆中,对于str1来说,字符串常量池StringTable会记录其在堆中的引用(即str1)。所以str1.intern() == str1成立。而str2情况则不一样了,因为“Hello Java”字符串已经存在于方法区的运行时常量池中,所以intern()返回的是方法区中的对象引用。所以str2.intern() == str2不成立。

国民程序员

运行时错误7内存溢出_JVM运行时内存数据区域相关推荐

  1. jvm内存模型和java内存模型_JVM运行时内存模型综述

    JVM内存模型 JVM分为五个区域:虚拟机栈.本地方法栈.方法区.堆.程序计数器. JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小. JVM五个区中虚拟机栈.本地方法栈.程序计数器 ...

  2. Java运行时的数据区域

    1.程序计数器 1.什么是程序计数器 当前线程所执行的字节码的行号指示器 2.特点 线程私有:每个线程都有一个独立的程序计数器,互不影响,独立存储,便于线程的切换 3.工作机制 所属的线程执行Java ...

  3. JVM内存不足增大运行时内存

    添加jdk参数 -Xms768m -Xmx768m -XX:CompileThreshold=8000 -XX:PermSize=768m -XX:MaxPermSize=768m -Xverify: ...

  4. Java虚拟机运行时的数据区域

    1.堆区(Heap) (1)Heap是被所用线程共享的区域,是虚拟机中内存最大的一块. (2)它的唯一目的就是存放对象的实例. 例如:A a=new A(100); 存放在堆中的是A(100)这个ne ...

  5. [转]JVM运行时内存结构

    [转]http://www.cnblogs.com/dolphin0520/p/3783345.html 目录[-] 1.为什么会有年轻代 2.年轻代中的GC 3.一个对象的这一辈子 4.有关年轻代的 ...

  6. JVM运行时内存概念-堆栈及新生代、老年代、持久代

    JVM内存有一些按不同标准划分的概念,这里来理清一下. 一.首先java虚拟机规范定义了java运行时数据区在概念上应该有的分区,这是抽象概念不对应也不限制物理上的具体实现,不同的虚拟机可以有不同的实 ...

  7. 运行时常量池在哪里_Java虚拟机详解(二)------运行时内存结构

    首先通过一张图了解 Java程序的执行流程: 我们编写好的Java源代码程序,通过Java编译器javac编译成Java虚拟机识别的class文件(字节码文件),然后由 JVM 中的类加载器加载编译生 ...

  8. 【Android 逆向】Android 逆向基本概念 ( 软件运行时内存结构 | 文件与内存之间的联系 )

    文章目录 一.Android 软件运行时内存结构 二.Android 文件与内存之间的联系 一.Android 软件运行时内存结构 Android 软件运行时内存结构 : 硬件层 : 内存中 , 硬件 ...

  9. 深入理解java虚拟机一 JAVA运行时内存区域与class文件

    一 JAVA运行时内存区域 JVM在加载class文件时,会将class文件定义的数据结构转为运行时内存中的数据,那么jvm是如何安排运行时的内存区域呢? jvm将运行时内存划分为以下几个部分: 堆: ...

最新文章

  1. 基于监督学习+自监督学习的智能抠图,精确到发丝 | CVPR 2020
  2. 强交变磁场下的AD转换数值的变化
  3. java包装器类_Java中的基本类型和包装类
  4. 网络应用程序设计模式
  5. 用“连接”勾勒角色:《死亡搁浅》亡人的设计及其背后的故事谜题
  6. NeurIPS 2019|腾讯AI Lab详解入选论文,含模仿学习、强化学习、自动机器学习等主题...
  7. 01.query的分类-条件查询和组合查询
  8. maven的安装、路径配置、修改库文件路径和eclipse中的配置、创建maven工程(转)...
  9. c语言grade d10,《电子技术10级C语言课程设计报告书写规范》.doc
  10. 美团开源 Logan Web:前端日志在 Web 端的实现
  11. 买了套房28年后才想起来,房价从33万涨到600万!上门却发现住着人?
  12. 制作首页的显示列表(2017.11.29)
  13. javascript示例代码
  14. centos 7 局域网丢包排查_一文掌握docker centos 安装python3.7「精品」
  15. 如何查看论文被收录的情况?
  16. 八大排序算法原理及实现
  17. TeeChart Pro ActiveX教程(十九):TeeChart工具集(六)
  18. 时间漩涡的世界 (三)
  19. matlab可见光通信,可见光通信研究工作matlab代码
  20. POJ 1359 Spacecraft Malfunction G++

热门文章

  1. 两张超级大表join优化
  2. Python简单遍历字典及删除元素的方法
  3. 按要求罗列所有字符串字符序列
  4. 接口抽象类继承父类和子类
  5. Cache模拟器(CacheSim)
  6. 知识管理系统Data Solution研发日记之六 窗体设计器
  7. 微软正在帮助政府构建AI灾难响应工具
  8. 转载:malloc()与new()的区别详解
  9. 检测到USB设备插入写法1
  10. c语言 临时文件作用,c语言函数mktemp()产生唯一临时文件名实例源码介绍