首页 > 系统相关 >Java内存区域与内存溢出异常

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

时间:2023-02-25 16:00:53浏览次数:34  
标签:Java 对象 虚拟机 线程 内存 方法 溢出

一、Java内存区域
1、运行时数据区
JDK 1.8之前

JDK1.8之后

2、程序计数器
一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
3、虚拟机栈
特点
线程私有的,它的生命周期与线程相同
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
在活动线程中,只有位千栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作
栈帧
栈帧示意图

局部变量表
存放编译期可知的各种Java虚拟机基本数据类型、对象引用、returnAddress类型
基本数据类型: boolean、long、short、int、float、double、byte、char
对象引用: reference类型, 不是对象本身, 而是对象起始地址的引用指针或对象的句柄或其他与此相关的位置
returnAddress类型: 指向了一条字节码指令的地址
这些数据类型在局部变量表的存储空间以局部变量槽(Slot)来表示, 例如一个变量槽占32位, 则long和double类型的数据会占用2个变量槽, 其余的只会占用1个变量槽
局部变量表所需的内存空间在编译期间就已经分配, 当进入一个方法时, 这个方法所需在栈帧中分配的局部变量空间是完全确认的, 在方法运行期间不会改变局部变量表的大小(大小指的是变量槽的数量)
操作栈
操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。
JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。
字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。
i++ 和 ++i 的区别:
i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小
动态连接
每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接
方法返回地址
方法执行时有两种退出情况:
正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
返回值压入上层调用栈帧。
异常信息抛给能够处理的栈帧。
PC计数器指向方法调用后的下一条指令
两类异常情况
StackOverflowError异常: 线程请求的栈深度大于虚拟机所允许的深度, 将抛出该异常
OutOfMemoryError异常: 虚拟机栈容量可以动态扩展, 当栈扩展时无法申请到足够的内存时, 将抛出该异常
4、本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和 OutOfMemoryError 异常。
线程开始调用本地方法时,会进入 个不再受 JVM 约束的世界。本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 NativeheapOutOfMemory。
JNI 类本地方法最著名的应该是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系统的特性功能,复用非 Java 代码。 但是在项目过程中, 如果大量使用其他语言来实现 JNI , 就会丧失跨平台特性。
5、Java堆
对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常
6、方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。
为什么要使用元空间取代永久代的实现?
由于永久代有上限, 字符串存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
将 HotSpot 与 JRockit 合二为一。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
7、内存区域解析

二、HotSpot虚拟机中的对象
1、对象的创建过程(从new开始)
虚拟机创建对象
检查类是否被加载, 如果没有则进行类加载过程, 对象大小在类加载完成后就已知
为新对象分配内存空间
指针碰撞: Java堆中内存空间绝对规整的, 有足够连续的闲置内存空间分配, 通过指针位移一段与对象大小的位置来分配内存空间
空闲列表: Java堆中内存空间不规整, 维护一张表记录空闲内存空间, 从该张表划分内存给对象实例
分配内存空间不是线程安全的: 分配内存空间时, 可能出现给A对象分配了内存, 但在把指针赋给对象实例时, 对象B同样使用了A对象的指针来分配内存, 解决方案:
方案一: 对分配内存空间的动作进行同步处理, 采用CAS配上失败重试的方式保证更新操作的原子性
方案二: 把内存分配的动作按照线程划分在不同的空间进行, 即每个线程在堆中都有预先分配好的内存, 称为本地线程分配缓冲(TLAB), 哪个线程要分配内存, 就在哪个线程的本地缓冲区中分配, 只有本地缓冲区分配完啦, 分配新的缓存区, 才需要同步锁定
内存分配完成后, 虚拟机必须将分配到内存空间(不包括对象头)都初始化为0, 如果使用了TLAB, 则在分配TLAB时就初始化为0
对对象进行必要的设置
例如:
这个对象是哪个类的实例
如何才能找到类的元数据信息
对象的哈希码(实际上会延后到真正调用Object::hashCode()方法时才计算)
对象的GC分代年龄等信息
到第4步, 虚拟机视角, 新对象已经产生了, 但是从Java程序的视角来看, 对象的创建才刚刚开始; 之后会接着执行构造方法, 按照程序员的意愿对对象进行初始化, 这样一个真正可用的对象才算完全被构造出来
2、对象的内存布局
在HotSpot虚拟机里, 对象在堆内存中的存储布局可以划分为三个部分:

对象头

实例数据
对象真正存储的有效信息
对齐信息
这并不是必然存在的, 也没有特别的含义, 仅是占位的作用, 保证对象是 8 的倍数
3、对象的访问定位
通过栈上的 reference 数据来操作堆上的对象
对像访问方式是有Java虚拟机实现的, 主流的访问方式有如下两种:
使用句柄
堆中划分一块内存来作为句柄池, 栈中的 reference 指向对象句柄地址, 而句柄中包含对象实例数据的指针和对象类型的指针

直接指针
reference 存储的就是对象地址

