JVM面试速成篇

老规矩,先赞在收藏,不做白嫖党。

内存区域划分

JVM的内存区域如何划分?并解释每个给区域的作用。

JVM面试速成篇_第1张图片

  • 程序计数器(私有):简单理解成行号。字节码解释器工作的时候就是通过程序计数器来寻取下一条字节码指令的。
  • 虚拟机栈(私有):描述Java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。
    虚拟机栈是描述方法的模型,所以为了方便记忆,想一下方法都有什么东西。
    public int Hello(int x){ 
        int a=1,b=1; # a b x 都放在局部变量表
        int c=a+b; # + 操作栈
        getHello(); # 被调用的目标方法在编译期无法被确定下来,
        #只能从运行时常量池中将符号引用转换成直接引用,这个过程就是动态链接。
        return c; # 这个返回就是方法的出口。
    }
  • 本地方法栈(私有):虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
  • 堆(共享):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存。
  • 方法区(共享):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码(JIT生成的符号引用就是放在运行时常量池)等数据。

JVM堆内存的内部如何划分?

JVM面试速成篇_第2张图片

  堆分为年轻代和老年代,年轻代又分为Eden区和Survivor区, Survivor分为From 和To区域
其中默认比例为 老年代:年轻代=2:1;eden:from:to=8:1:1

方法区、永久代、元空间区别是什么?

  方法区是JVM内存规范,永久代是这种规范的JVM实现。元空间则是在1.8之后取代了永久代的一种实现。并且只有 HotSpot 才有 永久代。

为什么用元空间替换成永久代?

  因为永久带有MAX上限,容易遇到内存溢出问题。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。所以1.8之后使用元空间代替永久代,元空间使用的是本地内存,只要本地内存足够大就可以解决oom问题。

深拷贝和浅拷贝的区别是什么?

JVM面试速成篇_第3张图片

  浅拷贝:增加了一个指针指向已存在的内存地址
  深拷贝:增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存

类加载机制

一个Java类是如何运行起来的?

  首先通过打包工具将java类编译成.class文件。然后JVM通过类加载器将.class文件加载到内存,最后JVM就会基于自己的字节码执行引擎,来执行加载到内存里的我们写好的那些类了。

说说JVM类的加载过程

  加载-链接-初始化
  链接包括:验证->准备->解析

能具体解释一下每一步都是做什么的?

  • 加载:将class文件加载到内存。
  • 验证:根据相关规范去验证你的.class文件是否合规。
  • 准备:给对应的类、变量分配内存空间,赋初始值。
  • 解析:符号引用变为直接引用。(具体何为符号引用,上一篇内存换分中动态链接处有说。)
  • 初始化:真正执行类中定义的java程序代码。包括逻辑处理赋值等操作。

    什么时候才会初始化一个类?

  1. 执行需要引用类或者接口的java虚拟机指令(new,getstatic, putstatic, invokestatic)的时候
  2. 调用类库中的某些反射方法的时候。
  3. 初始化子类发现父类没初始化时候,会先初始化父类。
  4. 包含“main()”方法的主类,是立马初始化的。

    Java中类加载器有几种?

  5. 启动类加载器Bootstrap ClassLoader:主要负载加载Java目录下的核心类的。在jdk安装目录下有一个lib目录,这下边的就是 java最核心的类库。启动类加载器就是加载这下边的类。
  6. 扩展类加载器Extension ClassLoader:他和启动类加载器类似,只不过他加载lib/ext目录下的类。
  7. 应用类加载器Application ClassLoader:主要是更具你的需求去加载你自己的类
  8. 自定义类加载器:这个没啥说的,就是你自定义的类加载器。

    为什么还需要自定义类加载器呢?

     大家知道java代码很容易被反编译,如果你需要把自己的代码加密防止反编译,这个时候就用到了自定类加载器了。

    说说什么是双亲委派加载机制?

    JVM面试速成篇_第4张图片

  打比方我们Hello.class这个类,他在被加载到内存的时候就会先问他爸爸-扩展类加载器,然后扩展类加载器再问自己老爸-启动类加载器,然后启动类加载器就开始在lib下找Hello.class,没找到,告诉扩展类加载器,你自己玩去,我这没有。扩展类一看老爸没有那我就自己来吧,找了半天他也没有,然后就告诉应用类加载器说我和你爷爷帮不了你了,你还得靠自己去找。这个过程就是双亲委派。

