JVM 内存分配策略

JVM 内存分配策略

在分析JVM 内存分配策略之前先介绍一下通常情况下操作系统都是采用哪些策

略来分配内存的。

通常的内存分配策略

在操作系统中将内存分配策略分为三种,分别是:

◎ 静态内存分配;

◎ 栈内存分配;

◎ 堆内存分配。

静态内存分配是指在程序编译时就能确定每个数据在运行时的存储空间需求,因此在

编译时就可以给它们分配固定的内存空间。这种分配策略不允许在程序代码中有可变数据

结构(如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编

译程序无法计算准确的存储空间需求。

栈式内存分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静

态内存分配相反,在栈式内存方案中,程序对数据区的需求在编译时是完全未知的,只有

到运行时才能知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的

数据区大小才能够为其分配内存。和我们所熟知的数据结构中的栈一样,栈式内存分配按

照先进后出的原则进行分配。

在编写程序时除了在编译时能确定数据的存储空间和在程序入口处能知道存储空间

外,还有一种情况就是当程序真正运行到相应代码时才会知道空间大小,在这种情况下我

们就需要堆这种分配策略。

这几种内存分配策略中,很明显堆分配策略是最自由的,但是这种分配策略对操作系

统和内存管理程序来说是一种挑战。另外,这个动态的内存分配是在程序运行时才执行的,

它的运行效率也是比较差的。

Java 中的内存分配详解

从前面的 JVM 内存结构的分析我们可知,JVM 内存分配主要基于两种,分别是堆和

栈。先来说说Java 栈是如何分配的。

Java 栈的分配是和线程绑定在一起的,当我们创建一个线程时,很显然,JVM 就会为

这个线程创建一个新的Java 栈,一个线程的方法的调用和返回对应于这个Java 栈的压栈

和出栈。当线程激活一个Java 方法时,JVM 就会在线程的Java 堆栈里新压入一个帧,这

个帧自然成了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算

过程和其他数据。

栈中主要存放一些基本类型的变量数据(int、short、long、byte、float、double、boolean、

char)和对象句柄(引用)。存取速度比堆要快,仅次于寄存器,栈数据可以共享。缺点是,

存在栈中的数据大小与生存期必须是确定的,这也导致缺乏了其灵活性。

如下面这段代码:

public void stack(String[] arg) {

String str = "junshan";

if (str.equals("junshan")) {

int i = 3;

while (i > 0) {

long j = 1;

i--;

}

} else {

char b = 'a';

System.out.println(b);

}

}

这段代码的stack 方法中定义了多个变量,这些变量在运行时需要存储空间,同时在

执行指令时JVM 也需要知道操作栈的大小,这些数据都会在Javac 编译这段代码时就已经

确定,下面是这个方法对应的class 字节码:

public void stack(java.lang.String[]);

Code:

Stack=2, Locals=6, Args_size=2

0: ldc #3; //String junshan

2: astore_2

3: aload_2

4: ldc #3; //String junshan

6: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/

Object;)Z

9: ifeq 30

12: iconst_3

13: istore_3

14: iload_3

15: ifle 27

18: lconst_1

19: lstore 4

21: iinc 3, -1

24: goto 14

27: goto 40

30: bipush 97

32: istore_3

33: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;

36: iload_3

37: invokevirtual #6; //Method java/io/PrintStream.println:(C)V

40: return

LineNumberTable:

line 15: 0

line 16: 3

line 17: 12

line 18: 14

line 19: 18

line 20: 21

line 21: 24

line 22: 27

line 23: 30

line 24: 33

line 26: 40

LocalVariableTable:

Start Length Slot Name Signature

21 3 4 j J

14 13 3 i I

33 7 3 b C

0 41 0 this Lheap/StackSize;

