- 深入理解Java虚拟机
- 第二章 Java内存区域与内存溢出
- 2.1 运行时的数据区域
- 2.1.1 程序计数器
- 2.1.2 java虚拟机栈
- 2.1.3 本地方法栈
- 2.1.4 Java堆
- 2.1.5方法区
- 2.1.6运行时常量池
- 2.1.7直接内存
- 2.2HotSpot虚拟机对象探秘
- 2.2.1对象的创建
- 2.2.2对象的内存布局
- 2.2.3对象的访问定位
- 2.3OutOfMemoryError异常
- 2.3.1Java堆溢出
- 2.3.2虚拟机栈和本地方法栈溢出
- 2.3.3方法区和运行时常量池溢出
- 2.3.4本机直接内存溢出
- 第三章 垃圾收集器与内存分配策略
- 3.1概述
- 3.2对象已死
- 3.2.1引用计数算法
- 3.2.2可达性分析算法
- 3.2.3再谈引用
- 3.2.4生存还是死亡
- 3.2.5回收方法区
- 3.3垃圾收集算法
- 3.3.1分代收集理论
- 3.3.2标记-清除算法
- 3.3.3标记-复制算法
- 3.3.4标记-整理算法
- 第七章 虚拟机类加载机制
- 3.3.4标记-整理算法
- 第七章 虚拟机类加载机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ka5s98BR-1633929757445)(C:UserszgAppDataRoamingTyporatypora-user-images1624983564059.png)]
2.1 运行时的数据区域 2.1.1 程序计数器程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。通过改变计数器的值来选取下一条需要执行的字节码,它是控制流程的指示器,比如分支,循环,跳转等基础功能。
对于多线程,每条线程都有一个独立的程序计数器,它们之间互不影响,相互独立,称为”线程私有’'的内存。
线程执行的是Java方法,计数器记录的是正在执行的虚拟字节码指令的地址;如果执行的是本地(Native)方法,则计数器的值为空。
2.1.2 java虚拟机栈
线程私有,Java虚拟机栈的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:当一个方法被执行,虚拟机栈同步创建一个栈帧用于存储局部变量,操作数栈,动态链接,方法出口等信息。方法从被调用到执行结束,对应着一个栈帧的入栈到出栈的过程。
栈多数情况下指的是虚拟机栈中的局部变量表部分。该表存放了编译期可知的各种基本数据类型、对象引用(reference类型)和returnaddress类型。
局部变量表的存储空间以局部变量槽表示,64位的double和long占两个槽,其余占一个。表所需的内存空间在编译期间完成分配,进入的方法所需要分配多少的局部变量空间是确定的,运行期间局部变量表的大小不会改变。
栈深度超出,StackOverflowError异常,无法动态扩展,OutOfMemoryError异常。
2.1.3 本地方法栈作用与虚拟机栈类似,本地方法栈为虚拟机使用到的本地(Native)服务。
本地方法栈在栈深度溢出或栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常
2.1.4 Java堆
Java堆是Java虚拟机管理的内存最大的一块,Java堆被所有线程共享的,在虚拟机启动时创建。该内存区域的唯一目的:存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的内存区域,一些资料也称其为“GC堆”。“新生代”,“老年代”,“Eden空间”,“From Surviror空间” ,”To Survivor空间“,这些区域划分仅仅是一部分垃圾收集器的共同特性或者设计风格。
从内存分配角度,Java堆可以划分出多个线程私有的分配缓冲区(TLAB)。将Java堆细分的目的只是为了更好地回收内存,或更快地分配内存。Java堆中存储内容的共性,存储的只能是对象的实例。
Java堆可以处于物理上不连续的内存空间,但逻辑上应该被视为连续的。对于大对象(如数组)出于实现简单,存储高效的考虑,很可能会要求连续的内存空间。
Java堆可以被实现成固定大小的,也可以是扩展的。无法在扩展时抛出OutOfMemoryError异常。
2.1.5方法区
线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区,”永久代“,内存溢出问题。
不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。回收目标主要时针对常量池的回收和对类型的卸载,但效果一般很难让人满意。
无法满足新的内存分配,抛出OutOfMemoryError异常。
2.1.6运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息,还有一项是常量池表,用于存放编译期生成的各种字面量和符号引用,这部分在类加载后存放于运行时常量池中。
具备动态性,常量并非一定只有编译期才能产生,运行期间也可将新的常量放入池中,如String类的intern()方法。
无法满足新的内存分配,抛出OutOfMemoryError异常。
2.1.7直接内存
直接内存并非虚拟机运行时数据区的一部分。
在JDK1.4中新加入了NIO(new Input/Output)类,引入一种基于通道(Channel)和缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。在一些场景提高性能,避免在Java堆和Native堆来回复制数据。
2.2HotSpot虚拟机对象探秘 2.2.1对象的创建
1、虚拟机遇到new字节码指令时,先检查指令的参数能否在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已被加载,解析和初始化过。若没有,则先执行相应的类加载过程。
2、内存分配,在类加载检查通过后,在这之后对象所需内存大小确定。分配方式主要如下:
| 分配方式 | “指针碰撞” | “空闲列表” |
|---|---|---|
| 概念 | 以一个指针为分界点,使用过的的内存和空闲的内存各在一边,指针向空闲区移动对象所需内存大小的距离。 | 使用过的和空闲的内存交错在一起,需要一个列表记录哪些内存块可用,分配时从表中找一块足够大的给对象实例,并更新表。 |
| 收集器 | Serial,ParNew带压缩整理过程 | CMS基于清除算法 |
在并发情况下考虑创建线程是否线程安全。两种方案:
- 对分配内存空间的动作进行同步处理:采用CAS配上失败重试的方式保证更新操作的原子性;
- 每个线程在Java堆中预先分配一下块内存,称为本地线程分配缓存(TLAB),线程先在自己缓存分配,如不足则需要同步锁定。
3、对分配到的内存空间(不包括对象头)初始化为零值。若使用TLAB,提前至分配TLAB时进行。
4、对对象进行设置。对象头的设置(对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息)
5、以上步骤,虚拟机认为对象已完成,Java程序——构造函数,Class文件中的()方法还没执行,按程序员的意愿对对象进行初始化之后,一个真正可用的对象被完全构造出来。
2.2.2对象的内存布局
分三个部分:对象头,实例数据,对齐填充
-
对象头:1、用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳;2、类型指针,对象指向它的类型元数据的指针,以此确定对象为哪个类的实例。并非所有对象都保留类型指针,如果对象是数组,还有给出数组长度,以此推断数组大小
-
实例数据:对象真正存储的有效信息。程序代码的各种类型的字段内容,存储顺序由虚拟机分配策略参数和Java源码中定义的顺序影响。默认分配顺序:longs/doubles、ints、short/char、bytes/booleans、OOPs
-
对齐填充,非必然存在,仅起占位符的作用。任何对象的大小都必须是8字节的整数倍
2.2.3对象的访问定位
Java程序通过栈上的reference数据(引用)来操作堆上的具体对象。两中访问方式如下:
| 访问方式 | 使用句柄访问 | 使用指针直接访问 |
|---|---|---|
| 概念 | Java堆可能会划出一块内存来作为句柄池,reference存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息 | Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址 |
| 优点 | 在对象移动(垃圾收集时移动普遍)时只会改变句柄中的实际数据指针,而reference本身不需要被修改 | 访问速度快,节省一次指针定位的时间开销 |
2.3OutOfMemoryError异常 2.3.1Java堆溢出
GC Roots: 就是 一组必须活跃的引用 , GC会收集那些不是GC roots且没有被GC roots引用的对象
报错:java.lang.OutOfMemoryError: Java heap sapce
处理方法:首先通过内存映射分析工具对Dump出来的堆转储快照进行分析。先确认内存中导致OOM的对象是否是必要的。即:
| 内存泄漏 | 内存溢出 | |
|---|---|---|
| 处理方法 | 通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径,与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息和它到GC Roots的引用链的信息,可以较为准确的定位对象创建的位置,进而找出内存泄漏的代码具体位置。 | 内存的对象必须是活的,检查虚拟机的堆参数(-Xmx与-Xms)设置,看是否可以上调。在观察代码是否有些对象生命周期过长,持有状态时间过长,存储结构设计不合理。 |
2.3.2虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机栈内存允许动态扩展,当扩展容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
- 单线程:无论是栈帧太大或者虚拟机栈容量太小,新的栈帧内存无法分配,抛出StackOverflowError异常。允许动态扩展的情况下抛出OutOfMemoryError异常
- 多线程:内存溢出取决于操作系统本身的内存使用状态,每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
2.3.3方法区和运行时常量池溢出
当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。
| 版本 | 字符串常量池的位置 |
|---|---|
| JDK6 | 永久代(方法区) |
| KDK7 | 在 Java 堆(Heap)中开辟了一块区域存放运行时常量池 |
| JDK8 | 元空间(堆区) |
2.3.4本机直接内存溢出
第三章 垃圾收集器与内存分配策略 3.1概述
垃圾收集器(Garbage Collection,简称GC)
需要完成的三件事:1、哪些内存需要回收,2、什么时候回收,3、如何回收
程序计数器、虚拟机栈,本地方法栈3个区域的随线程而生,随线程而灭。栈帧随方法的进出,不断进行入栈和出栈,栈帧的内存大小在类结构确定的时候已知,所以这几个区域的内存分配和回收都具备确定性。当方法或进程结束,内存自然就回收。
Java堆和方法区的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间才知道创建了哪些对象,创建了多少对象,这部分内存的分配和回收是动态的。
3.2对象已死
对堆进行回收之前要确定哪些对象已经“死去”,即不可能在被任何途径使用的对象。
3.2.1引用计数算法理论:在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器的值就加一,当引用失效时,计数器的值就减一,任何时刻计数器为零的对象就是不可能再被使用的。需要占用一些额外的内存空间来进行计数。
优点:原理简单,判定高效;缺点:需要配合大量额外的处理才能保证正确地工作,如对象之间相互循环引用
instance:对象.instance->获取一个引用
jvm没有使用这种算法
3.2.2可达性分析算法
思路:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,证明此对象不可能在被使用。如下图:
| 可作为GC Roots的对象 | 比如 |
|---|---|
| 虚拟机栈中引用的对象 | 各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量 |
| 方法区中静态属性引用的对象 | Java类的引用类型静态变量 |
| 方法区中常量引用的对象 | 字符串常量池里的引用 |
| 本地方法栈JNI(Native方法)引用的对象 | |
| Java虚拟机内部的引用 | 基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器 |
| 所有被同步锁(synchronized)持有的对象 | |
| 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存 |
此外根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
3.2.3再谈引用
传统定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据代表某块内存,某个对象的引用。缺陷,对于一些可有可无的引用该怎么办,如内存足,保留下来,否则需要抛弃
JDK1.2之后根据引用强度对其分类。
| 引用 | 定义 | 回收策略 |
|---|---|---|
| 强引用 | 最传统的“引用”的定义,是指程序代码中普遍存在的引用赋值 | 无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会收掉被引用的对象 |
| 软引用 | 描述一些还有用,但非必须的对象 | 只要别软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象进回收范围之中进行二次回收,如果这次还没有足够的内存,才会抛出内存溢出异常 |
| 弱引用 | 描述那行非必须对象,但是它的强度比软引用更弱一些 | 被弱引用关联的对象只能生存到下一次垃圾收集器发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象 |
| 虚引用 | 最弱的一种引用关系 | 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。关联对象的唯一目的,对象被收集的时候收到一个系统通知 |
3.2.4生存还是死亡
一个对象要真正死亡,至少要经历两次标记过程:第一次,可达性分析没有GC Roots相连接的引用链,标记;
随后,第二次,对象是否有必要执行finalize()方法。
如果需要执行finalize()方法,将对象至于F-Queue队列中,然后虚拟机会自动建立、低调度优先级的Finalizer线程去执行它们的finalize()方法。“执行”仅保证触发执行,不保证运行结束,为了防止某个对象的finalize()方法执行缓慢造成的后果。收集器对F-Queue队列中对象进行第二次标记,如何自救?重新与引用链上的任何一个对象建立关联即可。机会只有一次,finalize()方法只调用一次。尽量避免使用这个,运行代价高昂,不确定性大,调用顺序不保证
3.2.5回收方法区
方法区回收“性价比”低。回收的内容:废弃的常量和不在使用的类型。
常量废弃:没有对象引用这个常量,虚拟机也没有其他地方引用这个常量
不在使用的类型:1、该类的所有实例已经被回收2、加载该类的类加载器已经被回收3、该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。满足这三个仅被允许回收,有参数控制
Java虚拟机在一定的场景需要具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
3.3垃圾收集算法 3.3.1分代收集理论
弱分代假说:绝大多数对象都是朝生夕灭的。
强分代假说:熬过多次垃圾收集过程的对象就越难以消亡。
划分新生代,老年代。对象不是孤立的,对象之间存在跨代引用。
跨代引用假说。:跨代引用相对于同代引用来说仅占极少数。
在新生代上建立一个全局的数据结构:记忆集。把老年代划分若干小块,引用的小块内存里的对象才会被加入到GC Roots进行扫描
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集
- 新生代收集:(Minor GC/Young GC):指目标只是新生代的垃圾收集
- 老年代收集:(Major GC/Old GC):指目标只是老年代的垃圾收集
- 混合收集:(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
- 整堆收集:(Full GC):收集整个Java堆和方法区的垃圾收集。
3.3.2标记-清除算法
最早,最基础的垃圾收集算法。
分为"标记,清除"两个阶段:可以标记需要回收的对象,标记完成后,统一回收所有被标记的对象;也可以标记存活的对象,统一回收未被标记的对象。
缺点:
- 执行效率不稳定,如果Java堆中包含大量对象,而且大部分是需要回收,则需要进行大量标记和清除的动作。执行效率随对象数量增长而降低。
- 内存空间碎片化。标记,清除后产生大量不连续的内存碎片。如果分配较大对象没有找到足够的空间,则需要提前触发垃圾收集动作。
3.3.3标记-复制算法
解决标记-清除算法效率低问题。
“半区复制”的垃圾收集算法,将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后在把使用过内存空间一次清理掉。如果存活的对象多,则产生大量内存间复制开销,整个半区回收,不需考虑内存碎片化的问题。缺点内存缩小为原来的一半,空间浪费过大。
由于新生代有98%熬不过第一轮收集,提出:“Appel式回收”。将新生代分为一块较大的Eden和两块较小的Surivor空间,每次分配内存只使用Eden和其中一块Surivor。发生垃圾收集后,将这两块存活的对象复制到另一块Surivor,直接然后清理这两块。Eden:Surivor=8:1。存活对象超过10%,逃生门,需要依赖其他区域(实际上大多数就是老年代)进行分配担保。
3.3.4标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行处理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以为的内存。
区别:移动式,非移动式
移动的话内存回收时复杂(对对象索引进行更新,并且全程需要暂停用户应用程序的使用),不移动内存分配时复杂(内存空间碎片化)。移动:吞吐量会好一些。
和稀泥。使用 标记-清理,直到影响分配使用 标记-整理。
第七章 虚拟机类加载机制
直接然后清理这两块。Eden:Surivor=8:1。存活对象超过10%,逃生门,需要依赖其他区域(实际上大多数就是老年代)进行分配担保。
3.3.4标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行处理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以为的内存。
区别:移动式,非移动式
移动的话内存回收时复杂(对对象索引进行更新,并且全程需要暂停用户应用程序的使用),不移动内存分配时复杂(内存空间碎片化)。移动:吞吐量会好一些。
和稀泥。使用 标记-清理,直到影响分配使用 标记-整理。
第七章 虚拟机类加载机制