双亲委派的作用是什么?

  1. 安全性:防止核心类被篡改。比如你自定义一个java.lang.String类,他在加载的时候发现父类加载器已经加载了,就不会在进行加载这个类。
  2. 避免重复加载:父加载器已经加载过的类,子加载器就不会再进行加载了,有效的防止了重复加载问题。

    举例说明打破双亲委派机制

  3. 设计上的缺陷,由于越基础的类越由上层加载器进行加载,但如果有些基础类又要调用用户代码,这个时候就会打破(SPI)。如何你你对dubble了解就可从这个spi向dubble上引导。
  4. 由于用户程序动态性导致(热部署)OSGI。
    tomcat 打破双亲委派实例
  • tomcat是web容器,一个web容器可以部署多个程序,不同程序依赖的第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一个份,因此要保证好相互的隔离。并且为了安全tomcat容器依赖的类库和应用所以来的类库也需要隔离。这就打破了双亲委派。
  • 众所周知jsp文件也是最终编译成class文件被夹在到虚拟机的。对于class修改了,如果类名一样类加载器会直接去取内存中已经存在的class,修改就不会生效,tomcat在修改jsp页面不需要重启服务也可以生效。说明也打破了双亲委派机制。tomcat是如何做的呢?他给每个JSP都准备了一个Jsp类加载器。当jsp改变就卸载类加载器,重新创建类加载器,重新加载jsp文件。

    垃圾回收

    如何判断对象是垃圾

  1. 引用计数:给对象添加一个引用计数器,当有地方引用时+1,失效时-1。当这个计数器的数值位0的时候表示对象已死亡,可以回收了。
    缺点:这种算法会出现循环引用(A->b b->A)无法回收问题。
  2. 可达性分析:当一个对象到GCRoots没有任何引用的时候,表示该对象不可用
    对于GCRoots我们可以理解为方法的局部变量和类的静态变量。实例变量是不是。
    JVM面试速成篇_第5张图片

四种引用类型都是什么?

  • 强引用:任何时候都不能被回收。
  • 软引用:内存不足时会进行回收。可以应用在缓存上。
  • 弱引用:下一次GC的时候就会被回收。
  • 虚引用:又称作幽灵引用;无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 GC 时返回一个通知。

    垃圾回收算法

    标记清除

      标记出无用的对象,统一清除。
    存在问题:①效率问题,②空间利用率问题,容易出现悬浮碎片,在创建大对象的时候会再次触发垃圾回收。
    JVM面试速成篇_第6张图片

复制

  复制:将内存划分为大小相同的两块,每次只用其中的一块,当内存满了的时候,我们将存活的复制到另一块,然后剩下部分全部删除。
存在问题:虽然解决了悬浮碎片问题,但是内存使用效率下降,毕竟每次只有一般内存可用。并且如果对象存率较高,那么将频繁触发垃圾回收。

标记整理

  标记整理:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。他可良好的解决掉标记清除的悬浮碎片问题。
存在问题:效率问题
JVM面试速成篇_第7张图片

分代收集

  根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记清除算法。

垃圾回收器

serial和serial old

  单线程的垃圾回收,从名字可以看出一个作用在新生代,一个作用在老年代。现在已经几乎不使用了。

parnew

  多线程垃圾收集器,他作用在新生代,常和cms一起使用。

parallel

  他也叫做“吞吐量优先收集器” (吞吐量=代码运行时间/代码运行时间+垃圾回收时间)少的停顿时间可以增加用户的体验感,大的吞吐量可以提高cpu效率;他也常和cms一起使用

cms

  使用标记清除算法,适用于老年代。
cms 的四个阶段:

  1. 初始标记:标记的是GCRoot, 这个过程会stop the world.
  2. 并发标记:开始通过GCRoot去追踪所有存活对象,这个过程是程序是可以工作的。
  3. 重新标记:因为并发标记过程程序还在跑还会产生一些垃圾,需要重新标记。这个过程stop the world.
  4. 并发清除:并发的清除垃圾。
    以下边代码为例进行解释说明:

    public class Hello {
    private static A a = new A();
    }
    class A{
    private B b =new B();
    }
    class B{}

    JVM面试速成篇_第8张图片

缺点:

  • 标记清除的缺点会产生悬浮的空间碎片
  • 消耗cpu资源。并发清除和并发标记的过程cpu占用比较高。CMS默认启动的线程数 =(cpu核+3)/4。如果2核的4G默认就是需要占用一个cpu。
  • Concurrent Mode Failure问题, 因为并发清理的过程中是允许程序运行的,如果在清理过程中再次minorGC,并且老年等待的空闲空间不足以存储对象,这个时候就会触发 Concurrent Mode Failure ,然后垃圾回收器被强制切回Serial old收集器。
  • JVM面试速成篇_第9张图片

G1

  G1收集器是JDK7提供的一个新收集器,JDK9之后变为默认垃圾回收算法。G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
G1是将内存划分为多个大小相等的 Region 。默认是2048个大小从1-32MB之间。
JVM面试速成篇_第10张图片

G1的收集过程

  • 初始标记:同cms一样,初始标记GCRoot,这个阶段也需要stop the world
  • 并发并发:同cms一样,通过GCRoot进行追踪存活的对象,这个过程比较慢,所以是- - 并发执行的,但是这个过程是不影响系统正常工作的。
  • 最终标记:用于处理并发阶段结束后仍遗留下来的垃圾对象。stop the morld
    筛选回收:这一步是最为关键的,G1之所以可以控制回收预期的停止时间,就靠它了。G1会根据回收价和成本进行选择性回收。(如通过追踪发现回收一个region10m需要1s 另外一个回收200m需要1ms;他肯定选择回收200m的那个。)

