JVM 常规基础知识整理

JVM学习笔记——内容整理源于《深入理解Java虚拟机》

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

2.2 运行时数据区域

在这里插入图片描述

2.2.1 程序计数器(线程私有)

程序计数器是一个较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

​ 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,称这类内存区域为“线程私有”的内存。

2.2.2 Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

2.2.3 本地方法栈(线程私有)

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。

2.2.4 Java堆(线程共享)

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。

2.2.5 方法区(线程共享)

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

2.2.6 运行时常量池

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

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建

对象的创建过程:

​ 当java虚拟机遇到一条字节码new指令时:

  1. 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那必须先执行相应的类加载过程;

  2. 为新生对象分配内存;

  3. 将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前到TLAB分配时顺便进行;

  4. 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代信息等。

  5. 执行初始化方法(构造函数)

为对象分配空间的两种方式:

  1. 指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲方向挪动一端与对象大小相等的距离。

  2. 空闲列表:如果Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

解决对象创建过程中线程不安全的两种方案:

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:

  1. 对分配内存空间的动作进行同步处理——实际上虚拟机时采用CAS配上失败重试的方式保证更新操作的原子性。
  2. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓冲(TLAB,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只用本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。
2.3.2 对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头实例数据对齐填充

对象头部分包括两类信息:

  1. 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64比特,官方称它为"Mark Word" 。

    在这里插入图片描述

  2. 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

实例数据部分是对象真正存储的有效信息,即在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

2.3.3 对象的访问定位

对象访问方式是由虚拟机实现而定的,主流的访问方式主要有使用句柄直接指针两种。

  • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来做为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如下图所示。

    在这里插入图片描述

  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如下图所示。
    在这里插入图片描述

各自优势:

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销极少成多也是一项极为可观的执行成本。

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

​ Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息"Java.lang.OutOfMemoryError"会跟随进一步提示"Java heap space"。

​ 要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。

  • 首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。
  • 若是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
  • 若不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
2.4.2 虚拟机栈和本地方法栈溢出
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机的栈内存运行动态扩展,当扩展栈容量无法申请到足够的内存是,将抛出OutOfMemoryError异常。

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

2.4.4 本机直接内存溢出

​ 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

三、垃圾收集器与内存分配策略

3.1 概述

Java堆和方法区这两个区域有很显著的不确定:

​ 一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象,这

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热河路的IT男

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值