大三Java后端暑期实习面经总结——JVM篇

Posted Baret-H

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大三Java后端暑期实习面经总结——JVM篇相关的知识,希望对你有一定的参考价值。

img
博主现在大三在读,从三月开始找暑期实习,暑假准备去tx实习啦!总结下了很多面试真题,希望能帮助正在找工作的大家!相关参考都会标注原文链接,尊重原创!



参考:


1. jvm体系结构

  1. 类装载子系统
  2. 运行时数据区
  3. 字节码执行引擎
  4. 本地方法接口

image-20210510235026459
image-20210510214219240
image-20200802154319450
首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
image-20210510135256214
img


2. 类加载器

image-20210424175240303

其中ExtClassloader还是系统类加载器、线程上下文加载器

  • 系统类加载器:由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
  • 线程上下文加载器:贯穿于以上三个加载器,每个加载器都可以对其访问

如果想要自定义加载器,需要继承 java.lang.ClassLoader类,且为了满足双亲委派机制,需要指定父加载器为拓展类加载器


3. 类加载过程

Class Loader 类加载器

image-20210316223145616

image-20210316222046713
  1. 加载:加载.Class字节码文件加载到内存中,创建代表这个类的java.lang.Class对象
  2. 链接:对类进行链接,将类的二进制代码合并到Java运行时环境JRE
  3. 初始化:然后交给JVM对类进行初始化

详细过程图示
image-20210316223006506


4. 什么是双亲委派机制

当一个.class文件要被加载时,不考虑我们自定义类加载器类,首先会在AppClassLoader检查是否加载过,如果有那就无需再加载;如果没有会交到父加载器,然后调用父加载器的loadClass方法。父加载器同样也会先检查自己是否已经加载过,如果没有再往上,直到到达BootstrapClassLoader之前,都是在检查是否加载过,并不会选择自己去加载。到了根加载器时,才会开始检查是否能够加载当前类,能加载就结束,使用当前的加载器;否则就通知子加载器进行加载;子加载器重复该步骤。如果到最底层还不能加载,就抛出异常ClassNotFoundException

总结所有的加载请求都会传送到根加载器去加载,只有当父加载器无法加载时,子类加载器才会去加载
image-20210424172312780

作用

  • 避免类的重复加载
  • 保证Java核心类库的安全

5. 双亲委派机制怎么破坏

image-20210429223015451
image-20210429223410292

如何打破双亲委派机制?

【JVM笔记】如何打破双亲委派机制

破坏双亲委派模型 - 简书 (jianshu.com)

1️⃣ 第一次破坏

由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。

2️⃣ 第二次破坏

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。

一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计**:线程上下文类加载器**Thread Context ClassLoader。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
image-20210424190356182
有了线程上下文加载器,JNDI服务就可以使用去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

3️⃣ 第三次破坏

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。


6. jvm如何确定要回收的对象

1. 引用计数法

每个对象都有一个引用计数的属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收

该算法当前的jvm中并没有采用,因为它并不能解决对象之间循环引用的问题

假设有A和B两个对象之间互相引用,则两个对象的引用计数都不为0,都不能回收

public class Main {
public static void main(String[] args) {
MyObject A = new MyObject();
MyObject B = new MyObject();
A.object = B;
B.object = A;
A = null;
B = null;
System.gc();//不能将A、B对象进行回收
}
}

2. 可达性分析法(根回溯法)

由于引用计数法的缺点引入了可达性分析法,通过判断对象的引用链是否可达来决定对象是否可以被回收。

从GC Roots开始向下搜索,搜索过的路径称为引用链。当一个对象到 GC Roots没有任何引用链相连时,则证明此对象是不可达的,则JVM判断为可回收对象
image-20210526085856721

GC Roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈JNI(NATIVE本地方法)引用的对象

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalize队列中判断是否需要执行finalize()方法

当对象变为不可达时,GC会判断对象是否覆盖了finalize()方法,如果没有覆盖,则直接将其回收。否则,如果对象没有执行过finalize方法,将其放入F-Queue队列中,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断对象是否可达,如果不可达,则进行回收;否则对象复活

注意:每个对象只能触发一次finalize方法,此外,finalize方法的运行代价很大,不确定性也很大,无法保证各个对象的调用顺序,所以一般都不会进行覆盖。