使用句柄和直接指针的区别
使用句柄: reference 指向句柄地址, 较为稳定, 对象实例移动时, 只会影响句柄中的实例指针, 不会影响reference
直接指针: 访问速度快, 节省了一次指针地位的时间开销
三、OutOfMemoryError异常
1、Java堆溢出
发生原因
堆是存储对象实例的内存区域, 只要不断创建对象, 并且保证GC Roots到对象之间有可达路径来避免被垃圾回收机制清除, 总容量触及最大堆的容量限制后就会产生溢出异常
示例代码
/**

  • VM Args: -Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
    */
    public class HeadOOM {
    static class OOMObject {}
    public static void main(String[] args) {
    List list = new ArrayList<>();
    while (true) {
    list.add(new OOMObject());
    }
    }
    }

报错日志: java.lang.OutOfMemoryError

排查步骤
使用内存印象分析工具(jvisualvm.exe)对Dump出来的堆快照分析, 确认是内存溢出(Memory Overflow)还是内存泄漏(Memory Leak)
如果是内存泄漏, 查看GC Roots引用链, 是哪些导致GC没办法回收, 进而找到产生内存泄漏的代码的具体位置
如果是内存溢出, 则对比VM Args的配置和硬件设备配置, 可调整VM Args配置, 或升级硬件配置, 或代码减少内存使用

2、虚拟机栈和本地方法栈溢出
发生原因
如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出 StackOverflowError 异常
如果虚拟机的栈内存允许动态扩展, 当扩展栈容量无法申请到足够的内存时, 将抛出 OutOfMermoryError 异常
示例代码
1、StackOverflowError异常
方法一: 使用 -Xss 参数减少栈内存容量

/**

  • VM Args: -Xss128k
    */
    public class StackSOF {

    private int stackLength = 1;
    public void stackLeak() {
    stackLength++;
    stackLeak();
    }

    public static void main(String[] args) {
    StackSOF stackSOF = new StackSOF();
    try {
    stackSOF.stackLeak();
    } catch (Exception e) {
    System.out.println("stack length:" + stackSOF.stackLength);
    throw e;
    }
    }
    }
    报错日志: java.lang.StackOverflowError

方法二: 定义大量本地变量, 增大此方法帧中本地变量表的长度

public class StackSOF {

private static int stackLength = 1;
public static void  stackLeak() {
    long unused0, unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9,
            unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19,
            unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29,
            unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39,
            unused40, unused41, unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49,
            unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59,
            unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69,
            unused70, unused71, unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79,
            unused80, unused81, unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89,
            unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99;

    stackLength++;
    stackLeak();

    unused0 = unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 =
            unused10 = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 =
            unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 =
            unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 =
            unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 =
            unused50 = unused51 = unused52 = unused53 = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 =
            unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 = unused67 = unused68 = unused69 =
            unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79 =
            unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 =
            unused90 = unused91 = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = 0;
}

public static void main(String[] args) {
    try {
        stackLeak();
    } catch (Exception e) {
        System.out.println("stack length:" + stackLength);
        throw e;
    }
}

}
方法三: 创建线程导致内存溢出

/**

  • 高危操作, 会导致系统卡死
    */
    public class StackSOF {

    private void dontStop() {
    while (true) {}
    }

    public void stackLeakByThread() {
    while (true) {
    Thread thread = new Thread(() -> {
    dontStop();
    });
    thread.start();
    }
    }

    public static void main(String[] args) {
    StackSOF stackSOF = new StackSOF();
    stackSOF.stackLeakByThread();
    }
    }
    报错日志: java.lang.OutOfMemoryError

标签:Java,对象,虚拟机,线程,内存,方法,溢出
From: https://www.cnblogs.com/yangxiongbin/p/17154600.html

相关文章

  • java Vector
    publicstaticvoidmain(String[]args){Vectorv=newVector();//增v.addElement(111);v.addElement(222);v.addElem......
  • 【转】HBase最佳实践-内存规划
    [hbasefly]HBase最佳实践-内存规划HBase最佳实践-内存规划–有态度的HBase/Spark/BigData http://hbasefly.com/2016/06/18/hbase-practise-ram/线上HBase集群应该如何......
  • java——spring boot集成RabbitMQ——spring boot实现路由模式——消费者
    pom文件:<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instanc......
  • java——spring boot集成RabbitMQ——spring boot实现路由模式——生产者
    pom文件:<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instanc......
  • Java学习笔记----注解
    三个基本的注解@Override:限定重写父类方法,该注解只能用于方法@Deprecated:用于表示所修饰的元素(类,方法等)已过时@SuppressWarning:抑制编译器警告自定义注解public@i......
  • JAVA-
                         ......
  • 整形在内存中存储的一些典型题目
    1.第一题首先,-1是个整数,所以我们先写出它的二进制的原码、反码、补码:原码:10000000000000000000000000000001反码:11111111111111111111111111111110补码:1111111111111......
  • 32位系统支持多大内存?
    32位系统支持多大内存首先说明几个概念:32操作系统的地址总线为32,最大可寻址个地址;内存中一个存储单元为一个字节,即1byte或者8bit;1Gb=Mb=Kb=byte=bit;现在再整理......
  • JAVA学习笔记10-注解
    目录什么是注解内置注解元注解什么是注解Annotation是JDK5.0引入的新技术作用:不是程序本身,可以对程序做出解释就像注释一样;可以被其他程序(比如编译器)读取格式:注解是以......
  • JavaScript最简单的发布/订阅模式
    以下的是在ES6语法上构建的,非常适用于常见的应用场景//消息通知//发布/订阅模式classYLNotificationMessages{constructor(){//事件对象:存放事件的订阅......