01-JVM自动内存管理 Java内存区域与内存溢出异常

概述

C、C++开发人员自己负责内存管理;Java则由JVM去管理。

如果出现内存溢出与泄露,则java开发人员需要了解JVM去排错。

运行时数据区域

程序计数器

程序计数器可以看作当前线程执行的字节码的行号指示器。

  1. 字节码解释器通过改变计数器的值来选取下一条字节码指令,程序控制流(分支、循环、跳转等)、异常处理、线程恢复等都依赖这个计数器来完成。
  2. 为了线程切换后,可以恢复到正确的执行位置,各线程私有程序计数器,彼此互不影响,独立存储。

线程执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。

线程执行本地方法时,程序计数器为空。

程序计数器是唯一一个没有规定任何OOM情况的区域。

虚拟机栈

描述Java方法执行的线程内存模型,为虚拟机执行 Java 方法(也就是字节码)服务

  1. 每个方法执行时,JVM就会同步创建一个栈帧来存储局部变量表、操作数栈、动态连接和方法出口。一个方法的调用与执行完毕对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

  2. 对于JVM执行引擎来说,在在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法,定义这个方法的类叫做当前类

  3. 虚拟机栈异常
    StackOverFlowError:栈深度大于虚拟机栈所允许的最大深度。

    OutOfMemoryError:如果栈容量允许动态扩展,栈扩展无法申请到足够的内存。

  4. 虚拟机栈线程私有

栈帧的各部分数据结构与作用

  • 局部变量表
    一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。
    一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。
    虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。

    局部变量槽是可以复用的,会影响到垃圾收集。

    {
    	byte[] placeholder = new byte[1024 * 1024 * 64];
    }
    System.gc();
    

    由于局部变量槽内还存在引用placeholder指向数组对象,因此GC时并不会回收对象

    {
    	byte[] placeholder = new byte[1024 * 1024 * 64];
    }
    int i = 0;
    System.gc();
    

    由于程序执行到int i时离开了placeholder作用域,不会再访问placeholder了,因此变量i占用了placeholder所在的变量槽,GC时会回收数组对象。

  • 操作数栈
    一个后入先出栈,作为计算过程中变量临时的存储空间,主要用于保存计算过程的中间结果
    当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
    一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

  • 动态连接
    如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接而引用的过程称之为静态连接。 例如,super()方法。
    如果被调用的方法无法在编译期确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程中具备动态性,因此也被称之为动态连接。对应着接口回调,多态动态绑定等。
    在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池
    每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接

  • 方法返回
    正常退出方法:当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定
    异常退出方法:方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。

栈帧是线程本地的私有数据,不可能在一个栈帧中引用另外一个线程的栈帧

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量,在方法的Code属性中的max_stacks数据项中确定了该方法所的操作数栈的最大深度。

方法退出过程等同于把当前栈帧出栈,因此退出可以执行的操作有:

  • 恢复上层方法的局部变量表和操作数栈
  • 把返回值(如果有的话)压如调用者的操作数栈中
  • 调整PC计数器的值以指向方法调用指令后的下一条指令。

本地方法栈

为虚拟机使用到的 Native 方法服务。

线程调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

线程私有

也会出现OOM与SOF

用于存储对象实例与数组

  1. 几乎所有对象实例与数组都是在堆上分配。
  2. java堆是垃圾收集器管理的主要区域。
  3. 虚拟机启动时创建java堆
  4. java堆是被所有线程共享的一块内存区域
  5. 堆内存进一步划分为新生代、老年代、永久代,目的是为了更好的回收内存, 或者更快的分配内存。
  6. 主流的JVM按照堆可扩展实现的,通过-Xmx与-Xms设定

若堆无内存完成分配,且堆无法再扩展时,跑出OOM。

方法区

存储已加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等

  1. 方法区是被所有线程共享的一块内存区域
  2. Java虚拟机的规范中方法区是堆中的一个逻辑部分
  3. 方法区的内存回收主要是针对常量池以及类型的卸载,但是类型卸载在实际场景中的条件相当苛刻。
  4. 如果方法区无法满足新的内存分配需求时,将跑出OOM

运行时常量池

  • 方法区的一部分,存放常量池表
  • class文件有常量池表,用于存放编译器生成的各种字面量与符号引用