ZGC

  他是JDK 11中推出的一款低延迟垃圾回收器,ZGC 和 G1 一样是基于 reigon 的,几乎所有阶段都是并发的,整堆扫描,部分收集并且部分代。如果你可以回答出这个证明你对新知识存在敏感性。你就是面试官找打那个他。因为是速成片,就不去详细的介绍了。

对象入住老年代的条件

  • 经过多次minorGC都没都能回收。默认是15次就会到大老年代。可以通过-XX:MaxTenuringThreshold进行修改。
  • 大对象直接进入到老年代;可通过-XX:PretenureSizeThreshold设置大对象的标准。
  • minorGC后值大于survivor的大小。直接放到老年代
    JVM面试速成篇_第11张图片
  • 动态判断,survivor区中一批对象总大小大于survivor区域的50%,那么年龄大于等于这批对象的都会被移动到老年区。
    年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区 域的50%,此时就会把年龄n以上的对象都放入老年代。
    如下图就会把>=2岁的全部移动到老年代。
    JVM面试速成篇_第12张图片
    晋升老年代理解记忆图
    JVM面试速成篇_第13张图片

    说说新生代GC的过程以及算法

    新生代对象一般都是朝生夕死,所以使用的是复制算法。对象出生在eden区域后,当不能承载后会触发minorGC将存活的对象移动到survivor两块中的一块空间上,下次GC的时候会将这一块空间和eden一起回收,将存存活对象移动到量一块空间上。由于默认的eden:from:to=8:1:1 所以之后10%空间限制,也极大的解决了复制算法的浪费空间问题。
    JVM面试速成篇_第14张图片

    什么是空间分配担保

      在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于相安无事,如果小于虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;这个过程就空间分配担保。
    说明:HandlePromotionFailure jdk1.6之后就被移除。

    FullGC触发时机

    空间担保参数移除之后我们就不去考虑空间担保这件事情了。

    • 内存老年代可用内存小于历次新生代GC后进入老年代的平均对象大小
    • 新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足
    • -XX:CMSInitiatingOccupancyFaction 参数,老年代使用的内存超出了这个参数的比例也会自动触发。

JVM调优

调优工具

jps

可以查看进程号;jps -v 可以查看运行的参数。

jstat

jstat用来统计gc相关信息. jstat -gcutil 进程号

jinfo

查看虚拟参数的; 还可以调整虚拟机参数 。

jmap

内存快照 (也可以通过配置虚拟机参数获取快照)
jmap -dump:format=b,file=d:\aa.bin pid

jhat

分析快照的工具,他占用cpu比较多,一般我们会将生成的快照放到本地进行分析;

jstack

线程的堆栈监控。查看当前这一时刻的线程调用堆栈情况 (又叫threaddump) 可以用来分析死锁,死循环,请求外部资源长时间等待;

jconsole、visual vm

图形化的jvm调优工具。

调优参数

  • -Xms 最小分配内存,初始化内存;
  • -Xmx 最大分配的内存;
  • -Xmn 设置年轻代的大小;
  • -Xss 设置每个线程堆栈的大小
  • -XX:NewRatio=2 年轻代和老年代的比例 1:2
  • -XX:SurvivorRatio=8 eden和survivor的比例8:2
  • -XX:+PrintGCDetails:打印 gc 详细信息。
  • -XX:+UseParNewGC 开ParNew收集器
  • XX:+UseConcMarkSweepGC 开启cms
  • -XX:PretenureSizeThreshold 晋升老年代的大对象界限
  • -XX:MaxTenuringThreshold 对象进行老年代的门槛

你们公司项目参数一般如何配置?

不同的项目不一样,你可以jps -v 查看你公司的配置然后背下来。

常见场景分析题

线上时常出现卡顿现象

 首先出现卡顿我们要向是不是sql慢了;开启慢查询日志,查看sql执行时间。
然后考虑是不是垃圾回收的锅,通过内存监控工具查看是不是频繁的fullGC,如果是fullGC引起的解决方案:更换垃圾回收器,部署多个应用然后通过nginx进行方向代理。

线上出现oom如何解决

  首先线上问题应先想办法让程序可用,在去想解决的办法,因为用户就在那里等着你呢,影响了他们就是影响了上帝,一般都是负载均衡的项目,可以先将oom的这台机器关闭,但是这个时候还要考虑一些定时任务啊,消息的消费,剩下的机器是否可以抗的住流浪啊等问题。
然后去分析OOM原因,通过jmap产生heapdump文件,将文件从线上拿到本地通过Eclipse Memory Analyzer(MAT)进行分析。

最后看完别急着走给个关注,拒绝白嫖从我做起。

你可能感兴趣的:(面试javajvm)