0 41 1 arg [Ljava/lang/String;

3 38 2 str Ljava/lang/String;

在这个方法的attribute 中就已经知道了stack 和local variable 的大小,分别是2 和6。

还有一点不得不提,就是这里的大小指定的是最大值,为什么是最大值呢?因为JVM 在

真正执行时分配的stack 和local variable 的空间是可以共用的。举例来说,上面的6 个local

variable 除去变量0 是this 指针外,其他5 个都是在这个方法中定义的,这6 个变量需要

的Slot 是1+1+1+1+2+1=7,但是实际上使用的Slot 只有4 个,这是因为不同的变量作用

范围如果没有重合,Slot 则可以重复使用。

每个 Java 应用都唯一对应一个JVM 实例,每个实例唯一对应一个堆。应用程序在运

行中所创建的所有类实例或数组都放在这个堆中,并由应用程序所有的线程共享。在Java

中分配堆内存是自动初始化的,所有对象的存储空间都是在堆中分配的,但是这个对象的

引用却是在堆栈中分配的。也就是说在建立一个对象时两个地方都分配内存,在堆中分配

的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)

而已。

Java 的堆是一个运行时数据区, 这些对象通过new 、newarray 、anewarray 和

multianewarray 等指令建立,它们不需要程序代码来显式地释放。堆是由垃圾回收来负责

的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运

行时动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由

于要在运行时动态分配内存,存取速度较慢。

如下代码描述新对象是如何在堆上分配内存的:

public static void main(String[] args) {

String str = new String("hello world!") ;

}

上面的代码创建了一个String 对象,这个String 对象将会在堆上分配内存,JVM 创建

对象的字节码指令如下:

public static void main(java.lang.String[]);

Code:

Stack=3, Locals=2, Args_size=1

0: new #7; //class java/lang/String

3: dup

4: ldc #8; //String hello world!

6: invokespecial #9; //Method java/lang/String."<init>":(Ljava/lang/

String;)V

9: astore_1

10: return

LineNumberTable:

line 29: 0

line 35: 10

LocalVariableTable:

Start Length Slot Name Signature

0 11 0 args [Ljava/lang/String;

10 1 1 str Ljava/lang/String;

先执行new 指令,这个new 指令根据后面的16 位的“#7”常量池索引创建指定类型

的对象,而该#7 索引所指向的入口类型首先必须是CONSTANT_Class_info,也就是它必

须是类类型,然后JVM 会为这个类的新对象分配一个空间,这个新对象的属性值都设置

为默认值,最后将执行这个新对象的objectref 引用压入栈顶。

new 指令执行完成后,得到的对象还没有初始化,所以这个新对象并没有创建完成,

这个对象的引用在这时不应该赋值给str 变量,而应该接下去就调用这个类的构造函数初

始化类,这时就必须将 objectref 引用复制一份,在新对象初始化完成后再将这个引用赋

值给本地变量。调用构造函数是通过invokespecial 指令完成的,构造函数如果有参数要传

递,则先将参数压栈。构造函数执行完成后再将objectref 的对象引用赋值给本地变量1,

这样一个新对象才创建完成。

上面的内存分配策略定义从编译原理的教材中总结而来,除静态内存分配之外,都显

得很呆板和难以理解,下面撇开静态内存分配,集中比较堆和栈。

从堆和栈的功能和作用来通俗地比较,堆主要用来存放对象,栈主要用来执行程序,

这种不同主要是由堆和栈的特点决定的。

在编程中,如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量、形

式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就

好像工厂中的传送带一样,栈指针会自动指引你到放东西的位置,你所要做的只是把东西

放下来就行。在退出函数时,修改栈指针就可以把栈中的内容销毁。这样的模式速度最

快,当然要用来运行程序了。需要注意的是,在分配时,如为一个即将要调用的程序模块

分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,

但是分配的大小是确定的、不变的,而这个“大小多少”是在编译时确定的,而不是在运

行时。

堆在应用程序运行时请求操作系统给自己分配内存,由于操作系统管理内存分配,所

以在分配和销毁时都要占用时间,因此用堆的效率非常低。但是堆的优点在于,编译器不

必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长时间,因此,

用堆保存数据时会得到更大的灵活性。事实上,由于面向对象的多态性,堆内存分配是必

不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。在C++

中,要求创建一个对象时,只需用 new 命令编制相关的代码即可。执行这些代码时,会

在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价——在堆

里分配存储空间时会花掉更长的时间。

相关推荐