7. jvm确定要回收对象后何时回收

  • 会在cpu空闲的时候自动进行回收
  • 在堆内存存储满了之后
  • 主动调用System.gc()后尝试进行回收

8. jvm如何回收

GC垃圾回收,主要在年轻代和老年代。首先,对象出生在伊甸园区,该区只能存放一定数量对象,存满时就会触发一次轻GC, 将伊甸园区清空;清理后,有的对象可能还存在引用,存活下来进入幸存区;以此往复,如果对象在年轻代经历了15次(可设置-XX:MaxTenuringThreshold=n参数)GC还没有死亡,则会进入老年代,老年代满时会触发一次重GC,年轻代+老年代的对象都会进行清理,如果新生代+老年代都满了,则OOM

  • Minor GC:轻GC,伊甸园区满时触发;从年轻代回收内存

  • Full GC:重FC,老年代满时触发;清理整个堆空间,包含年轻代和老年代

如何回收说的也就是垃圾收集的算法,算法有四个:

1. 标记-清除算法

这是最基础的一种算法,分为两个步骤,首先就是标记,也就是标记处所有需要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象。

这种算法优点是简单,缺点是效率问题,还有一个最大的缺点是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费

2. 复制算法

复制将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只是这种算法的代价是将内存缩小为原来的一半

3. 标记-压缩算法

标记整理算法与标记清除算法很相似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不做任何处理,造成内存碎片;而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片

4. 分代收集算法

分代收集算法是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收

image-20210331122324505


9. System.gc()

System.gc()用于调用垃圾收集器,System.gc()函数的作用只是提醒或告诉虚拟机,希望进行一次垃圾回收,至于什么时候进行回收还是取决于虚拟机,而且也不能保证一定进行回收
image-20210331110948319
实际上调用了Runtime.getRuntime().gc()方法,Runtime类中有些常用方法:

static RuntimegetRuntime() 返回与当前Java应用程序关联的运行时对象
longmaxMemory() 返回Java虚拟机将尝试使用的最大内存量
longtotalMemory() 返回Java虚拟机中的内存总量
public class Test {
    public static void main(String[] args) {
        //返回jvm试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        //返回jvm的初始化内存总量
        long total = Runtime.getRuntime().totalMemory();
        //默认情况下:分配的总内存为电脑内存的1/4,初始化内存为电脑内存的1/64
        System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");
        System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");
    }
}

image-20210331111744461
其中gc()是一个本地方法,调用了底层C/C++的库


10. jvm性能调优

参考JVM调优工具详解

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

1️⃣ 设置jvm调优参数

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
  • -Xmx用来设置 jvm试图使用的最大内存,默认为1/4
  • -Xms用来设置 jvm初始化内存,默认为1/64
  • -XX:+HeapDumpOnOutOfMemoryError表示当JVM发生OOM时,自动生成DUMP文件

2️⃣ 通过java自带的jvm调优诊断工具jvisualvm

JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

D:\\JAVA_Environment\\jdk\\jdk1.8\\bin>jvisualvm

D:\\JAVA_Environment\\jdk\\jdk1.8\\bin>

The launcher has determined that the parent process has a console and will reuse it for its own console output.
Closing the console will result in termination of the running program.
Use '--console suppress' to suppress console output.
Use '--console new' to create a separate console window.

image-20210510222056937

3️⃣ 利用内存快照工具JProfiler分析dump文件

  • Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中

  • 利用内存快照工具JProfiler分析Dump内存文件,快速定位内存泄漏;获得堆中的文件;获得大的对象…

    # 配置当JVM发生OOM时,自动生成DUMP文件(.hprof)
    -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
    

4️⃣ Arthas工具

Arthas 用户文档 — Arthas 3.5.0 文档 (gitee.io)


11. jvm调优参数

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

以上是关于大三Java后端暑期实习面经总结——JVM篇的主要内容,如果未能解决你的问题,请参考以下文章

大三Java后端暑期实习面经总结——JVM篇

大三Java后端暑期实习面经总结——Java基础篇

大三Java后端暑期实习面经总结——Java基础篇

大三Java后端暑期实习面经总结——Java容器篇

大三Java后端暑期实习面经总结——Java容器篇

大三Java后端暑期实习面经总结——Java多线程并发篇