以下文章来源于小龙coding ,作者小龙coding
大厂进阶指南,专注分享后端技术、校招面试求职~
之前发表的「吃透MySQL系列」专栏与「吃透Redis系列」专栏收到很多小伙伴的来信,回馈效果都很好。但是反应关于JVM的文章很少。
因此,我打算开一个「吃透JVM系列」的专栏。
之前发过一篇关于JVM面试知识点总结的文章。但是缺乏系统每个知识点的讲解,于是我打算以那篇文章为目录根据每个知识点后面为大家详细讲解,不过需要等吃透MySQL系列讲解完。
今天,先公布之前JVM面试总结的修订版,并给大家预先宣传一波,「关注公众号,持续阅读后续精彩好文」。
本文将作为本专栏「吃透Redis系列」目录,也是大厂面试标准回答,具体每个点的详细解析会收录于本专栏,关注【小龙coding】,持续阅
读后续精品文章!!
❝本文收录于【面试笔记】,更多付费文章,可以后台回复【面试笔记】获取,【点击此处试读】。
❞
「堆」
对象实例、数组
-Xms表示堆初始大小
-Xmx表示堆最大大小
逻辑上连续,线程共享,虚拟机启动时创建,最大
没有内存完成实例分配,且无法扩展,OOM
「方法区」
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存
被线程共享,不会频繁GC
「实现」:jdk7把静态变量和字符串常量池移到堆中,jdk8移除永久代,把方法区移致元空间,它位于本地内存。
❝注意:JDk6、JDk7方法区即PermGen(永久代),JDK8方法区就是MetaSpace(元空间)
❞
运行时常量池:
「Class文件存放什么:」
类的版本、字段、方法、接口
常量池表(Constant pool)(存放编译期生成的各种「字面量」与「符号引用」)
int a = 1;//1、2、“abcdefg”就是字面量
int b = 2;//a b c d字段名就是符号引用,还有方法名、全限定类名等都属于=符号引用。
String c = "abcdefg";
String d = "abcdefg";
Class文件存放的常量池表的内容将在类加载后存放到方法区的运行时常量池(「符号引用转为直接引用」)
「与Class文件常量池区别」:动态性(可以在运行期间将常量放入池(String:intern()))
「OOM」:常量池无法申请到内存JVM常量池解析见下文
「Java虚拟机栈(栈帧组成)」
局部变量表(存放基本数据类型+对象引用+返回地址)
操作数栈
动态链接
方法出口
异常:
StackOverFlowError:线程请求的栈深度大于虚拟机允许的
OOM:栈容量动态扩展无足够内存
命令参数:java -Xss2M stackjava
「本地方法栈」:
为虚拟机使用到的Native方法服务
「程序计数器」:
当前执行的字节码指令,「唯一没有OOM的地方」 | 执行本地方法时本地方法计数器为NULL | 「线程私有」
「直接内存」
2.1、new-构造函数
2.2、Class类的newInstance方法-构造函数 「(相当于用无参构造」)
2.3、Constructor类的newInstance方法-构造函数
(「bClass.getConstructors() 可以按顺序获取所有构造函数」)
2.4、反序列化
2.5、clone
「代码:」
Constructor相关
Class<B> bClass = B.class;
B b = bClass.newInstance();
System.out.println(b.getName());
Constructor<B> constructor[]= (Constructor<B>[]) bClass.getConstructors();
B b1 = constructor[0].newInstance("11",22);
System.out.println(b1.getName()+b1.getAge());
反序列化
//序列化过程 B需要实现序列化接口
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("b.txt"));
objectOutputStream.writeObject(new B("11",2));
objectOutputStream.close();
//反序列化
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("b.txt"));
B b = (B)objectInputStream.readObject();
System.out.println(b.getName()+b.getAge());
1、当虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否被加载过,若没有执行相关类加载过程。「//类加载检查」
2、类加载通过后分配内存,若堆内存规整,执行指针碰撞分配内存,否则使用空闲列表分配;「//分配内存」
3、划分内存还需要考虑并发问题,可以CAS同步处理,或则本地线程分配缓冲。「//并发问题处理」
4、然后内存空间初始化操作(默认值,一)些必要的对象设置(元信息、哈希码),再行init()方法(按照程序员意愿初始化)。
「句柄:「指针的指针。堆划分一块内存作为句柄池(句柄池+实例池),引用存储句柄的地址,句柄中包含了」对象实例数据指针」(指向堆对象实例数据)+「对象实例类型指针」(指向方法区对象类型数据)
「直接指针」:直接指向对象,保存对象内存起始地址的指针
「指针碰撞」
堆内存规整,将堆分为空闲和使用过两部分,空闲的放一边,用过的放一边,中间放一个指针指向分界处,分配对象内存时就将指针向空闲部分移动相应大小
「空闲列表」
堆内存不规整,需借助列表存放可用空间,分配对象内存时查看列表找到足够的空间分配给对象,并更新列表
「分配内存需考虑并发问题」:可能正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
「CAS+失败重试保证更新原子性」:对分配内存空间动作进行同步处理
「本地线程分配缓冲」:每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲)哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,本地缓冲区用完了分配新的缓冲区才需要同步锁定
java对象=对象头+实例数据+对齐填充
对象头=Mark Word+ 对象所属类 的指针组成(如果是数组对象,还会包含长度)
Mark Word=存储对象自身的运行时数据,例如hashCode,GC分代年龄,锁状态标志,线程持有的锁等等
JVM堆,无法给实例分配内存,且无法扩展时,OOM。
方法区(以及运行时常量池)无法满足内存分配需求时,OOM.
Java虚拟机栈+本地方法栈,扩展时无法申请到足够内存,OOM(线程请求深度超过JVM允许栈深度,StackOverflowException).
无用内存得不到释放。程序申请了内存,使用完后又不能归还JVM,造成内存泄漏,内存泄漏多了就造成内存溢出。内存溢出——》
OOM(内存满了,没有内存给实例分配空间,且无法扩展)
Student stu=new Student();
List<Student> stus=new ArrayList<>();
stus.add(stu);
stu=null;
stu占用的内存得不到释放,stus占用着student. 发生内存泄漏
「引用计数器法:」
对象添加一个引用计数器,每当有个引用就加一,引用失效减一,当为减为0对象不可用
缺点:相互引用,A<-->B,然后A、B已经没有被其他有用对象引用,本视为垃圾,但是由于互相引用不能被检查回收。
「可达性分析法:」
从GC Roots到该对象可达
总结记忆口诀:两栈一方法
局部变量表:存放方法参数和方法内部定义的局部变量
「强引用」
Object obj=new Object(); StronglyReference ——不会被回收
「软引用」
new SoftReference(obj); 描述有用但非必须的对象——内存不够回收
「弱引用」
WeakReference 弱引用一定会被回收,下一次GC回收
「虚引用」
PhantomReference 为了能在这个对象被收集器回收时收到一个系统通知
「废弃的常量」
没有被引用
如:字面量回收
❝一个字符串“abc”放入常量池,现在没有一个值为"abc"的字符串对象,也就是 没有任何字符串对象引用常量池中“abc”的常量,且虚拟机其他地方没有引用这个字面量,如果发生垃圾回收且有必要时,常量会被系统清理出常量池 String s1="abc"; Strig s2=new String("abc"); 其他接口,方法,字段,符号引用类似
❞
「无用类的卸载」
「标记-清除」
「复制算法」(适用新生代—存活率低)
「标记-整理」
「分代收集」
「注重低延迟」
「CMS」:基于标记清除的并发垃圾收集器
「优点」:支持并发,停顿时间短
「缺点」:使用标记清除算法,空间碎片。并发标记产生浮动垃圾。
「G1」:并发+并行(重新标记+筛选回收)
「弱化分代(老年代与年轻代一起回收),引入分区。将堆分为多个大小相等区域分而治之。」
「特点:」
「空间整合:「整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不」会产生内存空间碎片」。
「可预测的停顿」:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
「stw」:垃圾回收,暂停所用用户线程执行,避免垃圾回收时产生新垃圾。
含义:类加载请求来了,类加载器自己先不加载,先让父类加载器加载,父类不行自己再来
「怎样打破双亲委派机制:」
Java涉及SPI机制的都用线程上下文类加载器。父类加载器请求子类加载器完成加载动作
SPI机制:为接口找寻服务(jdbc)
❝SPI约定:服务提供者为接口提供接口实现后,会在jar包的META-INF/service/目录下创建一个以服务接口命名的文件。
❞
JDBC4.0使用SPI机制,DriverManager需要去jar包下的META-INF/services/java.sql.Driver目录下去寻找对应的Driver加载,但是
DriverManager在rt.jar中,使用启动类加载器(BootStrapClassLoader),它需要调用服务提供者放在classpath下的类,启动类加载器无
法加载,就只得使用线程上下文加载器,让父类加载器调用子类加载器完成,打破了双亲委派机制。
「好处」:避免类重复加载+防止核心类篡改+安全性
Minor GC:回收年轻代
Major GC :回收老年代
Full GC:回收年轻代与老年代,方法区域
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
大对象指「需要连续分配空间」的对象,长字符串,数组。
对象过大,由于需要连续的内存空间,会导致提前进行垃圾回收以获取足够的连续空间 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
对象在Eden区出生,经过一次Minor GC存活下来,分代年龄就会加一,增加到一定年龄就会移向老年代。默认是15. -XX:MaxTenuringThreshold用来定义该年龄的阈值。
虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,当「Survivor区相同年龄的所有对象大小总和大于Survivork空间一半」,则年龄大于或等于该年龄的对象直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
「分代回收器有两个分区」:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
「它的执行流程如下:」
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。「大对象也会直接进入老生代。」
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
「场景:」
大对象直接进入老年代(老年代空间足,但是没有足够的连续空间) 长期存活的对象直接进入老年代
「解决:」
1、尽量「不要创建过大的对象」以及数组 2、可以通过-Xmn「调大新生代大小」,让「对象尽量在新生代被回收」,不进老年代 3、可以通过-XX:MaxTenuringThreshold「调大分代年龄阈值」,让对象得新生代多存活一段时间
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
「解释一」
老年代最大可用的连续空间<新生代所有对象总空间 && HandlerPromotionFailure设置不允许担保失败 full gc
老年代最大可用的连续空间>新生代所有对象总空间 && HandlerPromotionFailure设置允许担保失败 && 通过Minor GCJ进入老年代的象平均大小>老年代最大连续空间大小 full gc
「解释二」
Minor GC前,先判断老年代最大连续空间是否大于新生代所有对象总空间。
若大于,安全;若小于,查看HandlerPromotionFailure设置是否允许担保失败,允许,再看通过Minor GCJ进入老年代的对象平均大小>老年代最大连续
空间太小,则还是失败,进行Full GC。
永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
「对象定义在错误的范围」如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。「异常处理不当」各种资源的关闭一定要放在finally里面
「申请方式」:栈系统自动申请,堆需手动申请c语言malloc(),java new Object();
栈系统分配速度快,堆慢,容易内部碎片;栈地址空间连续,堆是不连续的(链表存储空闲内存地址);
「内容不一样」(堆存对象实例与数组,关注存储;栈存储局部变量表,操作数栈,关注运行);
「大下限制」(栈预先设定好的,编译器即可确定,堆取决有效虚拟内存,运行期间确定)
**概念:**当一个对象在方法中被定义后,它可能被方法外部其他对象所引用,则称逃出方法(内存逃逸现象)
使用逃逸分析,编译器优化
如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配
1、启动程序之前通过 HeapDumpOnOutOfMemoryError 和 HeapDumpPath 这两个参数「开启堆内存异常日志」
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
2、从日志从发现异常
3、通过top命令查看进程cpu使用率
4、再通过 top -Hp pid 查看进程下所有「具体线程占用系统资源情况」。
5、再通过 jstack pid 查看具体线程的「堆栈信息」(线程ID、状态(wait,sleep),是否持有锁)
6、再通过 jmap 查看「堆内存的使用情况」 jmap -heap pid
7、通过以上命令分析基本可以看出什么问题导致内存上升,现在分析问题产生的原因
8、我们在启动时,已经设置了 dump 文件,通过 MAT 打开 dump 的内存日志文件,分析即可。
最后三节,属于进阶内容,前面基础一定要掌握好。由于篇幅有限,关注公众号,后期会专门针对大厂面试常问性能调优与工具进行讲解。
关注我公众号“小龙coding”,我们一起探讨,帮助修改简历,回答疑问,项目分析,只为帮助迷茫的你高效斩获心仪offer!
后续会陆续更新大厂面经面试题与解析,大厂内推「直达部门主管」,也有交流群大家一起探讨共同进步。加油噢!