1、Java内存区域与内存溢出异常

JVM整体分布

JVM 整体组成可分为以下四个部分:

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

各个组成部分的用途:

程序在执行之前先要把java代码转换成字节码(class文件),jvm首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是jvm的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。


而我们通常所说的jvm组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的Heap(堆)模块,那接下来我们来看运行时数据区(Runtime Data Area)是由哪些模块组成的。

主要介绍Java虚拟机内存的各区域,以及它们的作用、服务对象以及可能产生的问题。

按照上图Java虚拟机运行时数据区做个梳理

Java虚拟机运行时数据区

1、程序计数器(线程私有)

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

2、Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的内存模型:++每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息++。每个方法的被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

通常说的“栈”就是指虚拟机栈,或者说是虚拟机栈中的局部变量表,它可以存放基本数据类型、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

异常情况:线程请求栈深度大于虚拟机所允许深度,StackOverflowError异常;虚拟机栈可以动态扩展,扩展无法申请到足够的内存时抛出OutOfMemoryError异常

1
2
3
4
5
6
7
//例子
public void sayHello() {
String name = "hello";
A a = new A();
}
//就在方法里定义了一个局部变量“name”,存放在栈中。
//a放在栈中,指向对中new A()

虚拟机栈中的局部变量表

虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈,八大数据类型(boolean、byte、char、short、int、float、long、double)。

  1. 对象引用(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
  2. ReturnAddress类型(指向了一条字节码指令的地址) 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。局部变量表所需的内存控件在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  3. 操作数栈:字节码指令指令给予操作数栈的指令架构,执行其指令需要从局部变量表或缓存中加载操作数到操作数栈中,改写再回到对应区域。
  4. 动态链接:多态,运行时根据实际类型进行虚方法分派

3、本地方法栈(线程私有)

本地方法栈与虚拟机栈的作用非常相似,区别是前者++为Native方法服务++,后者为Java方法服务。

4、Java堆(线程共享)

内存中最大一块,虚拟机启动时创建。++它的唯一目的就是存放对象实例,所有对象实例以及数组都要在堆上分配++。它是垃圾收集管理的主要区域,也被叫做GC堆,由于现在基本都采用分代收集算法,Java堆还可以细分为:新生代和老年代;

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

1
2
3
4
5
6
7
8
9
10
11
public void sayHello(String name) {
Student student = new Student(name);
student.study();
}
static int a = 10;
String a = "abc";

//“new Student(name)” 这个代码,就是创建了一个Student类型的对象实例放在堆中
//Student的“name”就是属于这个对象实例的数据,也会存放在Java堆内存里
//然后方法的栈帧的局部变量表里,会存放这个引用类型的“student”局部变量,即存放Student对象的地址
//注意:JDK1.8后 类变量和字符常量池都放在堆中,比如说"abc" 和 a=10都在堆中

5、方法区(线程共享)

它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。也被称为“永久代”,内存回收主要针对常量池和对类型的卸载。

异常情况:无法满足内存分配需求将会抛出OutOfMemoryError异常。

注意:方法区存放常量是指class文件中的类的常量,而非实例类中方法中的final常量。

例子:下面类中,abc三个常量存在常量池中,且常量池中只有一个1,abc指向1。de同理只有一个2在常量池,de指向2。

1
2
3
4
5
6
7
8
class Demo{
private final int a = 1;
private final int b = 1;
private final int c = 1;

private int d = 2;
private int e = 2;
}

常量池在CLASS文件中的位置:

常量池表示范围:

  • 运行时也可以添加新的常量,如String.intern()
  • 优点在于避免频繁的创建和销毁对象影响性能,实现对象共享
  • 基本来源于各个class文件中的常量池

6、运行时常量池

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

异常情况:常量池无法再申请到内存时会抛出OutOfMemoryError异常。

7、直接内存

不是虚拟机运行一部分,也被规范定义的内存区域。但也会被频繁使用

HotSpot虚拟机在Java堆中对象分配、布局和访问全过程

1、对象创建

创建过程

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果有先执行相应的类加载过程,通过后为新生对象分配内存。

在堆中分配空间方式

指针碰撞
假设Java堆中内存绝对规整,所用内存和空闲内存各方一边,中间放着一个指针作为分界点指示器,所分配内存就是把指针往空闲空间挪动一段与对象大小相同的距离
空闲列表
Java堆中内存并不规整,使用内存和空闲内存相互交错,虚拟机必须维护一个列表,记录哪些内存块可用,在分配时找一块足够大的空间划分给对象,并更新记录表

线程安全

介绍
创建对象频繁,仅修改指针指向位置,在并发时并不是线程安全的,可能在给对象A分配内存,指针没来得及改,对象B又使用原来的指针来分配内存
解决
对分配内存空间的动作进行同步处理,采用CAS配上失败重试的方式保证更新操作原子性
把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲TLAB

后续过程

在内存分配完成后,分配到的内存空间都初始化为零值,对对象进行必要设置,找到对象的元数据等信息,存在在对象头,最后执行方法

2、对象的内存布局

对象头

1、用于存储对象自身运行时的数据如哈希吗,GC分代年龄等,是非固定的数据结构
2、类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪类的实例

实例数据

对象真正存储的有效信息,也是代码中定义的各种类型的字段内容

对齐填充

非必要的存在,起着占位符的作用,实例数据未补齐,就需要通过对齐填充来补全

3、对象的访问定位

关于对象访问,涉及到了Java栈、Java堆、方法区三个重要区域之间的关联关系

Object obj = new Object();

Object obj会在Java栈中的本地变量表,作为一个reference类型数据出现
new Object()在Java堆中形成一块存储了Object类型所有实例数据值
从方法区中查找对应的对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息

两种访问对象的方式,通过句柄访问对象、通过直接指针访问对象