栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

深入理解Java虚拟机 读书笔记(二)Java内存区域与内存溢出异常

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

深入理解Java虚拟机 读书笔记(二)Java内存区域与内存溢出异常

深入理解Java虚拟机 读书笔记(二)Java内存区域与内存溢出异常
  • 一、运行时数据区域
    • 1. 程序计数器
    • 2. Java虚拟机栈
    • 3. 本地方法栈
      • 局部变量表
    • 4. Java堆
      • 分配缓冲区(TLAB)
    • 5. 方法区
      • 运行时常量池
    • 6. 直接内存
  • 二、对象探秘
    • 1. 对象的创建
      • 指针碰撞
      • 空闲列表
      • 线程安全
    • 2. 对象的内存布局
      • 对象头
      • 实例数据
      • 对齐填充
    • 3. 对象的访问定位
      • 直接指针访问
      • 句柄访问
      • 优缺点对比
  • 三、实战OutOfMemoryError
    • 1. 内存溢出和内存泄漏
    • 2. Java堆
    • 3. 虚拟机栈和本地方法栈
    • 4. 方法区和运行时常量池
    • 5. 本机直接内存溢出

一、运行时数据区域

Java虚拟机在执行时将管理的内存划分为不同的区域,有的区域随虚拟机的进程启动一直存在,有的则依赖用户线程的启动和结束建立和销毁,运行时数据区域如下图

1. 程序计数器

可以看作当前线程所执行字节码的行号指示器,字节码解释器工作时通过改变程序计数器的值选取下一条需要执行的字节码指令。每条线程都有独立的程序计数器。

  • 若线程执行Java方法,程序计数器虚拟机字节码指令地址
  • 若线程执行Native方法,程序计数器为空(Undefined)
2. Java虚拟机栈

Java虚拟机栈描述Java方法执行的线程内存模型,每个Java方法执行时,Java虚拟机会同步创建一个栈帧(Stack frame)在虚拟机栈

3. 本地方法栈

本地方法栈与Java虚拟机栈的功能相同,但Java虚拟机栈为Java方法服务,本地方法栈为本地(Native)方法服务。
在HotSpot虚拟机中,Java虚拟机栈和本地方法栈合二为一。

局部变量表

局部变量表存放在栈帧中,其中存放了编译期可知的各种Java基本数据类型、对象引用和returnAddress类型。
这些数据类型在局部变量表以局部变量槽表示,long和double占两个槽,其余占一个。

4. Java堆

Java堆是虚拟机管理的最大一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的是存放对象实例。

分配缓冲区(TLAB)

线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,但这并不改变堆存储的共性——只能是对象的实例。

5. 方法区

方法区用于被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,HotSpot虚拟机把收集器的分代设计扩展至了方法区。JDK7时,HotSpot将字符串常量池,静态变量等移动至Java堆中。

运行时常量池

Class文件中有一项信息是常量池表,用于存放编译期生成的字面量与符号引用,在类加载后存放到运行时常量池中。

6. 直接内存

直接内存并不是虚拟机运行时数据区域的一部分,但也受虚拟机总内存的限制。

总的来看,运行时数据区域格局如下:

二、对象探秘

HotSpot在Java堆中对象分配、布局和访问的全过程。

1. 对象的创建

对象的创建过程可以由以下五个步骤概括:

  1. 类加载检查
  2. 为新生对象分配内存
  3. 内存空间初始化零值
  4. 设置对象头
  5. 执行构造函数
指针碰撞

若Java堆内存规整,所有用过的内存放在一边,空闲内存在另一边,中间的指针为分界点,只需将内存往空闲方向划出一段即可。

空闲列表

虚拟机维护一个列表,记录可用内存块,分配时找到足够大的内存块给对象实例并更新列表实例。

线程安全

为防止多个对象冲突分配一块内存,可采用两种办法:

  1. 同步处理:采用CAS配失败重试保证更新操作原子性
  2. 使用TLAB:每个线程在Java堆预先分配一小块TLAB,在该线程直接分配即可
2. 对象的内存布局

HotSpot中的内存布局可以分为三个部分:对象头,实例数据和内存填充
在不开压缩指针的64位虚拟机的布局如下:

