JVM复习笔记参考Java虚拟机,优秀博客,以及经典面试题对面试题进行了按章归纳总结,采取尽量好记忆的方式进行总结,同时使用⭐️代表它的重要程度
文章目录
1.Java内存区域与内存溢出异常
1.1 介绍JVM的内存区域(运行时数据区)⭐️⭐️⭐️⭐️⭐️❤️❤️
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M0ivVTmc-1631847170133)(/Users/yazhouheilong/Library/Application Support/typora-user-images/截屏2021-04-22 21.50.17.png)]
数据区 | 存放数据已经作用 | 可能出现的错误😭 | 线程私有 |
---|---|---|---|
程序计数器 | 执⾏的字节码的⾏号指示器,实现循环选择 | 无 | 是 |
java虚拟机栈 | 存放局部变量和引用,为java方法服务 | OOM stack溢出 | 是 |
本地方法栈 | 和虚拟机栈类似,为native方法服务, | OOM stack溢出 | 是 |
堆 | 存放对象,最大的区域,垃圾回收的主要区域 (新生代老年代) | OOM | 共享 |
方法区(非堆) | 类信息、常 量、静态变量 | OOM | 共享 |
常量池 | 存放字面量和引用()(1.7后从方法区转移到堆中) | OOM |
思路:每一个说一遍,然后,总结说可能出现的错误和线程是否私有
1.2 简述永久代和元空间⭐️⭐️😭
-
永久代:是方法区的一种在hotspot的具体实现。方法区类似于接口,永久代就是方法区的实现类
-
元空间:Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
1.2.1 为什么要用元空间代替永久代
- 字符串:字符串在永久代中容易出现内存溢出,所以吧字符串移到了堆内部
- 类信息:元空间⾥⾯存放的是类的信息,难以确定大小,太大导致老年代溢出,太小导致永久代溢出,使用MAXPermSize相当于确定了永久代的上限如果使用元空间,那么这样加载多少类 就系统的实际可⽤空间来控制,这样能加载的类就更多了
- 合并:JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东⻄, 合并 之后就没有必要额外的设置这么⼀个永久代的地⽅了。
2.垃圾收集器与内存分配策略
2.1 如何判断对象是否死亡⭐️⭐️⭐️
-
引用记数法
算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计 数器值就加1;当引用失效时,计数器值就减1任何时刻计数器为0的对象就是不可能再被使用的。
问题: 难解决对象之间相互循环引用的问题。(例如只有两个对象相互引用)😭😭 😭
-
可达性分析
算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用 的
-
真正死亡: 宣告一个对象真正死亡要标记两次😭😭😭
2.1.1强 软 弱 虚 引用⭐️😭
引用强度 | 垃圾回收 | |
---|---|---|
强引用 | 最强(Object object = new Object()) | 永远不会回收 |
软引用 | 有用非必要 | 内存够则不会回收,内存溢出,会进行二次回收 |
弱引用 | 相当于软引用 | 只能活到下一次垃圾回收前 |
虚引用 | 最弱的引用相当于没有 | 任何时候都可能,垃圾回收前有通知 |
2.1.2 GCRoots是什么,包含哪些⭐️⭐️⭐️😭
虚拟机栈中引用的对象。方法区中,常量和静态变量引用的对象。本地方法栈中Native方法引用的对象。(都是对象,三个部分的对象)
GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。
2.2 垃圾回收算法⭐️⭐️⭐️⭐️⭐️❤️❤️
- 标记-清除算法
算法:首先标记出所有需要回收的对象(可达性分析)😭,在标记完成后统一回收所有被标记的对象
问题 :**效率问题 **标记和清除两个过程的效率都不高😭 空间问题:标记清除之后会产生大量不连续的内存碎片
- 复制算法
算法:将内存按容量划分为大小相等 的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收
优点:不用考虑内存碎片
缺点:空间缩小一半,如果存活对象较多,那么效率会比较低
改进:将内存分为一块较大的Eden空间和两块较小的 Survivor空间(记忆名词),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地 复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和 Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内 存会被“浪费”。
分配担保:当有超过10%对象存活,没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代 😭
- 标记整理算法
算法:前期过程与标记清楚算法一样,但是后续的过程是将存活的对象都向一端移动
优点:如果存活对象较多,那么避免了复制算法的效率低的问题😭
- 分代收集算法😭
新生代:只有少量对象存活 -》使用复制算法只需要付出少量复制成本
老年代:大量对象存活 且没有额外的担保空间-》使用标记清除或者标记整理算法
2.3 垃圾收集器😭
2.3.1CMS⭐️⭐️⭐️😭
采用的是标记清除算法,目的是最短回收停顿😭 老年代收集器⭐️
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SqEJ1WPd-1631847170136)(/Users/yazhouheilong/Library/Application Support/typora-user-images/截屏2021-04-23 16.16.38.png)]
- 初始标记 :暂停所有其它线程,记录下与GC root相连的对象
- 并发标记:同时开启GC线程和用户线程,继续标记
- 重新标记:修正并发标记由于用户线程开启导致标记产生变化的情况
- 并发清除: 开启用户线程,同时进行清理
优点:并发清除,速度快
缺点:1. 并发清除时候产生的垃圾只能等到下次清除😭
2. 吞吐量降低:在并发标记和并发清除时,和用户线程同步开启,导致吞吐量降低
2.3.1.1 CMS几个步骤,有几次停顿
2.3.2G1⭐️😭
采用的是标记整理算法,既可以用在新生代又可以用在老年代
将堆内存分为大小相等的 region,维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的 region😭😭,采用每一个 region 记录引用的关系,避免了可达性分析的全局扫描😭😭
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qLacEfFY-1631847170137)(/Users/yazhouheilong/Library/Application Support/typora-user-images/截屏2021-04-23 16.54.40.png)]
-
初始标记 :暂停所有其它线程,记录与GC
-
并发标记 :开启用户线程,继续标记
-
最终标记 :修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,😭
-
筛选回收:根据Remembered Set 优先列表,对回收价值和成本进行排序😭
特点 :并发性强,分代收集,标记整理进行了空间整合,可以预测停顿时间
2.3.3 G1和 CMS 区别⭐️⭐️
垃圾收集器 | CMS | G1 |
---|---|---|
用在哪里 | 老年代 | 老年代和新生代 |
什么算法 | 标记清除 | 标记整理 |
什么特点😭 | 最短回收停顿 | 可以预测停顿时间 |
最后回收 | 并发 | stop the world |
2.3.4 JDK8用的什么垃圾收集器😭❤️
Parallel Scavenge + Parallel Old
2.3.5 常见的垃圾收集器有哪些?⭐️⭐️
垃圾收集器 | 详细内容 | 新生代/老年代 |
---|---|---|
serial(JDK8) | 单线程收集器 | 新生代 |
serial old | 单线程收集器 | 老年代 |
parNew😭😭 | serial多线程版本 | 新生代 |
parallel Scavenge😭 | 多线程收集,侧重吞吐量和cpu资源配合 | 新生代 |
parallel Old | 多线程收集,侧重吞吐量和cpu资源配合 | 老年代 |
2.4 内存分配和回收策略⭐️⭐️
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2.4.1 Minor GC 和 Major GC 和 full GC 有什么不同⭐️
Minor GC:指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC 的速度一般会比Minor GC慢10倍以上。
2.4.2 什么对象会进入老年代⭐️⭐️😭😭
- 大对象直接进入老年代(长的字符串和长的数组)
- 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生 并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为 1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)即可进入老年代
- 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的 一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。😭😭
2.4.3 空间分配担保😭
-
安全的Minor GC :在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个 条件成立,那么Minor GC可以确保是安全的
-
冒险的Minor GC:如果允许担保失败,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试着进行一次Minor GC,如果小于或者设置不允许冒险那么进行重 GC😭
2.4.4 minor GC 和 full GC 的触发条件?⭐️😭😭😭
minor GC : eden 区满了
full GC :😭😭
1. 老年代空间满了:年轻代存活的对象超过了老年代,或者大对象直接进入老年代:老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象
2. 方法区满了;类信息、常 量、静态变量满了😭
3. 分配担保失败导致full gc
4.调用System.gc():只是建议虚拟机执行Full GC。但是虚拟机不一定真正去执行。😭
2.4.5 内存分配策略😭
-
对象优先在 Eden 分配
-
大对象直接进入老年代
-
长期存活的对象将进入老年代
-
动态对象年龄判定
-
空间分配担保😭
3.虚拟机类加载机制⭐️⭐️
3.1 java类加载机制是怎么样的⭐️⭐️⭐️⭐️⭐️❤️😭😭
-
加载:😭
-
查找加载类的二进制数据
-
方法区存放类信息
-
堆内存放类的实例
-
-
验证:确认加载的类符合虚拟机的要求,没有安全问题
-
准备: 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被final修饰的static变量(常量),会直接赋值注意是比如static int a = 10,此时只会赋值为0;
-
解析: 将符号引用转换为指针引用(例如 a = 10,a代表符号,转换a变为地址)
-
初始化:对类进行初始化,给静态变量赋值正确值(例如静态代码块,和赋值初始值)
3.2 简述JVM的类加载器以及双亲委派模型⭐️⭐️⭐️❤️
3.2.1 常见类加载器3️⃣
类加载器名 | 加载内容 |
---|---|
启动类😭 | <JAVA_HOME>\lib目录 java.* |
扩展类 | <JAVA_HOME>\lib\ext |
应用程序类 | 自己定义的类的路径下 |
3.2.2 双亲委派模型的工作过程3️⃣❤️❤️😭
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先看是否加载过这个类(从缓存中去读),首先判断是否加载过,把这个请求委派给父类加载器,父类也没有加载过那么,委托给顶层根加载器加载。如果顶层加载器无法加载,触发notfound这个异常,那么给下一级进行加载,一直到应用加载器
3.2.3 如果自定义一个java.lang.String会被加载嘛?❤️
答案:不会被加载,因为会一直向上传递,传递到根加载器,发现已经加载过,于是不再加载
3.2.4 如何自定义一个类加载器,如何打破双亲委派模型?⭐️
自定义类加载器:继承classloader,重写findclass方法。(无法被父类加载最终会被这个类加载)
打破双亲委派模型:继承classloadeer,重写loadclass方法。
3.2.5 双亲委派的优势❤️
-
防止核心类篡改:使得类的加载出现优先级,防止了核心API被篡改,提升了安全,所以越基础的类就会越上层进行加载,反而一般自己的写的类,就会在应用程序加载器(Application)直接加载。😭
-
防止重复加载:双亲委派机制使得类加载出现层级,父类加载器加载过的类,子类加载器不会重复加载,可以防止类重复加载
3.2.6 classnotfoundExceptiom 异常触发 ❤️
当父类找不到这个类,无法加载的时候,会出现这个异常
4.jvm参数配置和调优❤️
4.1 参数配置及简单调优😭
当JVM运行稳定之后,触发了FullGC我们一般会拿到如下信息:以上gc日志中,在发生fullGC之时,整个应用的堆占用以及GC时间。为了更加精确需多次收集,计算平均值。或者是采用耗时最长的一次FullGC来进行估算。上图中,老年代空间占用在93168kb(约93MB),以此定为老年代空间的活跃数据。则其他堆空间的分配,基于以下规则来进行。
- java heap:-Xms(初始值)和-Xmx(最大值),建议扩大至3-4倍FullGC后的老年代空间占用(设置相同的值减少内存自动扩容和收缩带来的性能损失)。
- 新生代:-Xmn,建议扩大至1-1.5倍FullGC之后的老年代。NewSize和MaxNewSize设置为一样
- 老年代:2-3倍FullGC后的老年代空间占用。
- 永久代:-XX:PermSize和-XX:MaxPermSize,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。
- -XX:SurvivorRatio=1:Eden区与一个Survivor区比值为1:1
- -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。
4.2 简单的jvm命令😭
- 打印日志:‐XX:+PrintGCDetails