Java虚拟机 --- 内存区域

时间:2022-12-27 15:51:09

一,运行时数据区域

1,Java虚拟机栈

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

  局部变量表存放了编译期可知的各种基本数据类型,其中64位长度的long和double类型的数据会占用2个局部变量空间,其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;如果虚拟机可以动态扩展,扩展时无法申请到足够的内存,就会跑出OutOfMemoryError异常。

 

2,Java堆

  堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

  Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代。

  Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样。

  如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

3,方法区

  方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息/常量/静态变量/即时编译器编译后的代码等数据。

  方法区和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。

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

 

4,运行时常量池

  它是方法区的一部分。Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

 

二,虚拟机对象

1,对象的创建

  a. 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载/解析和初始化过。如果没有,那必须先执行相应的类加载过程。类加载检查过后,虚拟机为新生对象分配内存,相当于从Java堆中划出一块内存来。由于对象的大小在类加载完成后就可以确定,所以也就可以确定划分出的内存大小。

  b. 划分内存有两种方式:一是指针碰撞,这种方法只有在内存空间是绝对规整时才能用,内存空间被一个分界点指针分为已用区域和未用区域。给对象分配内存就相当于将分界点指针往未用区域挪动一段与对象大小相等的距离。二是空闲列表,这种方法适用于堆不规整的时候,虚拟机维护了一个列表,记录哪些内存区域可用,然后从列表中找到一块足够大的空间划分出与对象大小相等的一块区域,然后更新列表上的记录。Java内存是否规整取决于垃圾收集器是否有压缩整理功能,如果采用压缩整理算法,系统采用的分配算法就是指针碰撞,如果采用标记清除算法,通常采用空闲列表。

  在并发情况下分配内存并不是线程安全的,有可能正在给A分配内存,指针还没改,对象B又同时使用了原来的指针。解决线程安全的方法有两种:一是对分配内存空间的动作进行同步处理--虚拟机采用CAS + 失败重试的方法保证更新操作的原子性。二是把内存分配的动作按照线程划分在不同的空间中进行,也就是说每个线程在Java堆中预先分配一小块内存,成为本息线程分配缓冲(Thread Local Allocation Buffer)。哪个线程要分配内存,就在它的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以用--XX:+/-UseTLAB参数设定。

  c. 虚拟机将分配到的内存空间都初始化为零值,这一步保证了对象的实例字段可以不赋初值就可以使用。

  d. 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例,如何找到类的元数据信息,对象的hash,对象的GC分代年龄等信息。这些信息存放在对象的对象头中。

  e. 执行对象的<init>方法,把对象按照程序员的意愿进行初始化。

一个真正的对象就算完全产生出来了。

 

2, 对象的内存布局

  对象在内存中存储的布局可以分为3块区域:对象头(Header)/实例数据/对齐填充。

 

3,对象的访问定位

  Java程序通过栈上的reference数据来操作堆上的具体对象,目前主流的访问方式有两种:使用句柄和直接指针。

  使用句柄方式: 

好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

Java虚拟机 --- 内存区域

Java虚拟机 --- 内存区域

 

  使用直接指针:

好处是速度更快,因为它节省了一次指针定位的时间开销。

Java虚拟机 --- 内存区域

 

三,OutOfMemory异常

1,Java堆溢出

  Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。

  将堆的最小值-Xms参数与最大值-Xmx参数设置为一样就可以避免堆自动扩展。通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump处当前的内存堆转储快照以便事后进行分析。

package com.ivy.vm;

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

/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*
@author ivy
*
*/
public class HeapOOM {

static class OOMObject {

}
public static void main(String[] args) {
// TODO Auto-generated method stub
List<OOMObject> list = new ArrayList<>();

while(true) {
list.add(
new OOMObject());
}
}

}

结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14664.hprof ...
Heap dump file created [
27606448 bytes in 0.147 secs]
Exception in thread
"main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:
3210)
at java.util.Arrays.copyOf(Arrays.java:
3181)
at java.util.ArrayList.grow(ArrayList.java:
261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:
235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:
227)
at java.util.ArrayList.add(ArrayList.java:
458)
at com.ivy.vm.HeapOOM.main(HeapOOM.java:
21)

 

2, 虚拟机栈和本地方法栈溢出

  栈容量只由-Xss参数设定。在Java虚拟机规范中描述了两种异常:

  如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

package com.ivy.vm;

/**
* VM args: -Xss128k
*
@author ivy
*
*/
public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak() {
stackLength
++;
stackLeak();
}
public static void main(String[] args) {
// TODO Auto-generated method stub

JavaVMStackSOF oom
= new JavaVMStackSOF();
try {
oom.stackLeak();
}
catch (Throwable e) {
System.out.println(
"stack length:" + oom.stackLength);
throw e;
}
}

}

结果:

stack length:980
Exception in thread
"main" java.lang.*Error
at com.ivy.vm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:
13)
at com.ivy.vm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:
14)
at com.ivy.vm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:
14)
....

实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是*Error异常。

  上边的实验是在单线程情况下进行的,如果在多线程下就会产生内存溢出异常,但这样产生的内存溢出异常与栈空间是否足够大没有任何联系,在这种情况下,为栈分配的内存越大,反而越容易产生内存溢出异常。原因是:操作系统分配给每个进程的内存是由限制的,假设为2GB。虚拟机提供了参数来控制Java堆(Xmx)和方法区(MaxPermSize)的最大值,由于程序计数器消耗内存很小,可以忽略,再假设虚拟机进程本身耗费的内存不计算在内,那剩余的内存= 2GB - Xmx - MaxPermSize,剩余这部分内存就被虚拟机栈和本地方法栈瓜分了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。所以要在开发多线程的应用时要注意。

  出现*Error异常可以阅读错误堆栈找到问题所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下达到1000--2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该够用了。但是如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少堆的最大值和减少栈容量来换取更多的线程了。