# 常见面试题目

## 一、JVM基础知识

Java内存区域

整理了n多遍了，但是由于JVM规范是相对宽松的，HotSpot虚拟机并不是一板一眼的按照JVM推荐进行实现，加上版本的更迭，难免会出现混乱的情况。

![img](https://gitee.com/SnailClimb/JavaGuide/raw/master/docs/java/jvm/pictures/java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/JVM%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9F.png)

![img](https://gitee.com/SnailClimb/JavaGuide/raw/master/docs/java/jvm/pictures/java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/2019-3Java%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9FJDK1.8.png)

继续来总结一遍吧，总是喜欢考

线程私有的：

程序计数器，用于存储下一条需要执行指令的位置，可以用于控制程序的执行，如顺序执行，循环执行，还可以用来在线程发生上下文切换时候记录下该线程再次分配到CPU时间片的时候应该执行什么指令，从哪儿开始执行

Java虚拟机栈，也就是我们常说的栈，用于存放方法执行的栈帧，每进行一次方法调用就会进行一次栈帧的入栈操作，栈帧中存储有局部变量表，方法出口等信息

本地方法栈：类似于Java虚拟机栈，只不过这里是存储的本地方法执行后的栈帧，HotSpot虚拟机将其融合到了Java虚拟机栈当中去，这里提出的本地方法栈是JVM给出的一个规范，并未强制要求虚拟机如何实现

线程共享的：

堆：用于存储对象实例，几乎所有的对象实例都是直接在堆上的，是GC回收的主要区域，因此也被称为GC堆，更进一步的划分可以分为新生代和老年代，新生代又可以被划分为一个Eden和两个Survivor，比例为8：1

方法区：存放虚拟机加载的类信息，常量，静态变量，即时编译器编译出的代码等信息，在Java6时候的HotSpot的实现为永久代，永久代是直接占据部分堆内存，因此可能造成内存泄漏等问题，在Java8时候的实现为元空间，占据的是直接内存，减少了内存泄露问题出现的可能性，同时不受Java堆大小的控制，减少发生OOM的概率

> 运行时常量池跟随方法区移动，属于方法区的一部分
>
> 字符串常量池则一直处在堆当中

直接内存：并不在JVM规范当中，但也频繁的被使用到，如1.4引入的NIO的Buffer就使用到了直接内存，主要是避免了系统从用户态到核态的频繁切换带来的开销，还有就是Native堆和JVM堆的数据来回复制开销。

![img](https://img-blog.csdn.net/20161019204824590?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)

**当支持动态扩展的时候报StackOverflowError，不支持动态扩展且达到了最大内存报OutOfMemoryError**

对象的创建过程

![Java创建对象的过程](https://gitee.com/SnailClimb/JavaGuide/raw/master/docs/java/jvm/pictures/java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/Java%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%BF%87%E7%A8%8B.png)

1、到方法区中查看这个类是否被加载过，如果没有被加载过就去加载

2、如果类加载检查通过后就可以在堆中划分一块内存来存储对象了，具体大小在读取类信息的时候就已经定了

> 对内存的分配则取决于JVM的垃圾收集器了
>
> （内存规整的情况）可能通过指针碰撞的方式：这时候的堆内存模型为一边是使用过的内存，一边是没有使用过的内存，中间由指针隔开，此时分配内存只需要拨动指针，完成对象内存的分配即可
>
> （内存不规整的情况）此时的内存模型为JVM维护一个可用队列记录着可用内存快的大小，从中分配一块大小近似的即可。

Java在创建对象的时候需要保证是线程安全的且高效的，采用了初始化锁的机制，实现为CAS，主要通过判断预分配的Eden内存是否改变了来判断是否有线程安全问题

3、初始化零值

完成内存分配后，开始将分配到的内存数据清零，这样可以保证在不赋值的情况下基本数据类型使用默认值，引用类型为null

4、设置对象头，完成对象头信息的设置，如：对象的类信息（指针），哈希码，锁的状态，GC年代信息等等

5、执行初始化方法，主要包含构造函数和代码块的执行

对象的内存布局：对象头、实例数据和填充对齐，GC年代

对象头中包含：对象的类信息（指针指向）、哈希码、MarkWord信息

实例数据：对象的有效信息如定义的各种非静态字段

对齐填充：更利于对象的管理，要求对象占用内存为8字节的整数倍

对象的访问方式：

主要有两种：句柄和指针

句柄就是对象的指针的指针，各有优劣，Java使用指针访问，速度快，句柄的优势是在于它的稳定性，只需要改变对象的指针即可，句柄信息不用改变。

字符串常量的简单回顾

```java
String a = "222";
String b = new String("111");
String c = a.intern();
```

主要就这几种形式：第一种直接赋值，在字符串常量池中获取，如果没有则创建，返回一个指向字符串常量池的指针，第二种而是创建一个或者两个对象，取决于字符串是否存在于字符串常量池当中，返回指向堆的一个引用，第三种是返回对象的字符串常量池引用，如果没有则创建（一般都是有的，因为都已经存在一个字符串引用了）。

至于其他的基本数据类型的常量池，则是直接存储于方法区当中的，可以在valueof方法中看到

一般（Byte，Short，Long，Integer）都是-128到127，char为0到127，Boolean为TRUE和FALSE，float和Double则没有。

JVM的常见运行参数指定：

`-Xms`：堆的最小内存，也就是初始化堆内存

`-Xmx`：堆的最大内存

```
-Xms2G -Xmx5G
```

`-XX:NewSize`：新生代内存的最小值，也就是初始化新生代内存大小

`-XX:MaxNewSize`：新生代内存最大大小

```
-XX:NewSize=256M -XX:MaxNewSize=1024M
```

如果不想让新生代扩容，可以指定两个值为一样的，也可以使用`-Xmn`

```
-Xmn256M
```

`-XX:NewRatio`：新生代和老年代的比值

```
-XX:NewRatio=1
```

`-XX:MetaspaceSize`：元空间的初始化大小

`-XX:MaxMetaspaceSize`：元空间最大的大小

![项目中垃圾回收器常用配置](https://gitee.com/SnailClimb/JavaGuide/raw/master/media/pictures/jvm/java_jvm_suggest_parameters.png)

**GC调优思路：到万不得已的时候才会进行GC调优，一般都是通过GC日志信息来分析Java代码哪里写的不合理，从而解决问题。目的是将转移到老年代的对象数量降至最低，减少GC的执行时间**

## 二、JVM垃圾回收

操作的对象：Java堆

![img](https://gitee.com/SnailClimb/JavaGuide/raw/master/docs/java/jvm/pictures/jvm%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png)

经过一次垃圾回收，存活对象的GC年代信息加一，直到达到一个阈值进入老年代，小对象优先分在Eden区，大对象直接进入老年区。

当Eden区没有空间时候发生一次Minor GC。

GC的分类：

* 部分收集（Partial GC）
  * 新生代收集（Minor/Young GC）：只对新生代进行收集
  * 老年代收集（Major/Old GC）：只对老年代进行收集
  * 混合收集（Mixed GC）：对新生代和老年代进行收集
* 整堆收集（Full GC）：整个堆和方法区

要收集哪一些对象？

收集一些无效的对象，两种方法：引用计数法和可达性分析

引用计数法：当对象被引用，引用计数器+1，取消引用就减一。效率高，但是没被使用，因为无法解决循环引用这个问题。

可达性分析：从GC Roots为起点向下搜索，进行对象的可达性分析，不可达的对象会被回收掉

可以作为GC Roots的对象：

* 虚拟机栈和本地方法栈中本地变量表所引用的对象（方法中的引用）
* 方法区中类静态属性引用的对象（类中的静态属性）
* 方法区中常量引用的对象（类中的final属性）

引用类型

强引用——软引用——弱引用——虚引用

垃圾收集器绝对不会回收强引用

垃圾收集器在空间足够的时候不会回收软引用，但是在缺乏空间的时候会进行回收

垃圾收集器只要扫描到弱引用就会回收（ThreadLocalMap中的Key使用的就是弱引用）

虚引用顾名思义和形同虚设一样，如果一个对象持有虚引用，就和没有任何引用是一样的，虚引用主要是用来跟踪对象被垃圾回收的活动

被标记为不可达对象不一定立马就会被回收，还会执行finalize方法（如果覆盖了的话），此时如果对象可以通过finalize方法重新连接到GC Roots上，那么就不会被回收

垃圾收集算法：标记-清除、复制算法、标记-整理、分代收集算法

标记-清除算法：标记需要清理的然后就地进行清理，问题：会产生大量的空间碎片

复制算法：只使用其中一半，发生GC时候将存活对象移动到另一半，问题：效率低（需要对对象进行大量复制） 2、资源利用率低（50%）

标记-整理算法：标记存活对象，将其挪至内存一端，清理剩下的空间。

分代收集算法：就是对每一个地方使用不同的算法，如在新生代中最终只会有少量对象存活，复制成本低，可以使用复制算法，老年代的存活率高，我们需要使用标记-整理算法来腾出空间来存放对象

垃圾收集算法实现：垃圾收集器

|      垃圾收集器名字      |     是否会STW     |             是否是单线程             |                   收集算法                   |           特性           |
| :---------------: | :------------: | :----------------------------: | :--------------------------------------: | :--------------------: |
|       Serial      | 会一直STW直到垃圾收集结束 |                是               |                   标记-整理                  |        历史最悠久的收集器       |
|       ParNew      | 会一直STW直到垃圾收集结束 |                否               |          新生代采用复制算法，老年代采用标记-整理算法          |     就是Serial的多线程版本     |
| Parallel Scavenge | 会一直STW直到垃圾收集结束 |                否               |          新生代采用复制算法，老年代采用标记-整理算法          |          强调吞吐量         |
|     Serial Old    | 会一直STW直到垃圾收集结束 |                是               |                     /                    |       Serial老年代版本      |
|    Parallel Old   | 会一直STW直到垃圾收集结束 |                否               |                     /                    | Parallel Scavenge老年代版本 |
|        CMS        |     较其他来说短     | 完全是（**基本上**实现了垃圾收集器线程与用户线程的并行） |                  标记-清除算法                 |         强调停顿时间         |
|         G1        |  较其他来说短，比CMS长  |               完全是              | 去除老年代的概念，将内存划分为一个个Region，每次回收最有价值的Region |  时间段，吞吐量高，Java9的默认收集器  |

JDK的常见工具（在bin目录下）

常见工具（在bin文件夹下）：

* jps：类似于Linux下的ps命令，查看所有Java进程
* jstat：收集HotSpot的运行数据
* jinfo：显示虚拟机配置信息
* jmap：生成堆快照
* jstack：生成线程快照

## 三、class文件结构及类加载过程

![类文件字节码结构组织示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E7%B1%BB%E6%96%87%E4%BB%B6%E5%AD%97%E8%8A%82%E7%A0%81%E7%BB%93%E6%9E%84%E7%BB%84%E7%BB%87%E7%A4%BA%E6%84%8F%E5%9B%BE.png)

基本上都是照搬《深入理解Java虚拟机》这本书的了，看自己笔记就行

un表示n个字节

|           名称          |                     解释                    |        占位        |           数量          |
| :-------------------: | :---------------------------------------: | :--------------: | :-------------------: |
|         magic         |             魔数，以此确定是否为class文件             |        u4        |           1           |
|     minor\_version    |                    副版本号                   |        u2        |           1           |
|     major\_version    |                    主版本号                   |        u2        |           1           |
| constant\_pool\_count |             常量池计数器，后面常量池的元素个数             |        u2        |           1           |
|     constant\_pool    |              常量池，其中存放cp\_info             |     cp\_info     | constant\_pool\_count |
|     access\_flags     | 访问标志，查看当前class是类还是接口，是否标注了final，abstract等 |        u2        |           1           |
|      this\_class      |                  该类的全限定名                  |        u2        |           1           |
|      super\_class     |                  父类的全限定名                  |        u2        |           1           |
|   interfaces\_count   |                  实现接口的数量                  |        u2        |           1           |
|       interfaces      |        具体实现接口的全限定名，顺序为implement的顺序        |        u2        |   interfaces\_count   |
|     fields\_count     |              包含静态变量和非静态变量的个数              |        u2        |           1           |
|         fields        |              存放一个个field\_info             |    field\_info   |     fields\_count     |
|     methods\_count    |              包括静态方法和非静态方法的个数              |        u2        |           1           |
|        methods        |             存放一个个method\_info             |   method\_info   |     methods\_count    |
|   attributes\_count   |                   属性表数量                   |        u2        |           1           |
|       attributes      |            属性表用于存放前面的字段表和方法表的信息           | attributes\_info |   attributes\_count   |

:zap:**类加载过程**

![img](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B-%E5%AE%8C%E5%96%84.png)

这是整个类的生命周期，只有前面的加载——连接——初始化属于类加载过程

第一步：加载

1、通过全类名获取此类的二进制文件流（ZIP也行，也就是后面的JAR、WAR基础）

2、将字节流所代表的静态存储结构转换为方法区的运行时数据结构

3、在内存中生成一个代表该类的Class对象，作为方法区这些数据的访问入口

简而言之：将class文件从硬盘上转入内存中，创建一个Class实例化对象指向这片内存

> 我们可以通过重写类加载器的loadclass方法来实现类的加载
>
> ```java
> public Class<?> loadClass(String name);
> ```

会涉及到类加载器，双亲委派模型的知识点，明天再总结

2、连接

加载和连接过程往往是交叉进行的，加载阶段尚未结束可能连接阶段已经开始了

分为三个阶段，三个阶段也是穿插执行，如验证阶段会验证解析的结果是否符合规范

1、验证：主要验证class文件格式符合Java虚拟机规范（魔数，版本号等信息），保证class运行后不会损坏JVM自身，会验证解析阶段将符号引用转换为直接引用的时候的校验

2、准备：为静态变量分配空间并执行置零操作，如果变量被final修饰则会直接置为指定的值

3、解析：将常量池中的符号引用（相当于偏移量）转换为直接引用（转换为直接地址）的过程，主要是针对类，接口，字段，方法的符号引用的转换

3、初始化

类加载的最后一步，类的初始化，主要是init方法的调用

类的初始化也有点懒加载的味道了，**只有**在以下几种情况下对类进行初始化（很少见的词汇了，因为JVM规范一般没有做如此严格的限定）：

1、第一次执行new的时候

2、访问类的静态变量的时候

3、给类的静态变量赋值

4、调用类的静态方法

5、初始化一个子类，如果父类没有初始化需要进行初始化

6、默认主类会被初始化

4、卸载

不在类加载过程当中了，卸载等价于Class对象被GC了

需要同时满足如下三个需求：

1、所有实例对象已经GC

2、Class没有在任何其他地方被引用

3、类加载器已经被GC

因此，出现类被GC的情况很少，因为JDK自带的ClassLoader是一定不会被GC的，必不满足第三条，因此通常只有我们自定义的类加载器加载的类可能发生GC。

## 四、类加载器和双亲委派模型

类加载的三个步骤：加载——连接（验证、准备、解析）——初始化

类加载器的作用就是将.class文件加载到内存当中

JVM内置的三个类加载器：

BootstrapClassLoader（启动类加载器），不在JDK 中，是使用C++实现的，加载`JAVA_HOME/lib`目录下的jar包或者是class文件

ExtensionClassLoader（扩展类加载器），主要负责加载`JRE_HOME/lib/ext`目录下的JAR包和class文件的加载

AppClassLoader（应用程序类加载器），负责加载当前classpath下的jar包和class文件

![ClassLoader](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/classloader_WPS%E5%9B%BE%E7%89%87.png)

**双亲委派模型：**

**得先从类加载器说起，JVM内置有三层类加载器，这三层之间不是通过继承来实现功能复用的，而是通过组合的方式实现功能复用的，最上层的BootStrapClassLoader是使用C++来实现的，主要加载JAVA\_HOME/lib下的JAR包和class文件，第二层的ExtensionClassLoader是第二层，负责对JRE\_HOME/lib/ext的JAR包和class文件进行加载，第三层的AppClassLoader负责加载classpath下的JAR包和class文件。我们自己实现的类加载器就都处于第四层，自定义类加载规则，当需要加载类的时候，会先从下往上进行查找是否加载过了，如果加载过了就直接返回，如果没有加载就自上而下地尝试去加载类。**

这里的双亲更多指的是父辈与子辈之间的关系

使用双亲委派模型可以保证JVM的稳定，类不会被重复加载。

> 自定义类加载器，继承classLoader，如果不想打破双亲委派模型，需要实现ClassLoader类中的findClass方法，如果想要打破双亲委派模型，重写loadClass方法。
>
> 因为loadclass方法里面存在着双亲委派模型的代码，在其中会调用findclass，相当于是一种模板模式，如果我们覆盖了loadclass的默认实现就破坏了双亲委派模型。

## 五、总线嗅探机制和总线风暴

主要是volatile的弊端，而JUC包里面的核心AQS使用到了volatile，所以RentrantLock也会存在这个问题，面试时候可以着重回答。

总线风暴具体的体现就是总线带宽飙升

**每个线程都有一个自己的工作内存，为了保证工作内存的变量的可见性，引入了缓存一致性协议，而其中的MESI（Intel提出的）就是其中的佼佼者，每个线程需要通过嗅探器来保证volatile变量是否过期，在频繁修改CAS和volatile的时候就会产生大量的消息，从而导致总线带宽飙高，总线风暴问题产生。**
