备战大半年秋招资料分享-JVM篇

备战秋招大半年,目前已经拿到offer上岸,将半年来的笔记分享给大家。更多 笔记涵盖  MYSQL、Elasticsearch、Kafka、设计模式、JVM、Java语言基础、集合原理、并发技术 。
需要的同学可以加我vx:uukiinternet 私发给你们哦。

引用类型强引用(new Object()类型,不会被回收),软引用(new SoftReference<Object>(obj):系统将要发生内存溢出的异常前,会回收),弱引用(new WeakReference:只能存活到下一次垃圾回收发生之前),虚引用(new PhantomReference:在对象回收时收到系统通知)

判断对象存活
1、引用计数法,引用该对象则计数器加一、引用失效则减一、引用计数为0时候,表示对象可以回收。但是该方法不能解决循环引用的问题,因此大多数虚拟机实现并不按照此方法。  
2、可达性分析:从一系列GC Root对象出发(包括虚拟机栈中引用的对象、方法区中静态类属性引用的对象、方法区中常量引用的对象、JNI引用的对象),向下搜索,所走过的路径称为“引用链”。若一个对象是GC ROOT不可达的,那它们就会被判定为可回收对象。注意:一般地,对象没有实现finalize()方法,被标记成可回收对象后会被回收。如果实现了finalize()方法,被标记后会进入一个F-Queue队列,等待一个JVM低优先级线程执行对象的finalize()方法,倘若在方法中该对象重新别GC ROOTS链上对象引用,则“逃脱死亡”,不会被回收

垃圾回收算法
1、“标记-清除”:最基本的算法,给可回收对象做个标记,然后在GC的时候进行清除。带来问题是导致内存空间不连续、空间碎片多,下一次分配大内存对象时候又会触发垃圾收集动作。
2、复制算法:将内存空间分为Eden:Survivor1:Survivor2=8:1:1,新创建对象分配到Eden、Survivor1中,当GC时,将存活的对象复制到Survivor2中,然后清空Eden、Survivor1。该算法适用于较少对象存活的情况、对于多数对象存活的情况,大量复制会导致耗时增加、因此一般适用于新生代的GC
3、“标记-整理”:给可回收对象加标记后,让存活对象都向一端移动,然后直接清除端边界以外的内存。
4、分代收集算法:将堆内存分为新生代、老年代。新生代使用复制算法、老年代使用“标记清除”或者“标记整理”算法。

垃圾回收器
1、Serial:作用于新生代,单线程完成垃圾收集工作,新生代采用复制算法。“Stop the world”,省去了多线程切换的消耗,client模式下很好的选择
2、ParNew:作用于新生代,多线程版本的Serial,其余和Serial无差,在单CPU性能甚至弱于Serial,适用于多核多CPU的Server模式
3、 Parallel Scavenge:作用于新生代,采用复制算法,多线程,并不关注停顿时间,而是关注吞吐量优化,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间),适用于不需要太多用户交互的后台任务。另外UseAdaptiveSizePolicy 自适应策略(找到最大吞吐)也是该垃圾回收器特点之一
4、Serial Old:作用于老年代,采用标记整理算法,在于client模式下是好的选择。
5、Parallel Old:作用于老年代,标记整理算法,配合Parallel Scavenge是吞吐量最大的组合
6、CMS(Concurrent Mark Sweep)(Java9之后弃用,推荐G1收集器): 作用于老年代垃圾回收,采用标记-清除算法,是以停顿时间最短为目标的垃圾收集器,适用于多核Server端上。 可以通过JVM启动参数:-XX:+UseConcMarkSweepGC来开启CMS,主要分为四个阶段:
a) 初始标记:GC Root直接关联的老年代对象,标记存活。(Stop the World)
b) 并发标记:从被a标记的对象开始往下找, 标记老年代剩余存活对象。
c) 重新标记:扫描新生代、老年代,标记新分配到老年代对象、并发期间修改的对象等。(Stop the World)
d) 并发清除:清除未标记的对象。

缺点:1、垃圾回收线程占用一定CPU资源,使得整个应用吞吐下降。CMS垃圾回收器默认启动线程数(CPU个数+3)/4,在CPU数量较少的时候,吃计算资源多。
          2、在并发清除阶段,应用程序产生新的垃圾,即“浮动垃圾”留给下一次GC回收。且由于CMS与用户程序并发,因此需要预留部分空间给用户使用。
          3、标记清除算法导致内存空间碎片。当无法找到足够内存空间分配,不得不提前出发Full GC。
7、G1:Java9默认垃圾回收器。主推。JVM堆内存大的时候,CMS也会相对慢,且造成内存碎片。弱化新老代的边界,把内存区域划分为大小相等的Region,每个Region可以是Eden、Survivor、Old、Hugexx。每次回收都是,G1有整理内存(压缩)的过程,不会产生碎片且加上了停顿时间预测模型,用户可以指定期望停顿时间。优点:能与应用程序并发执行;整理空闲空间更快;可预测GC停顿时间;不希望牺牲大量吞吐性能;虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:
  • a) G1的设计原则是"首先收集尽可能多的垃圾(Garbage - First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部- 采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时- 间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • b) G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进- 行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天- 然就是一种压缩方案(局部压缩)
  • c) G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的- survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不- 同代之间前后切换;
  • d) G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次- 收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合- 收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿

