JAVA虚拟机内存模型
Posted Java技术充电站
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA虚拟机内存模型相关的知识,希望对你有一定的参考价值。
java内存模型,分为程序计数器,虚拟机栈,本地方法栈,java堆,java栈。根据受访的权限不同设置,可以分为线程共享和线程私有。线程共享指可以允许所有的线程共享访问的一类内存区域,包括堆内存区,方法区,运行的常量池。
程序计数器(Program Counter Register)
java虚拟机栈(Java Virtual Machine Stacks)
定义:
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame )用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
组成
栈帧由三部分组成:局部变量区,操作数,帧数据区
操作数栈也被定义为一个数字数组,,不同于局部变量区的通过下标访问,而是通过栈的push和pop操作。
帧数据区主要作用为
解析常量池的数据
方法执行完后处理方法返回,恢复调用方现场
方法执行过程中抛出异常时异常的处理,当出现异常时虚拟机查找相应的异常表看是否有对应的catch语句,如果没有就抛出异常终止这个方法调用。
StackOverflowError和OutOfMemoryError
在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。
StackOverflowError:可以通过-Xss选项来设置虚拟机栈的大小,栈的大小直接决定了函数调用的最大可达深度。
public class TestJvmStack {
static int count = 0;
public static void main(String[] args) {
try {
test1();
} catch (Throwable e) {
System.out.println("stack length is " + count);
e.printStackTrace();
}
}
private static void test1() {
count ++;
test1();
}
}
设置栈的深度为: -Xss1m
报错信息为:
函数的调用次数由栈的大小决定。一般来说,栈越大,函数嵌套调用次数越多。一个函数而言,它的参数越多,内部局部变量越多,它的栈帧越大,其嵌套的调用次数就会减少。
本地方法栈(native method stack)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
java堆
Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Collected Heap)。如果从内存回收的角度看,由于现在
收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
heap的OutOfMemoryError
import java.util.ArrayList;
import java.util.List;
public class JavaHeapOutOfMemoryError {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
while (true) {
list.add("此外,小南斯还告诫前湖人队友们,成为詹姆斯队友后,他们时刻会被置于放大镜下,这种压力他们必须能够应对。");
}
}
}
设置参数 -Xmx100m-Xms100m
方法区(Method Area)
方法区主要保存的信息是类的元数据。是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型的信息,常量池,域信息,方法信息。
- 类型信息包含类的完整名字,父类的完整名字,类型修饰符(private,protected,public)和类型的直接接口类表
- 常量池包含类方法,域等信息的常量信息
- 域信息包含域的名称,域的类型以及域的修饰符
- 方法信息包含方法名称,返回类型,方法参数,方法修饰符,方法字节码,操作数和方法栈帧的局部变量区的大小以及异常表
方法区溢出
package com.own.learn.jvm;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MethodAreaOutOfMemory {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestCase.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3)
throws
Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
static class TestCase {
}
}
vm options: -XX:MaxMetaspaceSize=5m
GC
JDK8开始,持久代已经被彻底删除了,取代它的是另一个内存区域也被称为元空间。
方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集
方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待。
方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常
jdk8中常量池放在堆中
常量池的OutOfMemoryError
import java.util.ArrayList;
import java.util.List;
public class ConstantPoolOutOfMemory {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i =1;
while (true) {
list.add(String.valueOf(i).intern());
i++;
}
}
}
VM options: -Xmx10m-Xms10m
直接内存(Direct Memory)
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError
异常。
直接内存的OutOfMemoryError
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class DirectoryMemoryOutOfMemory {
static int ONE_MB = 1024 * 1024;
static int index = 0 ;
public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(null);
while (true) {
index ++;
unsafe.allocateMemory(ONE_MB);
}
} catch (Exception e) {
System.out.println("index : " + index);
e.printStackTrace();
} catch (Error e) {
System.out.println("index : " + index);
e.printStackTrace();
}
}
}
vm options:-Xmx20M-XX:MaxDirectMemorySize=10M
虚拟机对象的访问
由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
句柄
直接指针
比较
以上是关于JAVA虚拟机内存模型的主要内容,如果未能解决你的问题,请参考以下文章