简介:

本期文章将汇总jvm虚拟机相关的高频面试题,给最近需要找工作的朋友们总结一波,帮助大家全面掌握jvm优化及jvm核心知识点,提升竞争力,为你的面试之旅保驾护航!


你们用什么工具监控JVM

jconsole, jvisualvm

JVM类加载流程(重要)

加载 -> 验证 -> 准备 -> 解析 -> 初始化

loading加载:class文件从磁盘加载到内存中

verification验证:校验class文件,包括字节码验证,元数据验证,符号引用验证等等

preparation准备:静态变量赋默认值,只有final会赋初始值

resolution解析:常量池中符号引用,转换成直接访问的地址

initializing初始化:静态变量赋初始值

JVM类加载器有几种类型,分别加载什么东西,用到什么设计模式?

1. BootStrap ClassLoader 启动类加载器,加载<JAVA_HOME>\lib下的类

2. Extenstion ClassLoader 扩展类加载器,加载<JAVA_HOME>\lib\ext下的类

3. Application ClassLoader 应用程序类加载器,加载Classpath下的类

4. 自定义类加载器

这里是用到了双亲委派模式,从上往下加载类,在这过程中只要上一级加载到了,下一级就不会加载了,这么做的目的:

  1. 不让我们轻易覆盖系统提供功能,安全
  2. 可以扩展第三方包的功能。

扩展

这种情况也是可以打破的,比如tomcat就打破了这种模式。

tomcat自定义类加载器webClassLoader,不用父类加载器,比如我们部署了多个应用,它加载多个应用时,可能会出现类的重复加载,它还有一个公共的shareclassloader 来加载公共的类

JVM组成,以及他们的作用

运行时数据区:

:存放对象的区域,所有线程共享

虚拟机栈:对应一个方法,线程私有的,存放局部变量表,操作数栈,动态链接,返回地址等等

本地方法栈:对应的是本地方法,在hotspot中虚拟机栈和本地方法栈是合为一体的

程序计数器:确定指令的执行顺序

方法区/元空间:存放虚拟机加载的类的信息,常量,静态变量等等,JDK1.8后,改为元空间

执行引擎:

即时编译器:用来将热点代码编译成机器码(编译执行)

GC垃圾收集:将没用的对象清理掉

其他:

本地方法库:融合不同的编程语言(c/c++...)为java所用

在JVM层面,一个线程是如何执行的

线程执行,每个方法都会形成一个栈帧进行压栈保存到虚拟机栈中,方法调用结束就会出栈。

调用过程中创建的变量在虚拟机栈,对象实例存放在堆内存中,栈中的变量指向了堆中的内存。

当方法执行完成就出栈,创建的变量会被销毁,堆中的对象等待GC。

程序内存溢出了,如何定位问题出在哪儿?

增加启动参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\ 可以把内存溢出的日志输出到文件,然后通过JVM监视工具VisualVM来分析日志,定位错误所在。

在linux服务器也可以使用命令: jmap -dump 来下载堆快照。

垃圾标记算法

垃圾标记算法有:引用计数可达性算法

引用计数:给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方不再引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象. 这种算法的问题是当某些对象之间互相引用时,无法判断出这些对象是否已死

GC Roots :找到一个对象作为 CG Root , 当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的

垃圾回收算法

标记清除算法:分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象;缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

复制算法:把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环,缺点:实际可使用的内存空间缩小为原来的一半,比较适合

标记整理算法:先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存

分代收集算法:把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此新生代采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

垃圾回收器有哪些

新生代:Serial :一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World

新生代:ParNew : ParNew就是一个Serial的多线程版本,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的效果,它默认开启的收集线程数和CPU数量一致,可以通过-XX:ParallelGCThreads来设置垃圾收集的线程数。

新生代:Parallel Scavenge(掌握) Parallel Scavenge也是一款用于新生代的多线程收集器,与ParNew的不同之处是,ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge的目标是达到一个可控制的吞吐量.Parallel Old收集器以多线程,采用标记整理算法进行垃圾收集工作。

老年代:Serial Old ,Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法。

老年代CMS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤

初始标记:标记一下GC Roots能直接关联到的对象,速度较快

并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长

重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短

并发清除:用标记-清除算法清除垃圾对象,耗时较长

整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。

老年代:Parallel Old ,Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与Parallel Scavenge收集器搭配,可以充分利用多核CPU的计算能力。

堆收集:G1 收集器, G1 收集器是jdk1.7才正式引用的商用收集器,现在已经成为jdk1.9默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1进行垃圾收集的范围是整个堆内存,它采用“化整为零”的思路,把整个堆内存划分为多个大小相等的独立区域(Region)在每个Region中,都有一个Remembered Set来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个Remembered Set来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据

Jdk1.7.18新生代使用Parallel Scavenge,老年代使用Parallel Old

Minor GC和Full GC

新生代的回收称为Minor GC,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短 。

而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收。

而老年代采用标记-整理算法,这种GC每次都比较慢,造成的暂停时间比较长,通常是Minor GC时间的10倍以上。所以尽量减少 Full GC。

JVM优化的目的是什么?

优化程序的内存使用大小,以及减少CG来减少程序的停顿来提升程序的性能。

堆怎么调,栈怎么调

-Xms : 初始堆,1/64 物理内存

-Xmx : 最大堆,1/4物理内存

-Xmn :新生代大小

-Xss : 栈大小

你了解JVM 的逃逸分析吗?(加分)

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。

这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。(不要在轻易回答面试官,所有对象都是在堆上创建了)

逃逸分析的基本原理是:

分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;

甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;

从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

JVM中通过如下参数可以指定是否开启逃逸分析

-XX:+DoEscapeAnalysis :表示开启逃逸分析。

-XX:-DoEscapeAnalysis :表示关闭逃逸分析。

开启逃逸分析,编译器可以对代码进行如下优化:

1、同步消除如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。

2、栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。

3、标量替换:如果一个对象被逃逸分析发现不会被外部方法访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个比这个方法使用的成员变量来代替。将对象拆分后,可以让对象的成员变量在栈上分配和读写。


结语

🔥如果文章对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下小老弟,蟹蟹大咖们~ 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