存储内容占用空间
MarkWord8bit
类型指针8bit
实例数据由类决定
对齐填充保证对象大小为8的倍数
对象头
  1. MarkWord:一个有着动态定义的数据结构,存储对象自身运行时的哈希码、GC分代年龄和锁信息等数据
  2. 类型指针:对象指向类型元数据的指针,JVM通过这个指针确定该对象是哪个类的实例。若对象是一个Java数组,则还必须存储数组的长度数据

若开启压缩指针,则64位虚拟机对象头一共占8bit

实例数据

存储父类子类定义的所有字段,存储顺序受虚拟机影响,默认分配顺序为

longs/doubles
ints
shorts/chars
bytes/booleans
oops(Ordinary Object Pointers)

满足这个前提下,父类的位置比子类更靠前一些。
若CompactFileds为true,子类中较窄的变量允许插入父类变量的空隙。

对齐填充

HotSpot自动内存管理系统要求对象起始地址必须是8byte的倍数,因此需要对齐填充将对象大小补全为8byte的整数倍

3. 对象的访问定位 直接指针访问


HotSpot中采用的便是直接指针访问,栈上的reference存储的是对象实例数据在堆中的地址,访问到对象后,又可以通过对象的对象类型数据指针访问对象类型数据。

句柄访问

Java堆中可能会划分出一块内存叫句柄池,reference存储的是对象的句柄地址,句柄中则存储着对象的实例数据地址和类型数据地址。

优缺点对比
直接访问句柄访问
优点一次访问,速度更快对象移动时不需要修改reference本身
缺点在对象移动时需要修改reference本身两次访问,速度较慢
三、实战OutOfMemoryError 1. 内存溢出和内存泄漏

内存溢出:申请内存超出系统可以提供的内存
内存泄漏:用完的内存没有被GC回收

2. Java堆

Java堆的大小可以设计成固定的或可扩展的,主流虚拟机的Java堆大小都是可扩展的。当Java堆中没有完成实例分配,堆也无法扩展时,将会抛出OutOfMemoryError异常。
我们限制Java堆大小后,不断创建对象会出现该异常。

public class DemoApplication {
    static class Object {};
    public static void main(String[] args) {
        List list = new ArrayList<>();
        while(true) {
            list.add(new Object());
        }
    }
}
 

测试结果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1800.hprof ...
Heap dump file created [32257710 bytes in 0.074 secs]
3. 虚拟机栈和本地方法栈

在HotSpot中,栈是不可以动态拓展的,在栈容量无法容纳新的栈帧时会抛出StackOverflowError异常。

public class DemoApplication {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String args[]) throws Throwable{
        DemoApplication oom = new DemoApplication();
        try {
           oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("栈长度: "+oom.stackLength);
        }
    }
}

测试结果如下:

栈长度: 1668
java.lang.StackOverflowError

若不断创建线程,也可能耗尽虚拟机内存导致OutOfMemoryError

4. 方法区和运行时常量池

我们经常默认运行时常量池是方法区的一部分,这里我们一起测试

public class DemoApplication {
    public static void main(String args[]) throws Throwable{
        Set set = new HashSet();
        int i = 0;
        while(true) {
            set.add(String.valueOf(i++).intern());
			// intern()函数用于将String字符串添加到常量池中
        }
    }
}

测试结果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10876.hprof ...
Heap dump file created [30111704 bytes in 0.103 secs]

在JDK11的环境中,运行时常量池已经被移动到了Java堆中,因此是Java heap space溢出。我们用如下代码验证我们的想法

public class DemoApplication {
    public static void main(String args[]) {
        String str = new StringBuilder("Hello").append("JVM").toString();
        System.out.println(str.intern()==str);
    }
}

输出结果如下,可以确定现在的运行时常量池已经在Java堆中。

true
5. 本机直接内存溢出
public class DemoApplication {
    private static final int _1MB = 1024*1024;
    public static void main(String args[]) throws Exception{
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while(true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

测试结果如下:

Exception in thread "main" java.lang.OutOfMemoryError
	at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)
	at jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)
	at com.example.demo.DemoApplication.main(DemoApplication.java:24)

可以看到,本地直接内存溢出时Heap Dump没有出现异常

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号