2019.09.26 面试题总结,基础决定走多远。Persist!

一、JVM学习

1,JVM内存模型?

  • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存
  • 虚拟机栈:Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息 存储局部变量
  • 本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。
  • 方法区:存储已被虚拟机加载的类元数据信息(元空间) 类信息、常量、静态域
  • 程序计数器:当前线程所执行的字节码的行号指示器

https://my.oschina.net/u/2935389/blog/3038822 这个大神写的炒鸡好啊!!!



2,JVM类加载机制?

  • 1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
  • 2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
  • 3)App ClassLoader:负责记载classpath中指定的jar包及目录中class

工作过程:

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  • 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上

好处:

  • 防止内存中出现多份同样的字节码(安全性角度)

特别说明:

  • 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载

类加载过程(加载-->连接(验证,准备,解析)-->初始化)

3,String s = "aaa",类似这些题目?

  • 常量池位于堆中
  • 运行时常量池位于堆中
  • 字符串常量池位于堆中
  • 如果常量池中存在当前字符串,那么直接返回常量池中它的引用
  • 如果常量池中没有此字符串, 会将此字符串引用保存到常量池中后, 再直接返回该字符串的引用
  • 类加载后,常量池中的数据会在运行时常量池中存放

常量池存储的是:

  • 字面量(Literal):文本字符串等---->用双引号引起来的字符串字面量都会进这里面
  • 符号引用(Symbolic References)

常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放--->来源:深入理解Java虚拟机 JVM高级特性与最佳实践(第二版)

现在我们的运行时常量池只是换了一个位置(原本来方法区,现在在堆中),但可以明确的是:类加载后,常量池中的数据会在运行时常量池中存放

HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容

字符串常量池只存储引用,不存储内容

再来看一下我们的intern方法:

* When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.

*当调用intern方法时,如果池已包含
*字符串等于此{@code string}对象,由
*{@link equals(object)}方法,则池中的字符串是
*返回。否则,此{@code string}对象将添加到
*返回池和对此{@code string}对象的引用。

以下面试题引用自大神https://my.oschina.net/u/2935389 群星纪元。 

二、JVM面试题

拿些常见的JVM面试题来做做,加深一下理解和查缺补漏

  • 1、详细jvm内存模型
  • 2、讲讲什么情况下回出现内存溢出,内存泄漏?
  • 3、说说Java线程栈
  • 4、JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
  • 5、JVM 出现 fullGC 很频繁,怎么去线上排查问题?
  • 6、类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式?
  • 7、类的实例化顺序
  • 8、JVM垃圾回收机制,何时触发MinorGC等操作
  • 9、JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的
  • 10、各种回收器,各自优缺点,重点CMS、G1
  • 11、各种回收算法
  • 12、OOM错误,stackoverflow错误,permgen space错误

题目来源:

2.1详细jvm内存模型

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

具体可能会聊聊jdk1.7以前的PermGen(永久代),替换成Metaspace(元空间)

  • 原本永久代存储的数据:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap
  • Metaspace(元空间)存储的是类的元数据信息(metadata)
  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
  • 替换的好处:一、字符串存在永久代中,容易出现性能问题和内存溢出。二、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

图片来源:https://blog.csdn.net/tophawk/article/details/78704074

参考资料:

2.2讲讲什么情况下回出现内存溢出,内存泄漏?

内存泄漏的原因很简单:

  • 对象是可达的(一直被引用)
  • 但是对象不会被使用

常见的内存泄漏例子:

public static void main(String[] args) {

        Set set = new HashSet();

        for (int i = 0; i < 10; i++) {
            Object object = new Object();
            set.add(object);

            // 设置为空,这对象我不再用了
            object = null;
        }

        // 但是set集合中还维护这obj的引用,gc不会回收object对象
        System.out.println(set);
    }

解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上诉内存泄漏问题了。其他内存泄漏得一步一步分析了。

内存泄漏参考资料:

内存溢出的原因:

  • 内存泄露导致堆栈内存不断增大,从而引发内存溢出。
  • 大量的jar,class文件加载,装载类的空间不够,溢出
  • 操作大量的对象导致堆内存空间已经用满了,溢出
  • nio直接操作内存,内存过大导致溢出

解决:

  • 查看程序是否存在内存泄漏的问题
  • 设置参数加大空间
  • 代码中是否存在死循环或循环产生过多重复的对象实体、
  • 查看是否使用了nio直接操作内存。

参考资料:

2.3说说线程栈

这里的线程栈应该指的是虚拟机栈吧...

JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。

