Java虚拟机学习记录-内存与垃圾收集器

本文详细介绍了Java虚拟机的自动内存管理机制,包括程序计数器、虚拟机栈、本地方法栈、Java堆和方法区等运行时数据区的用途和异常情况。讨论了对象的创建过程、内存布局以及访问定位。此外,还涵盖了垃圾收集的原理,如引用计数和可达性分析算法,以及各种垃圾收集器的特点和内存分配策略。文章最后提到了对象存活判断和方法区的回收,以及不同的垃圾收集算法,如标记-清除、复制、标记-整理和分代收集。
摘要由CSDN通过智能技术生成

本系列全部参考自 深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)

2.自动内存管理机制

2.2 运行时数据区

来源于javaguide.cn

程序计数器

线程私有
当前线程执行字节码的行号指示器,通过程序计数器的值取下一条需要执行的字节码指令。
如果线程执行的是Java方法,那么计数器记录的是执行的虚拟机字节码指令地址,如果执行Native方法,计数器值为空null。
该内存区域为唯一一个Java虚拟机规范中没有规定仍和OutOfMemoryError情况的区域。

Java虚拟机栈

线程私有
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型(int…boolean等),对象引用(只想对象起始地址的引用指针或者指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
两种异常情况:
1.线程请求的栈深度大于虚拟机允许深度,StackOverflowError
2.若虚拟机栈可以动态扩展,若扩展时无法申请到足够的内存,OutOfMemoryError

本地方法栈

为虚拟机使用到的Native方法服务
发挥作用和Java虚拟机栈类似
也会抛出StackOverflowError和OutOfMemoryError

Java堆

线程共享
在虚拟机启动时创建,用于存放对象实例,所有的对象实例都在这分配内存。
垃圾收集器管理的主要区域(GC堆)。
从内存回收角度看划分为:
新生代和老年代(细致点:Eden空间、From Survivor空间、To Survivor空间)
Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。

方法区

线程共享
存储已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。
一般很少垃圾收集,HotSpot虚拟机把GC分代收集扩展至方法区,方法区称为“永久代”。
这部分区域的内存回收目标主要是针对常量池的的回收和对类型的卸载,条件较为苛刻。
当方法区无法满足内存分配要求抛出OutOfMemory异常。

运行时常量池

方法区的一部分,存放编译期生成的各种字面量和符号引用。一般也会把翻译出来的直接引用也存在运行时常量池。

直接内存

不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是也会抛出OutOfMemoryError.
NIO(New Input/Output)类可以使用Native函数库直接分配堆外内存

2.3 HotSpot虚拟机对象揭秘

对象的创建

1.虚拟机遇到new指令,先去检查该指令参数能否在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,若没有,则先执行相应的类加载过程。
2.类加载检查通过后在堆中为新生对象分配内存,设置对象信息(实例,类的元数据信息、对象哈希码、对象的GC分代年龄等),存放在对象头中
3.执行<init>方法初始化

对象的内存布局

分为三个区域:
对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

对象头:1.用于存储对象自身的运行时数据(哈希码,GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等)2.类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象对应的类实例
实例数据:对象真正存储的有效信息,在程序代码中定义的各种类型的字段内容。
对齐填充:起占位符作用,HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。

对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象,reference类型在java虚拟机规范中只规定了一个指向对象的引用,没有定义具体实现。当前主流的访问方式是句柄和直接指针。
句柄: Java堆中划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了兑现实例数据与类型数据各自的具体地址信息。
在这里插入图片描述
优势:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不用改

**直接指针:**Java堆对象的布局中必须考虑如何放置类型数据的相关信息,reference中存储的直接就是对象地址。
在这里插入图片描述
优势:访问速度更快。

3.垃圾收集器与内存分配策略

哪些内存需要回收?
什么时候回收?
如何回收?

3.2 对象已死吗

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。
但是主流的Java虚拟机没有选用引用计数法来管理内存,主要原因时很难解决对象间相互循环引用的问题。

可达性分析算法

基本思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索走过的路被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,证明此对象时不可用的,就会被判定为可回收的对象。
在这里插入图片描述
在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象。

再谈引用

JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这4种引用强度依次逐渐减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围进行二次回收。
  • 弱引用用来描述非必须对象,只能生存到下一次垃圾收集发生之前
  • 虚引用完全不会影响对象生存时间,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡

要宣告一个对象的死亡,至少要经过两次标记过程。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,直接宣判对象死亡。
如果对象被判定有必要执行finalize()方法,那么会被放置在一个F-Queue队列中,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中重新与引用链上任何一个对象建立关联,就会在第二次标记时被移除出“即将回收”的集合,如果对象这时候还没有逃脱,那么基本上它就真的被回收了。
不推荐用finalize()方法!!!

回收方法区

Java规范中说过可以不要求虚拟机在方法区(永久代)中实现垃圾收集,因为效率非常低!
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量:加入一个字符串“abc”在常量池中,当前系统没有任何一个String对象引用常量池中的“abc”常量,也没有其他地方引用这个字面量,那么发生内存回收且有必要时,这个“abc”常量就会被清理出常量池。常量池中的其他类、方法、字段的符号引用也与此类似。
判定类是否无用:
1.该类所有的实例都已经被回收,Java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法。

垃圾收集算法

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:1.效率问题,标记和清除的效率都不高。
2.空间问题,标记清除后会产生大量不连续的内存碎片。

复制算法

将可用内存按照容量划分为大小相等的两块,每次只使用其中一块,当这一块内存使用完了,就将还存活着的对象复制到另外一块上面,再把已使用过的内存空间一次清理掉。
但是这样代价太大。
根据研究表明新生代中的对象98%是很快会被回收的,所以不需要按照1:1来划分内存空间。
现在是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时将Eden和这块Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉刚才用的Eden和Survivor空间。
HotSpot默认Eden和Survivor大小比例是10%。

标记-整理算法

同样标记出所有需要回收的对象,然后让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集算法。
根据对象存活周期不同将内存划分为几块,一般Java堆分为新生代和老年代。
新生代,每次GC会有大批对象死去,少量存活,采用复制算法。
老年代,对象存活率高、没有额外空间进行分配担保,必须使用“标记-清理”或者“标记-整理”算法来回收。

垃圾收集器

在这里插入图片描述

Serial收集器

单线程收集器,它在垃圾收集时,必须暂停其他所有的工作线程。
优点是简单有效。

ParNew收集器

Serial收集器的多线程版本
能够与老年代的CMS收集器配合工作。

Parallel Scavenge收集器

特点:目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值)

