Java内存分配

时间:2024-05-28 16:07:08

概述

对从事C和C++的程序员来说,在内存管理方面,他们既是拥有最高权利的人,也是从事最基础工作的“劳动人民”。

而对于Java程序员来说,JVM自动进行内存管理,程序员不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄露和内存溢出问题。

但是,正因为JVM帮我们管理了内存,一旦出现内存泄露或溢出问题,如果不了解虚拟机是怎么管理内存的,那么排查错误会成为一项异常艰难的工作

So, 小伙伴们,走起,让我带你们进入JVM的内存世界!


运行时数据区域(Run-Time Data Areas)

首先,叫“Java内存分配”并不是我本意,我是为了能让大家搜索进来才起的这个名字,Java(JVM)关于内存的管理是一个叫:“运行时数据区”的章节

JVM在运行时(运行时就是执行Java程序时)根据《Java虚拟机规范(Java SE 7)》的规定,会包括如下几个运行时数据区域

Java内存分配


1. 程序计数器(Program Counter Register)

或者叫:程序计数寄存器、PC寄存器,学过计算机组成原理应该就懂

a. 作用

  1. 程序计数器是一块较小的内存空间,可以看做是当前线程(Thread)所执行的字节码的行号指示器

    在JVM概念模型中(各种JVM可能并不依照此开发),解释器(Interpreter)就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

  2. 为了在线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器。这种内存区域称为“线程私有”的内存

  3. 线程正在执行Java方法时,计数器记录的是虚拟机字节码指令的地址;如果执行的是Native方法,则计数器值为空(Undefined)

b. Error

这块内存是JVM规范中唯一没有规定任何OutOfMemoryError的区域


2. 虚拟机栈(Virtual Machine Stacks)

也是线程私有的,其生命周期和线程一样,每个Java线程有一个虚拟机栈

a. 作用

虚拟机栈描述的是Java方法执行的内存模型,即:每个方法在执行的时候都会创建一个栈帧(Stack Frame),栈帧中存储:

  1. 局部变量表

    1. 存放了编译期就可知的:各种基本数据类型(8个基本数据类型)、对象引用、returnAddress类型(指向一条字节码指令地址)
    2. 局部变量表所需的内存大小在编译期就完成了分配,也就是说当进入一个方法时,此方法需要在栈帧中分配多大的局部变量表空间时完全确定的,运行期不会改变
  2. 操作数栈

  3. 动态链接

  4. 方法出口等

方法从调用到执行完成的过程,就对应了,一个栈帧在虚拟机栈中的入栈和出栈的过程

b. Error

有两种异常: 
1. 如果线程请求的栈深度大于JVM所允许的深度,将抛出*Error异常 
2. 如果栈扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常


3. 本地方法栈(Native Method Stack)

a. 作用

作用和虚拟机栈基本一样,只不过这里为Native方法服务 
JVM规范没有强制规定本地方法栈中的方法使用的语言、使用方式、数据结构,所以具体JVM不同实现

HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一了

b. Error

如1.2的虚拟机栈一样


4. Java堆(Java Heap)

基本上Java堆是虚拟机管理的内存中最大的一块。是被所有线程共享的一块区域,在虚拟机启动时创建,通过参数“-Xmx和-Xms”控制

a. 作用

所有对象实例和数组都要在堆上存放(例外的情况在我另一篇博客有描述JVM - JIT编译器 - 6.1) 
Java堆是垃圾回收器管理的主要区域

b. 分类

下面是一些具体的细分,但是不论如何分类,其存储的仍然是对象实例,进一步划分的目的是为了更好的回收、更快的分配内存

1. 从内存回收的角度看

由于现代GC基本都采用分带收集算法,所以Java堆还可以细分为:

  1. 新生代
  2. 老年代

再细分一下还可分为:

  1. Eden空间
  2. From Survivor空间
  3. To Survivor空间

2. 从内存分配角度看

线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

c. Error

如过堆中内存不够继续进行实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError


5. 方法区(Method Area)

方法区和Java堆一样,被所有线程共享

a. 作用

用于存储已被虚拟机加载的

  1. 类信息(class metadata)
  2. 常量(包括interned Strings)
  3. 静态变量(类变量 class static variables)
  4. 即时编译器编译后的代码等

虽然JVM规范把方法区描述为堆的一个逻辑部分,但是它确有一个别名叫Non-Heap,目的应该是与Java堆区分开来

b. 永久代(HotSpot特有)