内存分配策略
1、优先分配到Eden:新生成的对象优先分配在Eden区+From Survivor区,空间不够时候触发Minor GC,将存活对象复制到To Survivor区,Survivor区容量仍然不够,则将部分直接分配到老年代
2、大对象直接分配到老年代:通过 -XX:PretenureSizeThreshold参数控制。
3、长期存活的对象分配到老年代:每次进行Minor GC时,没被回收,对象年龄+1,超过默认值(15),则对象进入老年代
4、空间分配担保:每次进行MInor GC的时候都要查看当前老年代连续的可用空间是否大于Eden对象总和,若是,则证明Minor GC后若大量对象存活,Survivor空间存放不下,
可以将对象分配到老年代;如果不是,则需要检-XX:+HandlePromotionFailure 参数,是否为之担保,若不是,则进行Full GC回收老年代,若是,则再次检测老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小,
若是则进行Minor GC,否则Full GC回收老年代

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

字节码与机器码
高级程序语言(.java)通过编译器编译成字节码文件(.class),字节码文件是包含数据片段、虚拟机指令的二进制文件,由JVM解释器解释成机器码(0101机器指令),交由操作系统执行。

类加载过程
加载:从网络(如applet)/zip(war、jar)加载字节码文件,并将描述的静态存储结构转换成方法区中运行时结构。
验证:验证字节码文件格式正确性、元数据语义校验(确保符合Java语义)、字节码校验(校验数据流、跳转等保证安全)、符号引用校验(是否正确调用方法、访问变量);该环节很重要,但是可以跳过 -Xverify:none。
准备:将类一些静态变量附上“零值”,比如Public static int a = 1;  这时候将a变量放在方法区 且值为0;
解析:常量池内符号引用替换成直接引用
初始化:静态变量赋值,静态语句、静态代码块等。
使用:xx
销毁:

双亲委派机制
Java中的类加载大体分为四类:启动类加载器(Bootstrap ClassLoader)扩展类加载器(Extension ClassLoader)应用程序类加载器(Application ClassLoader)用户自定义类加载器(User ClassLoader)。

前提:在JVM中,只有类加载器与类名共同确定一个类对象。同一个类文件,通过不同类加载加载,判断为不同对象,通过Instanceof关键字判断为false

根据类加载器流程图,当需要查找一个class对象时候,由于类加载机制只要加载过该类,就不需要去重新加载,只需要查找缓存
1、缓存路:查找自身加载器是否有缓存,没有则委托父类AppClassLoader加载器---->查找AppClassLoader加载器是否有缓存,没有则委托父类ExtClassLoader---->查找ExtClassLoader加载器是否有缓存,没有则委托BoopStrap加载器–>查找BoopStrap加载器是否有缓存,没有则开始加载(在任何一个加载器中该类已经加载,则直接返回)
2、加载路:BoopStrap在核心库中加载,如果未加载成果---->ExtClassLoader在lib/ext中加载,如果未加载成果----->AppClassLoader在当前classpath中加载,如果未加载成果---->自定义加载器加载,如果未加载成果---->抛出异常ClassNotFoundException

因此每当加载一个类的时候,会先请求父类加载器加载,父类加载器无法加载时候才会由子类加载器加载。这种机制避免了程序员自行写一个java.lang.object类(双亲委派机制保证所有加载器最终都会交给Bootstrap ClassLoader加载,Bootstrap加载器认为已经加载过Object类了,从而拒绝加载其他的Object类),然后被成功加载,导致内存里有两个Object类


字面量:文本字符串、声明为final的常量值等
符号引用:类和接口的全限定名,字段的名称和描述符等。





JMM Java内存模型定义了一组多线程环境下变量访问的规范规则,主要围绕原子性、可见性、有序性展开。

通常情况下,为了优化性能,JVM会进行编译器优化重排(编译器在保证指令间没有数据依赖的情况下,将指令进行重排,优化执行效率),处理器指令重排(常规顺序执行指令可能导致操作中断,而延后等待,通过进行指令重排,可以提高执行效率)



JVM除了提供volatile、ReetrantLock保证变量读取的原子性、可见性、有序性以外,JMM还规定了happens before原则来保证多线程环境下两个操作间的原子性、可见性、有序性:
程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
传递性 A先于B ,B先于C 那么A必然先于C

线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

对象终结规则 对象的构造函数执行,结束先于finalize()方法


Volatile:1、保证可见性( 被volatile修饰的变量对所有线程总数立即可见的)2、禁止指令重排
Volatile通过内存屏障来实现上述两个特性: 内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

Tips:1、虚拟机提供-XX:+PrintGCDetails 参数来打印垃圾回收日志,并在进程退出前打印当前内存各个区域分配情况。
2、 JVM有一个参数可以让我们设定:-XX:PretenureSizeThreshold。下面我们来尝试一下。大对象直接进入老年代

JVM调优
JVM调优参数参考
1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。
比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
3.年轻代和年老代设置多大才算合理
1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
在抉择时应该根 据以下两点:
(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。
5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。
理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统

JDK 1.8 默认垃圾回收器: Parallel Scavenge + Parallel Old   Elasticsearch默认垃圾回收器 ParNew + CMS (追求更低的停顿)



#Java开发##Java##学习路径#
全部评论
大佬太强了
点赞 回复
分享
发布于 2021-11-29 18:54
谢谢大佬的资料
点赞 回复
分享
发布于 2021-12-02 14:59
乐元素
校招火热招聘中
官网直投
感谢大佬
点赞 回复
分享
发布于 2021-12-07 17:40

相关推荐

4 21 评论
分享
牛客网
牛客企业服务