HotSpot使用永久代来实现方法区,并把GC的分代收集扩展至永久带。这样设计的好处就是能够省去专门为方法区编写内存管理的代码。但是在实际的场景中,这样的实现并不是一个好的方式,因为永久代有MAX上限,所以这样做会更容易遇到内存溢出问题。

Java1.7已经将永久代中的字符串常量池和静态变量移到堆中。

Java1.8使用在本地内存中实现的元空间替代永久代,主要存储运行时常量池、类信息等。

关于直接内存

  • 直接内存不属于java运行时数据区域;各内存区域总和大于物理内存限制,会导致动态扩展时出现OOM。
  • 本地IO可以直接操作直接内存(直接内存->系统调用->硬盘/网卡),而非直接内存需要二次拷贝(非直接内存->直接内存->系统调用->硬盘/网卡)。

HotSpot虚拟机在Java堆中的对象

对象的创建

  1. JVM遇到一条字节码new指令时,首先检查该指令的参数是否可以在常量池中定位到一个类的符号引用

  2. JVM检查该符号引用代表的类是否被加载过,如果没有则执行相应的类加载过程。

  3. 类加载检查后,JVM为新生对象分配内存
    分配内存大小在类加载完成后就可以确定。
    两种内存分配方式
    指针碰撞:java堆的内存是规整的,以一个作为分界点的指针,用过的内存放在一边,空闲的内存放在另一边。
    空闲列表:java堆的内存不规整,空闲与已使用的内存空间相互交错。需要使用一张列表,记录哪些内存块是可用的。在分配时根据列表找出足够大的内存空间划分给对象,并更新列表上的记录。

    内存分配方式由内存区块是否规整决定,而内存是否规整由垃圾收集器是否带有空间压缩整理能力决定。

    并发情况下分配内存可能出现线程A分配内存时,线程B又同时使用了原来的指针来分配内存。有如下两种解决方案:

    1. 对分配内存空间的动作进行同步处理,JVM采用CAS配上失败重试的方式保证更新操作的原子性。
    2. 内存分配的动作按照线程划分在不同空间进行,即每一个线程预先分配一小块内存,称为本地线程分配缓冲TLAB。只有线程的分配缓冲区用完了,才需要同步锁定。
  4. 内存分配完成后,JVM将分配的内存空间初始化为零值。
    初始化保证了对象的实例字段不用赋初值就可以使用,使程序可以访问到这些字段的数据类型所对应的零值。

    如果使用了TLAB,则初始化在TLAB分配时顺便进行。

  5. JVM对对象进行必要的设置
    在对象头中设置对象的类型指针、哈希码、GC分代年龄等。

JVM创建完对象后,从java程序上看,对象创建才刚刚,构造函数()还没有执行,所有的字段都默认为零值。执行完()方法后,真正可用的对象才算完全构造出来。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充。

对象头

  • 对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
  • 类型指针:对象指向它的类型元数据的指针,通过该指针确定对象是哪个类的实例。如果对象是数组,则对象头还需要有一块记录数组长度的数据。

实例数据

对象的各种类型的字段内容。

对齐填充

任何对象大小需要是8的整数倍。如果实例数据大小不是8的整数倍,则通过对齐填充补齐。

对象的访问定位

对象创建后,有两种访问定位的方式:

  1. 句柄访问
    堆中划分出一块句柄池,栈上的引用存储的是对象的句柄地址。句柄中包含了对象的实例数据和类型数据的地址信息。

  2. 直接指针访问
    栈上的引用存储的就是对象地址,对象的内存布局需要考虑如何放置访问类型数据的相关信息。

句柄访问的优势:引用中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,引用本身不会改变。

直接指针访问的优势:存储的是对象地址,如果只访问对象本身的话,可以节省一次指针定位的时间开销。对象访问十分频繁,可以节省可观的执行成本。

IDEA设置JVM配置

设置全局JVM配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h3M8XqgS-1626702516116)(C:\Users\15829\Desktop\JVM\IDEA设置JVM的全局设置.JPG)]

IDEA -> help -> Edit Custom VM options

  • Xms:堆内存最小值
  • Xmx:堆内存最大值
  • MaxMetasapceSize:最大元空间大小
  • +HeapDumpOnOutOfMemoryError:内存溢出异常时转储出当前堆内存快照,以便事后分析

设置局部JVM配置

OOM&SOF

java堆溢出

package heap;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args: -Xms20m -Xmx20m  -XX:+HeadDumpOnOutOfMemoryError
 * @author cqx
 */