加删除线的原因请看b.2. 现状 
对于使用HotSpot VM的程序员来说,很多人把方法区称之为“永久代(Permanent Generation)”

为什么叫永久带?如1.4中所说,按内存回收的角度,有新生代、老年代,所以就有了这里的“永久代”。另外,其它虚拟机是没有永久代这个概念的

本质上两者不等价,仅仅是因为HotSpot团队把GC分代收集扩展到了方法区(或者说用永久代这种方式来实现方法区),这样的话GC就可以像管理Java堆一样管理这部分内存,省去了为方法区编写内存管理代码的工作

1. 坏处

如何实现方法区JVM规范并没有强制规定,但是现在看来“永久代”并不是一个好主意

  1. 更容易遇到内存溢出问题 
    因为有参数“-XX:MaxPermSize”的上限限制,其它虚拟机只要不达到进程可用内存上限,例如32系统的2GB,就不会出现内存溢出
  2. 有极少数方法(如String.intern()),会因此导致在不同的JVM下有不同表现

2. 现状

  1. 在JDk1.7的HotSpot中,字符串常量池已经被从永久代中移除了
  2. 在Java8中,根据JEP122,永久带PermanentGeneration已经被从HotSpot中removed.
  3. 这是JDK1.8中JRockit和HotSpot合并的成果
  4. 一篇翻译自国外的译文:Java永久代去哪儿了

c. 垃圾回收

JVM规范对方法区限制非常宽松,甚至可以选择不实现垃圾收集

但并不是如其“永久代”的名字一样,方法区的垃圾回收只是比较少出现。回收目标是:类信息的卸载、常量池的回收

d. Error

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常


6. 运行时常量池(Run-Time Constant Pool)

运行时常量池其实是1.5方法区的一部分↑

class文件中有一项信息是常量池表(constant_pool table),用于存放编译期生成的“字面量”和“符号引用”,这部分内容将在类加载后进入方法区的运行时常量池中(Run-Time Constant Pool)存放

也就是说:每一个class都会根据constant_pool table 来1:1创建一个此class对应的Run-Time Constant Pool

a. 作用

  1. 就是运行时所需要的常量数据的容器
  2. JVM规范对class文件的每一部分(包括constant_pool table)都有严格的规范,但是对于运行时常量池却没有做任何细节要求,不过一般来说,除了class文件中的符号引用外,直接引用也会存储在运行时常量池中
  3. 运行时常量池具备动态性,Java语言并没有要求常量一定只能编译期产生,运行期也可以将新常量放入池中。这个特性用的较多的便是String类的intern()方法

b. Error

既然运行时常量池是方法区的一部分,自然受到方法区限制,当运行时常量池无法再申请到内存时,将抛出OutOfMemoryError异常

x. 字符串常量池 - String pool

注意:这并不是Run-Time Constant Pool的一部分,放在这里只是为了能让大家在比较相似的地方找到

与上面两个概念不一样,String pool是用来存储被驻留的字符串的(interned string),是JVM实例全局唯一的,被所用类共享

HotSpot中实现string pool的方式是一个哈希表:StringTable,这个StringTable的value是字符串实例的引用,也就是说某个普通的字符串实例如果被纳入StringTable的引用之后就等同于被赋予了“interned string”的身份

再有,类的运行时常量池的CONSTANT_STRING类型的常量,经过解析(resolve)后,同样是存字符串的引用,解析的过程中回去查询StringTable,来保证运行时常量池和StingTable所引用的字符串是一致的

关于字符串部分,请看我的另一篇专门写字符串常量池的博文


7. 直接内存(Direct Memory)

直接内存不是JVM规范中定义的内存区域,也不是运行时数据区的内容 
(我现在理解为:直接控制的属于物理机的内存,不属于JVM线程使用的内存) 
但是,这部分内从也被频繁的使用,且可能导致OutOfMemoryError异常,所以罗列为1.7在这里叙述

a. 缘由

JDK1.4中加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,这个类可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

b. Error

直接内存不会受到Java堆大小限制,但是会受到本机总内存大小,以及处理器寻址空间的限制。JVM管理员在配置JVM参数时,会根据本机实际内存设置(如-Xmx等参数),但是经常忽略了要被使用的这一份“直接内存”。最终使得各个内存总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常



TIPS

    1. 字面量(literal)

      int a;//a变量 
      static final int b=10;//b为常量,10为字面量 
      string str=”hello world”;//str为变量,hello world为也字面量 
      字面量只能以右值的形式出现

    2. 符号引用

      符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用 项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出 类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出 类名、方法名以及方法的描述符。