java是强类型高级语言
JVM(Java Virtual Machine,Java虚拟机)是Java平台的核心组件,它是一个虚拟的计算机,能够执行Java字节码(bytecode)。
1、区域划分
JVM对Java内存的管理也是分区分块进行,方便管理。
这一部分称之为
运行时数据区域
这个区域也可以进行细分
区域a:线程私有,对别的线程不可见
栈——存储方法调用和局部变量
本地方法栈——用于本地方法的存储,本质上和虚拟机栈没有 区别(很多虚拟机都将二者一起管理)
本地方法(Native Method) 是指由非 Java 语言(如 C、C++ 等)实现,并通过 Java 虚拟机(JVM)调用的方法。它的作用是让 Java 代码能够与底层系统或硬件交互,弥补 Java 语言在性能或系统级操作上的局限性。
虚拟机栈——存储方法的局部变量,出口等一系列数据(栈帧)
程序计数器——指向线程正在执行的字节码行号(唯一不会发生OOM(OutOfMemoryError)的区域)
区域b:线程共享
方法区——类信息,常量,静态变量,JDK 8后由元空间(Metaspace)实现,使用本地内存
运行时常量池:方法区的一部分,存放编译期生成的各种字面量和符号引用
堆(最大)——为对象和数组分配内存的地方,CG主要管理的区域
2、对象的组成
创建对象和销毁对象的过程是什么
2.1 分配内存给对象的方式
都采用循环CAS策略进行并发情况下的差错避免
假如内存是规整的:指针碰撞,移动指针分配空间即可,但当某块内存被回收后需要对当前内存进行整理
假如内存是不规整的:建立空闲列表,需要挑选一个合适大小的空间分配,但会出现大量的空间碎片
TLAB:考虑到对象的创建是一个十分频繁的内容,指针争夺和CAS要大量进行,所以每个线程提前分配一块内存,以减少争夺指针的次数,这个叫本地线程分配缓存(Thread Local Allocation Buffer,TLAB),分配后初始化为零值。
2.2 对象的内存布局
对象头
MarkWord标记字——存储对象信息,是一个动态定义的数据结构。会有不同的标志位,不同标志位代表后续存储的内容不同
指向对象类型的指针——表明对象类型
如果对象是数组时,会额外存储数组长度
实例数据
private string name等的和对象相关信息,就是实例数据。可以存基本类型也可以存地址指针。
对齐填充
填充到要求大小
2.3对象的访问方式
我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。
使用句柄
reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息,栈——>句柄池——>实例池&方法区
直接指针
reference中存储的直接就是对象地址,栈——>堆——>方法区
- 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,对象位置发生移动时,只需要修改句柄地址,不需要修改reference
- 直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,但在大量访问的情况下这个好处可以被忽略
3.垃圾收集
3.1 哪些是垃圾?
判断生死
脑门刻字法(引用计数法):引用指向该对象——>计数+1
引用断开指向该对象——>计数-1,为0就回收,但单纯的引用计数就很难解决对象之间相互循环引用的问题。
平地长树法(可达性分析):现行虚拟机使用的主要方式。从CGRoot开始向下搜索,不可达的对象被判定为可回收对象。
GC Roots的对象包括以下几种:虚拟机栈中的对象、方法区静态属性引用的对象、方法区常量引用的对象、本地方法栈引用的对象、虚拟机内部对象、同步锁持有的对象,还有一些会被临时加入
引用类型
强引用:在代码中普遍存在的引用赋值
软引用:还有用但非必须可以作为缓存使用
弱引用:可以作为缓存,被下一次垃圾回收回收
虚引用
3.2 怎样回收?
分代收集理论
JVM会把性质类似的对象放在一起集中管理
弱分代假说
不需要分代,因为大部分对象死的都很快
强分代假说
需要分代,获得时间越长的对象越倾向于活下去
把Java堆划分为不同的区域,回收对象根据年龄划分:新生代、老年代。
新生代的回收频率要高于老年代。
跨代引用假说
相对于同代引用仅占极少数。新生代引用老年代。老年代引用新生代。这种情况下会在被老年代引用的新生代上做标记(记忆集),标记出该新生代被哪个老年代的区域引用,此时把老年代的那一内存小块放入CGRoot中进行扫描(CGRoot的特殊部分)
3.3 收集算法
标记-清除算法
算法简单,但缺点明显:空间碎片问题,stop the world问题这要求用户线程完全停止。
空间碎片化的解决方式:
标记-复制算法
划分为AB区域,左右复制删除清空,仅适用于收集效率高的场合(朝生夕死),并且只有一半的有效空间。(适用于新生代)
标记复制优化——划分为三个区域:Eden区(8)Survivor区(From/To)(1)
标记-整理算法
把存活的部分集中在前部,清除后部分的。没有空间碎片,但是需要移动对象
3.4 HotSpot算法实现细节
根节点枚举
我们通过OopMap来实现根节点枚举,让虚拟机在扫描时就直接得到根节点的信息,它的核心作用是让垃圾回收器快速准确地识别栈和寄存器中哪些位置包含指向对象的引用(即 "对象指针")。
遍历oopmap即可搜集到根节点
安全点
OopMap 与安全点紧密相关,安全点是程序执行过程中可以暂停并执行 GC 的位置,每个安全点都关联一个 OopMap。
安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的。(方法调用,循环跳转,异常跳转等指令序列复用)
如何在垃圾收集时,让所有线程都跑到安全点?
抢占式中断-先中断所有线程,不能中断时恢复。不好
主动式中断-借助标志
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
记忆集与卡表
记忆集:所有涉及部分区域收集行为的垃圾收集器中,用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
记忆集有三种精度:字长精度、对象精度、卡精度
卡精度——卡表实现,卡页内存有跨代指针,标志位置1,此时该卡表”变脏“,只需遍历变脏卡页就可得到区域。
写屏障
HotSpot通过写屏障维护卡表状态。这是对引用对象赋值的一个环形通知(细分为写前、写后屏障)
3.5并发的可达性分析
基于一个保持一次性快照——对象引用关系不可改变
三色标记:
黑:已经被垃圾收集器放问过,且所有引用已经被扫描
白:尚未被标记
灰:已经被垃圾收集器放问过,且有引用未被扫描
由于这个过程会和用户进程并发进行,此时会有浮动垃圾的产生和引用被误删的情况:当同时满足 1 赋值器插入了一条或多条从黑色对象到白色对象的新引用,2 赋值器删除了全部从灰色对象到该白色对象的直接和间接引用
解决方案:
增量更新:破坏1
原始快照:破坏2
3.6常见的收集器
Serial收集器:
新生代标记-复制,老年代标记-整理
ParNew收集器:
是Serial收集器并发版本。新生代标记-复制,老年代标记-整理
Parallel Scavenge收集器:
标记-复制法目标是达到一个可控制的吞吐量
Serial Old收集器:
标记-整理法。它作为CMS发生失败的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器:
标记-整理算法
CMS(Concurrent Mark Sweep)收集器
是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现,整个过程分为四个阶段
初始标记(stop the world)标记直接关联到的对象,速度很快
并发标记 遍历整个路径,但可以并发运行
重新标记(stop the world)
并发清除 也可以并发运行
其中耗时最长的并发标记和并发清除阶段
有三个明显的缺点:
- CMS对处理器资源非常敏感
- 由于CMS收集器无法处理“浮动垃圾”(产生于标记结束后的垃圾)
- CMS运行期间预留内存无法满足新对象分配需要(启动阈值为92%)
- 产生空间碎片
GI收集器(Garbage First)
是一种主要面向服务端应用的垃圾收集器。收集器面向局部收集的设计思路和基于Region的内存布局形式。
目标是:支持一个在长度为M毫秒的时间段内,花在垃圾收集上的时间不超过N毫秒。
基于Region帮助实现这个目标,把Java内存划分为多个大小相等的区域,采用MixedGC模式,使回收收益最大
内存碎片问题不严重,但内存负载较高
3.7低延迟垃圾收集器
衡量垃圾收集器的三项最重要的指标是:
内存占用(Footprint)
吞吐量(Throughput)
延迟(Latency)
3.8内存分配与回收策略
- 对象优先在Eden区
- 大对象直接进老年代
- 长期存活对象将进入老年代:对象年龄计数器(对象头)
- 动态年龄判定:动态调整进入老年代的年龄限制
- 空间分配担保:判断老年代空间是否满足新生代晋升(与历史平均值对比)