深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

时间:2023-03-10 06:40:23
深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

概述

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

一、运行时数据区域

在java程序执行的过程中,jvm会把它所管理的内存区域划分为不同的数据区域,因为在计算机中,操作系统是老大,操作系统分配一定的内存给到jvm,jvm就是一个小家庭,拿到了土地了,比如这块呢做房子,那块种菜园,后面一块做池塘。。这样每块地都互不干扰,也都有自己的用途。如图:

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

红框区域代表是所有线程共享的区域,绿色区域则非线程共享,每个线程都自己管理这些数据区域。

1、堆

大部分的java项目中,堆内存都是最大的,因为堆的唯一目的就是存放对象实例,包含所有的对象以及数组都存放在这里。但是随着JIT编译器的发展,这一点也不是那么绝对了,这点以后再介绍,当前可以认为所有对象都存放在堆内存中。

另外堆中内存还可以细分:Eden、From Survivor、To Survivor、Old Generation,前三者属于新生代,最后一个则是老年代。

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

2、栈

在java中,栈分为虚拟机栈以及本地方法栈。是java程序方法执行时的内存模型,当执行到一个方法时,栈中就会创建一个栈帧,如图所示:

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

本地方法栈与虚拟机栈的作用非常类似,只是虚拟机栈执行的是java方法,本地方法栈执行的是native方法。

3、程序计数器

是一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

4、方法区

用于存储已被虚拟机加载的类信息、常量(放入运行时常量池)、静态变量、即时编译器编译后的代码等数据。说到运行时常量池,string很有发言权,当声明String s = "虚拟机";首先自然是创建一个string对象放在堆中,另外会把"虚拟机"字符串放入到

运行时常量池,可以理解成缓存,不明白的可以看走进JDK(二)------String

另外值得一提的就是直接内存,这块内存不归jvm管理,但是在nio中使用到,可以使用native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。拿nio来说,这种操作能够大大提高性能,因为避免了在java堆和native堆中来回复制数据。

二、对象创建过程

当我们new xxx()时,可曾想过背后到底发生了什么? 看图:

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

三、对象内存布局

对象在内存中存储的布局主要分为三块:对象头、实例数据和对齐补充。
对象头:
1、用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
2、另一部分是类型指针,即对象指向它的类元数据的指针,迅疾通过这个指针来确定这个对象时哪个类的实例。
实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。父类继承下来的字段,也会在子类中存储。
对齐补充:仅仅起着占位符的作用。主要是因为jvm要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍,所以当实例数据部分没有对齐时,就需要通过对齐填充来补全。

四、对象的访问定位 

对象创建好了之后,如何使用对象呢?通过栈上的reference数据来操作堆上的具体对象,主流的访问方式有句柄和直接指针两种:

1、句柄

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

优点:当对象移动的时候(垃圾回收的时候移动很普遍),这样值需要改变句柄中的指针,但是栈中的指针不需要变化,因为栈中存储的是句柄的地址
缺点:需要进行二次定位,寻找两次指针,开销相对于更大一些

2、直接指针

深入理解java虚拟机(一)-----java内存区域以及内存溢出异常

优点:速度快,不需要和句柄一样指针定位的开销

对于sun hotspot虚拟机来说,使用的是第二种方式。

五、OutOfMemoryError与*Error

1、OutOfMemoryError:

各内存区域中,栈、堆、方法区、直接内存都有可能抛出此异常。

栈:当虚拟机栈可以动态扩展时,无法申请到足够的内存时,就会抛出此异常。

while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true) { }
}
});
thread.start();
}

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

List list = new LinkedList();
while(true) {
list.add(student);
}

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

2、*Error:

线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。

这么说估计很多人都不明白,来个示例:

。。。main() {
method()
}
//method()不停的调用自己,此时就会出现*Error
void method() {
method()
}