public class HeapOOM {
    static class OOMObject{

    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while(true){
            list.add(new OOMObject());
        }
    }
}
D:\Java\jdk1.8.0_151\bin\java.exe ...
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9132.hprof ...
Heap dump file created [28349251 bytes in 0.111 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at heap.HeapOOM.main(HeapOOM.java:17)

Process finished with exit code 1

虚拟机栈溢出

package heap;
/**
 * -Xss128k
 * @auther cqx
 */

public class JavaStackSOF {
    private int stacklength = 1;
    public void stackLeak(){
        stacklength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaStackSOF oom = new JavaStackSOF();
        try{
            oom.stackLeak();
        }catch(Throwable e){
            System.out.println("stack length:"+oom.stacklength);
            throw e;
        }
    }

}
D:\Java\jdk1.8.0_151\bin\java.exe...
stack length:1591
Exception in thread "main" java.lang.StackOverflowError
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)
	at heap.JavaStackSOF.stackLeak(JavaStackSOF.java:7)

方法区和运行时常量池溢出

package heap;

import java.util.HashSet;
import java.util.Set;
/**
 * -Xms1m -Xmx1m
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        int i = 0;
        while(true){
            set.add(String.valueOf(i++).intern());
            System.out.println(set.size());
        }
    }
}

jdk1.7开始,字符串常量池移到堆内存,因此应该设置Xms和Xmx。

1.7之前应该使用-XX:PermSize=6M -XX:MaxPrimSize=6M。

jdk1.8开始,使用元空间存储永久代剩下的部分,-XX:MetaspaceSize和-XX:MaxMetaspaceSize限制大小

ew HashSet<>();
int i = 0;
while(true){
set.add(String.valueOf(i++).intern());
System.out.println(set.size());
}
}
}

 jdk1.7开始,字符串常量池移到堆内存,因此应该设置Xms和Xmx。
 1.7之前应该使用-XX:PermSize=6M -XX:MaxPrimSize=6M。
 jdk1.8开始,使用元空间存储永久代剩下的部分,-XX:MetaspaceSize和-XX:MaxMetaspaceSize限制大小

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令&#xff1a; 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体&#xff0c;就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛&#xff0c;这门课会介绍许多方法去利用离散点最大化地逼近这个函数&#xff0c;甚至它的导数、积分&#xff0c;甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在阿里巴巴的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目&#xff1a; 解题思路&#xff1a; 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 总代码&#xff1a; 题目&#xff1a; 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者&#xff08;Observer&#xff09;模式的定义&#xff1a;指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验&#xff0c;睡觉过程中突然意识清醒&#xff0c;身体却动弹不了。这时候感觉非常恐怖&#xff0c;希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况&#xff0c;一年有一百多次&#xff0c;那时候很害怕晚上到来&#xff0c;睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别&#xff08;move-only type&#xff0c;只移型别还有std::mutex等&#xff09;。 结合一下工厂模式&#xff0c;看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在&#xff0c;但代码中需要用到它&#xff0c;通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能&#xff0c;通常需要完成两步操作&#xff1a;一.是在Web页面中添加上传输入项&#xff1b;二是在Servlet中读取上传文件的数据&#xff0c;并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度&#xff08;&#xff1f;&#xff09; 不管两个高积木之间的的积木是怎样乱七八糟的高度&#xff0c;最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的&#xff0c;用的是MFC VS2019. 详见&#xff1a;http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936&#xff0c;然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一&#xff1a;区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N&#xff0c;表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家&#xff0c;在他的着作中曾经提到&#xff1a;「若有一只免子每个月生一只小免子&#xff0c;一个月后小免子也开始生产。起初只有一只免子&#xff0c;一个月后就有两只免子&#xff0c;二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源&#xff0c;一般分为两类&#xff1a; 系统内置资源&#xff1a;Android SDK中所提供的已经定义好的资源&#xff0c;用户可以直接拿来使用。 用户自定义资源&#xff1a;用户自己定义或引入的&#xff0c;只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法&#xff08;Algorithm&#xff09;是一系列解决问题的清晰指令&#xff0c;也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获得所要求的输出。 如果一个算法有缺陷&#xff0c;或不适合于某个问题&#xff0c;执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习人工智能&#xff1a;知识图谱实战前言一、实体建模工具Protegepython&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习 人工智能&#xff1a;知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…