JVM内存模型
参考
https://note.youdao.com/s/cgrAPqs
jvm包括哪些组件/系统?jvm的组成?
JVM内存区域?
https://www.processon.com/view/60b594bd6376894e10e7f9ca
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521140923473-21228050.png)
JVM的内存模式,Java8做了什么修改?
堆空间的划分中去除了永久代。把类型信息、字段、方法、常量保存在本地内存的元空间中,但字符串常量池、静态变量仍在堆。
每个内存区域保存哪些数据,不同的变量分别保存在哪儿 (静态、全局、局部)?1.8做出的调整及原因?
https://blog.csdn.net/m0_55913429/article/details/127867743
各种内存区域的设置?
堆内存的设置?(JVM32/64 操作系统)
OOM的触发?栈堆元空间,OOM时JVM进程一定会挂?不一定,线程异常处理。不影响JVM。但是、操作系统内存耗尽,操作系统会kill调jvm进程,所有用户线程都OOM挂掉,支撑守护线程,孜然会挂掉
常量池和运行时常量池
常量池存在字节码文件
运行时常量池存在方法区
堆外内存包括那些?
为啥要用堆外内存?
减少GC,减少复制次数从而提升复制速度
内存泄漏?
各种异常?解决方案?
长寿命周期的对象持有对生命周期的强引用,threadlcoal
内存溢出,栈statckoverflow可以动态扩展栈深度但是申请不到足够内存OOM
堆OOM,元空间OOM
解决方案
对象
深浅拷贝的区别?
对象的创建方式?new一个对象的过程?(类加载过程?内存分配方式?并发问题处理)、
对象结构?
大小计算?指针压缩,对齐填充
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521134319504-1006288210.png)
指针压缩
什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed--压缩、oop(ordinary object pointer)--对象指针
3.启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好
对象:对象头、实例数据、对其填充。
对象头:markword、pointclass 、(arrylength)
markword:锁标记、hashcode、分代年龄、地址指针
不同级别锁对象头markword不一样。
对象大小计算
计算:对象头固定大小+实例数据()+对其填充
int 4字节 32位
byte 1字节
long 8字节
char 2字节 16位
String? hash int 4+头12(压缩)+char[] 4 +对其填充 = 24字节
指针压缩
怎么配置?默认压缩不?压缩发生条件?
对象访问方式、优劣、默认?
指针:栈中存对象指针、对象指向类对象中包含类指针-------------高效(hotspot默认)
句柄:栈中局部变量指向句柄池中的句柄,句柄包括对象指针、类指针-----稳定
对象创建过程
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521133929068-1490426320.png)
对象内存分配
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521134520428-234352785.png)
栈上分配
为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
划分内存的方法:
- “指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
解决并发问题的方法:
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
堆上分配
对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。
在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?
- Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可,
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
哪些对象会进入老年代
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代
为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”
分代年龄的不同?
早期?CMS?G1?
对象的生命周期
- 创建阶段(Created):创建Java对象
- 为对象分配内存空间。
- 构造对象。
- 从超类到子类对static成员进程初始化,类的static成员的初始化在ClassLoader加载该类时进行。
- 超类成员变量按顺序初始化化,递归调用超类的构造函数。
- 子类成员变量按顺序初始化,一旦对象被创建,子类的构造函数就调用该对象并为一些变量赋值。完成以上这些后对象的状态就切换到了应用阶段InUse。
- 应用阶段(InUse):对象至少被一个强引用持有,除非显示的使用软引用、弱引用、虚引用。
- 不可见阶段(Invisible):不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。
- 不可达阶段(Unreachable):指对象不再被任何强引用持有,GC发现该对象已经不可达。
- 收集阶段(Collected):GC发现对象处于不可达阶段并且GC已经对该对象的内存空间重新分配做好准备,对象进程收集阶段。如果,该对象的finalize()函数被重写,则执行该函数。
- 终结阶段(Finalized):对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段。
- 对象内存空间重新分配阶段(Deallocaled):GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失。
GC
GC是什么?
有啥作用?如何发生?
GC是指jvm对内存的管理,在对象创建时,GC就开始监控对象的引用、大小等。当内存不足时,GC会通过引用计数法(主流jvm没有在用)或者可达性分析找到无用的对象、把它们当作垃圾、回收掉他们所占有的空间。
从而腾出更多的内存空间供新的对象使用。由于不同的对象被使用的情况不一样,有的很快变成垃圾,有的需要长期使用,针对不同的垃圾可以用不同的方式处理(标记-清楚,复制,标记-整理),所以对对象进行分代处理,年轻代、老年代。发生gc的频率和效率不同。年轻代频繁而快速、老年代少而慢。年轻代内存不足时触发minorGC,老年代内存不足时触发full gc。
分代模型:
age =15(默认&最大),cms=5
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521143500749-16897175.png)
G1
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521143646232-1986784116.png)
垃圾回收器
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521142942227-1511398870.png)
不同垃圾收集器回收过程及特点,追求目标,使用场景。
不同垃圾收集器使用的垃圾回收算法?如何分区?每个区域对象的特征?如何调优?
CMS浮动垃圾的处理?
对象内存回收策略:
引用计数法;
可达性算法:
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521135005062-1352392217.png)
常见引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用:
普通的变量引用
public static User user = new User();
软引用:
将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用:
将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用:
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
类加载机制
类加载过程?每个步骤具体做了什么?初始化的时机?类的继承,接口的继承,初始化执行顺序。
双亲委派机制?如何打破?
类加载过程
解析和初始化的顺序不一定
执行引擎
包括:解释器&即时编译器&垃圾回收器
JVM执行模式:
-Xint : 完全采用解释器模式执行程序
-Xcomp: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
-Xmixed: 采用解释器+即时编译器的混合模式共同执行程序。
JIT和解释器?
即时编译是解释执行和编译执行的平衡。什么是逃逸分析(方法、线程)?为什么要逃逸分析,JIT如何判断热点代码?怎么优化?栈上分配、标量替换、内置锁消除则怎么开启,前提是开启逃逸分析?此外还有方法内联等可以减少出站入栈,JIT(c1,c2,分层c1->c2(默认))分层编译?即时编译失败会去优化继续使用解释执行。
![](https://img2024.cnblogs.com/blog/1295707/202405/1295707-20240521135553347-1665126339.png)
热度衰减:
已执行过若干次数过后如果有一段时间没有执行,已执行次数会倍速减少,未来要达到即时编译条件需要更多执行次数。
JIT分类
分为C1(client compiler),C2(server compiler)
CompileThreshold
Client模式下,N默认值为1500;
Server模式下,N默认值为10000.
C1和C2编译器不同的优化策略
-
在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
* 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
* 区虚拟化:对唯一的实现类进行内联
* 同于消除:在运行期间把一些不会执行的代码折叠掉。
-
C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
* 标量替换:用标量值替换聚合对象的属性值
* 栈上分配:对于为逃逸的对象分配对象在栈而不是堆
* 同步消除:清除同步操作,通常指synchronized。
分层编译(Tiered Compilation)策略:
程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
逃逸分析
对象的活动范围时局部的,就是没有逃逸。
基于逃逸分析JVM开发了三种优化技术:栈上分配、标量替换、锁消除
为什么基于逃逸分析开发了这些优化技术?
-- 如果对象发生了逃逸,情况就会变得非常复杂(外部可能对该对象进行改变、重新赋值等),优化无法实施。所以这些优化措施都是在逃逸分析的基础(确定对象没有发生逃逸)上进行的。
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
-XX:+EliminateAllocations 开启标量替换
-XX:+EliminateLocks 开启同步消除
-XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。