当方法调用的时候,会生成一个栈帧。栈帧是保存在虚拟机栈中的,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素

通过jstack工具查看线程状态

参考资料:

2.4JVM 年轻代到年老代的晋升过程的判断条件是什么呢?

  1. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
  2. 如果对象的大小大于Eden的二分之一会直接分配在old,如果old也分配不下,会做一次majorGC,如果小于eden的一半但是没有足够的空间,就进行minorgc也就是新生代GC。
  3. minor gc后,survivor仍然放不下,则放到老年代
  4. 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代

2.5JVM 出现 fullGC 很频繁,怎么去线上排查问题

这题就依据full GC的触发条件来做:

  • 如果有perm gen的话(jdk1.8就没了),要给perm gen分配空间,但没有足够的空间时,会触发full gc。

    - 所以看看是不是perm gen区的值设置得太小了。

  • System.gc()方法的调用

    - 这个一般没人去调用吧~~~

  •  当统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间,则会触发full gc(这就可以从多个角度上看了)

    - 是不是频繁创建了大对象(也有可能eden区设置过小)(大对象直接分配在老年代中,导致老年代空间不足--->从而频繁gc)     - 是不是老年代的空间设置过小了(Minor GC几个对象就大于老年代的剩余空间了)

2.6类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式?

双亲委托模型的重要用途是为了解决类载入过程中的安全性问题

  • 假设有一个开发者自己编写了一个名为java.lang.Object的类,想借此欺骗JVM。现在他要使用自定义ClassLoader来加载自己编写的java.lang.Object类。
  • 然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在Bootstrap ClassLoader的路径下找到java.lang.Object类,并载入它

Java的类加载是否一定遵循双亲委托模型?

  • 在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制。
  • SPI就是打破了双亲委托机制的(SPI:服务提供发现)。SPI资料:

    - https://zhuanlan.zhihu.com/p/28909673     - https://www.cnblogs.com/huzi007/p/6679215.html     - https://blog.csdn.net/sigangjun/article/details/79071850

参考资料:

2.7类的实例化顺序

  • 1. 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
  • 2. 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
  • 3. 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
  • 4. 父类构造方法
  • 5. 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
  • 6. 子类构造方法

检验一下是不是真懂了:

class Dervied extends Base {


    private String name = "Java3y";

    public Dervied() {
        tellName();
        printName();
    }

    public void tellName() {
        System.out.println("Dervied tell name: " + name);
    }

    public void printName() {
        System.out.println("Dervied print name: " + name);
    }

    public static void main(String[] args) {

        new Dervied();
    }
}

class Base {

    private String name = "公众号";

    public Base() {
        tellName();
        printName();
    }

    public void tellName() {
        System.out.println("Base tell name: " + name);
    }

    public void printName() {
        System.out.println("Base print name: " + name);
    }
}

输出数据:

Dervied tell name: null
Dervied print name: null
Dervied tell name: Java3y
Dervied print name: Java3y

第一次做错的同学点个赞,加个关注不过分吧(hahaha

2.8JVM垃圾回收机制,何时触发MinorGC等操作

当young gen中的eden区分配满的时候触发MinorGC(新生代的空间不够放的时候).

2.9JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的

这题不是很明白意思(水平有限...如果知道这题的意思可在评论区留言呀~~)

  • 因为按我的理解:执行fgc是不会执行ygc的呀~~

YGC和FGC是什么 

  • YGC :对新生代堆进行gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。
  • FGC :全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。以我们生产环境为例,一般比较少会触发fgc,有时10天或一周左右会有一次。

什么时候执行YGC和FGC

  • a.eden空间不足,执行 young gc
  • b.old空间不足,perm空间不足,调用方法System.gc() ,ygc时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gc

2.10各种回收算法

GC最基础的算法有三种:

  • 标记 -清除算法
  • 复制算法
  • 标记-压缩算法
  • 我们常用的垃圾回收器一般都采用分代收集算法(其实就是组合上面的算法,不同的区域使用不同的算法)。

具体:

  • 标记-清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
  • 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

2.11各种回收器,各自优缺点,重点CMS、G1

图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用.

  • Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,但可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本
  • Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量
  • Parallel Old收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担。CMS无法处理浮动垃圾。CMS的“标记-清除”算法,会导致大量空间碎片的产生
  • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

2.12stackoverflow错误,permgen space错误

stackoverflow错误主要出现:

  • 在虚拟机栈中(线程请求的栈深度大于虚拟机栈锁允许的最大深度)

permgen space错误(针对jdk之前1.7版本):

  • 大量加载class文件
  • 常量池内存溢出


​​​​​​