本节会介绍jvm的内存设计,并通过制造error,来体验内存溢出,辅助理解。
image.png这张图是java虚机规范(SE7)描述的。对于各种不同的虚拟机,只要在逻辑上遵从这样的设计即可,比如将虚拟机栈和本地方法栈合并,但是可以被抽象两个区的功能可。灰色块是所有线程共享,白色是线程独享。
功能介绍
具体请参考《深入理解java虚拟机》 这里的描述。这里仅概要记录自己理解的梳理。
程序计数器 Program Counter Register
一块小的内存,每个线程都有独立的。一个flag,指示下一条该执行的指令。上层的控制器,如字节码解释器,通过改变这个字段来控制JVM行为(可以想象,jvm底层有个循环,不断查询这个flag,并根据这个flag去行动)。
执行java方法时,这是 编译后java方法字节码地址。
执行Native方法,计数器值为空(Undefined)。
该区是唯一没有 OutOfMerrorError区
java虚拟机栈 Java Virtual Machine Stacks
线程私有,生命周期同线程。java方法执行的内存模型,为每个方法创建一个栈桢(stack Frame):局部变量,操作数栈,动态链接,方法出口等。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
异常
- StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常”
- OutOfMemoryError: 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈 Native Method Stack
与虚拟机栈类似。Native 方法是虚拟机自己的方法,类似于linux内核的系统调用。这个栈也是为这些方法产生的变量和信息做保存。
Java堆 Java Heap
内存管理,所有的对象及数组存放的地方。heap数据结构,具有自我排序,非常方便存储如放入的树状结构。
GC回收机制,主要采用分代回收的机制:新生代,老年代
不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区 Method Area。
永久代,即永久存在的数据(不是绝对)。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池 Runtime Constant Pool
方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。当无法满足内存分配需求时,将抛出OutOfMemoryError异常。
* 直接内存 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异常。”
hotpot 新建对象
详细过程 《深入理解java虚拟机》 2.3.1 对象的创建
过程
- 虚机遇到new指令
- 常量池,是否有该类服务的引用,如new String("abc")
- 所属类是否被加载。没有则执行加载过程。
- heap分配内存。有几种分配策略
- 初始化,设置对象的信息。例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中
- 执行<init>。程序员自己定义的行为。
对象的内存分布
对象在内存中的存储布局分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- 对象头 HotSpot虚拟机的对象头包括两部分信息
第一部分存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为"Mark Word"。对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于[…]。
第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身,这点将在2.3.3节讨论。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
- 实例数据部分 各种类型字段内容
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。
- 对齐填充
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象访问的定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象
句柄访问
那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
句柄访问
直接指针访问
那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。 直接指针
*对比
句柄 reference 中的句柄地址稳定,对象被移动(GC会移动)开销小。
直接指针 节省一次定位成本,速度快。hotspot用的这种。
实战:制造OutofMemoryError 与StackOverflowError
类型:heap溢出
环境:IDEA
- 测试文件
import java.util.ArrayList;
import java.util.List;
public class TestOutMemoryError {
//空对象,会占用heap堆
static class OOMObject {
int[] temp =new int[2000];
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
int i=0;
//不断申请
while (true)
{
System.out.println(i++);
list.add(new OOMObject());
}
}
- 设置堆大小 参考
入口-Xms: 堆最小为10m; -Xmx:堆最大为10m; -Xmn: 新生代初始化为5m
vm参数
- 输出
1119
1120
1121
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at TestOutMemoryError$OOMObject.<init>(TestOutMemoryError.java:9)
at TestOutMemoryError.main(TestOutMemoryError.java:19)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
Process finished with exit code 1
类型:java方法区溢出
环境:IDEA
- 测试文件
public class TestOutMemoryError {
//空对象,会占用heap堆
private int stackLength = 1;
//方法不断调用自身,虚拟机栈不断申请
public void stackLeak(){
System.out.println(stackLength);
stackLength++;
stackLeak();
}
public static void main(String[] args) {
new TestOutMemoryError().stackLeak();
}
}
- 设置堆大小 参考
-Xms: 堆最小为10m; -Xmx:堆最大为10m; -Xmn: 新生代初始化为5m;操作同上
- 输出
12059
12060
12061
Exception in thread "main" java.lang.StackOverflowError
at java.base/sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:564)
at java.base/java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:578)
at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:292)
at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:281)
at java.base/sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.base/java.io.OutputStreamWriter.write(OutputStreamWriter.java:211
类型:常量池溢出 ;参考《深入理解Java虚拟机2》
类型:本机内存溢出 ;请参考《深入理解Java虚拟机2》
“由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因”
网友评论