Serial Old收集器

Serial收集器的老年代版本,使用“标记-整理”算法

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法。
四个步骤:

  • 初始标记:仅仅标记GC Roots能直接关联到的对象
  • 并发标记:进行GC Roots Tracing的过程
  • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动那一部分对象的标记记录
  • 并发清除

缺点:
1.并发阶段,会因为占用一部分线程(CPU资源)导致应用程序变慢,总吞吐量降低。
2. 无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败导致另一次Full GC的产生。浮动垃圾是CMS在并发清理阶段用户线程还在运行,伴随程序运行产生的新的垃圾
3. 基于标记-清除的算法,会产生大量的空间碎片。

G1收集器

G1(Garbage-First)面访服务端应用的垃圾收集器。
特点:

  • 并行与并发:G1能利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间。
  • 分代收集:能采用不同的方式去处理新创建的对象和已经存活了一段时间的旧对象
  • 空间整合:整体上基于“标记-整理”算法实现,局部上基于“复制”算法实现,在G1运作期间不会产生内存空间碎片
  • 可预测的停顿:除了追求低停顿外还建立可预测的停顿时间模型,能够明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不超过N毫秒
    它将堆划分为多个大小相等的独立区域(Region),保留了新生代和老年代的概念,但二者不再物理隔离,都是一部分Region的集合。
    全区域GC时,优先回收价值最大的Region。
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

内存分配与回收策略

在这里插入图片描述

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有空间了,虚拟机发起一次Minor GC

大对象直接进入老年代

大对象指:需要大量连续存储空间的Java对象,比如长的字符串以及数组。

长期存活的对象进入老年代

虚拟机给每个对象定义一个对象年龄(Age)计数器,如果对象在Eden出生并且经过第一次Minor GC后仍然存活,并且能被Survivor容纳,就被移入Survivor空间中,对象年龄置为1,每熬过一次Minor GC,年龄就增加1,超过15(可配置,默认15)以后就会被晋升到老年代

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就直接进入老年代。

空间分配担保

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值