2021春招Java面试题大全(精华二)

返回首页

11.JVM的三大垃圾回收算法

  • 引用计数法:给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
    • 缺陷不能解决循环引用
    • 维护一个计数器有一定的消耗

可达性分析:从GCRoots对象开始作为起点进行链路扫描,如果可以到达某个对象,那么这个对象就是存活的,还不能被回收,如果不可达,那这个对象就是死亡的。

Java中可以作为GCRoots的对象

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象
  • 标记-清除算法
    • 标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。

    • 详解:就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停(Stop The World),随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行

    • 缺点:

      • 效率低(需要对全堆对象遍历,遍历两次),导致STW时间长。
      • 清理出来的空闲内存是不连续的,造成的内存碎片比较多。
  • 复制算法(新生代的GC算法,YoungGC/MinorGC采用的就是这种算法)(用空间换时间)

    复制算法就是指,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
    但是年轻代中的对象都是朝生夕死的,没必要按照一比一的比例划分空间,而是将内存空间划分为较大的eden区和两个较小的survivor区。每次新创建的对象都会被放进eden区,当eden区被填满了,会触发younggc(采用复制算法),这会将eden区和from区中存活的对象复制到to区,然后将垃圾清除,然后from和to交换角色,反复这样,直到to区被放满了。会将存活的对象移至老年代中

    • 这种算法解决了内存碎片化的问题,运行高效,但是代价就是将内存缩小为原来的一半了
  • 标记-整理算法(老年代的GC算法,FullGC/MajorGC)
    • 首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。
    • 标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点(内存碎片),也消除了复制算法当中,内存减半的高额代价。
      • 但是,标记/整理算法唯一的缺点就是效率也不高。
  • 分代收集算法
    • 只是根据对象存活周期的不同将 java 堆内存划分为几块(新生代、老年代),根据各个年龄代的特点采用最适当的收集算法。在新生代中,对象的存活率低,一般采用复制算法进行回收。在老年代中,对象的存活率高,一般采用标清或标整。
    • 没有最好的算法,只能根据每一代的垃圾收集的特性用特定的算法。即,不同的代用不同的算法,新生代用复制算法,老年代用标清或标整
  • STW

    • 全局停顿,所有Java代码停止多半情况下是由于GC引起
      • 为什么非要停止程序的运行呢?
        • 假设我们刚标记完图中最右边的那个对象,暂且记为A,结果此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0,因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。
        • 避免无法彻底清理干净(打个比方:类比在聚会,突然GC要过来打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。)
        • GC的工作必须在一个能确保一致性的快照中进行。

内存碎片就是:在清理内存时,由于被使用的内存是连续的,但是要销毁的对象一定不是按顺序销毁的,是随机的。这种连续的排序,但是随机的清除的过程就会产生内存碎片。

拓展

Java强引用

  • 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
public class Test {
     
     
    public static void main(String[] args) {
     
     
        Object obj1 = new Object();
        Object obj2 = obj1;
        obj1 = null;
        System.gc();
        System.out.println(obj1); //null被回收了
        System.out.println(obj2); //java.lang.Object@74a14482 没有被回收,因为obj2是强引用
    }
}

JAVA软引用

  • 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存。

JAVA弱引用

  • 弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

软引用和弱引用的适用场景:

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中又可能造成内存溢出

此时使用软引用可以解决这个问题。

设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

image-20200821173147240

WeakHashMap,如果他的key值变为null了,只要一gc就会被回收,腾出内存空间。

JAVA虚引用

  • 虚引用需要 PhantomReference 类来实现,被虚引用引用的对象,他跟没有任何引用一样,在任何时候都可能被垃圾收集器回收。它不能单独使用,必须和引用队列联合使用。

对于软弱虚引用,都有一种机制,就是他们在被gc时,会被回收,但是在回收之前会先被放在引用队列里面。

12.垃圾回收器的种类(他们就是对GC算法的应用)

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。

年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年轻、老年代范畴内]

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

image-20200712103721780

Minor GC和Full GC(Major GC):

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

吞吐量:= 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)

新生代GC(YoungGC)
Serial收集器
  • 是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。Stop The World
  • 是虚拟机运行在Client模式下的默认新生代收集器
  • 由于没有线程交互的开销,专心做垃圾收集,所以效率高。但是速度慢
  • 对应的配置参数:-XX:+UseSerialGC
  • 开启后会使用:Serial+SerialOld的收集器组合
ParNew收集器
  • 就是Serial收集器的多线程版本
  • 运行在Server模式下的虚拟机中首选的新生代收集器,可以和cms老年代回收期配合使用。当old代采用CMS GC时new代默认采用ParNew
  • 也需要STW
  • 对应配置参数:-XX:+UseParNewGC
Parallel Scavenge收集器
  • 它也是使用复制算法的收集器,又是并行的多线程收集器,server模式下的默认GC方式
  • 他注重的是高吞吐量(但是他不注重响应时间,比如:运行用户代码6000分钟,但是垃圾收集6分钟,这时吞吐量是相当高的。但是如果stw这6分钟恰巧被我们赶到了,我们就很不爽。想想我们打王者,为什么总要停服更新,就是这个原理),主要适合在后台运算而不需要太多交互的任务。
  • CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
  • 它具有自适应调节策略(即不用过多的配置参数)

老年代GC
Serial Old收集器
  • Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。每次进行全部回收,进行Compact,非常耗费时间。
  • client模式下的默认GC方式

Parallel Old收集器

  • Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。同样提供吞吐量优先的垃圾收集器
  • server模式下的默认GC方式
CMS收集器

一般情况下ParNew(Young区使用)+CMS(old区使用)+Serial Old组合使用,Serial Old作为CMS出错的后备处理器

  • CMS收集器是基于“标记—清除”算法实现(速度快,但是会产生内存碎片)的,这种GC是一种以获取最短回收停顿时间)(响应时间短)为目标的gc,他不太注重吞吐量

    • A、初始标记:初始标记仅仅只是标记一下GC Roots能直接关联的对象,速度很快,需要“Stop The World”。

      B、并发标记:从第一次标记的对象出发,并发的标记可达对象,和用户线程一起工作,不需要暂停工作线程。

      C、重新标记:(防止一些对象在第一次标记为需要被回收,但是第二次标记又不能被回收了)重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,仍然需要“Stop The World”。

      D、并发清除:并发清除阶段会清除GC Roots不可达对象,和用户线程一起工作。

      由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    • 开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC

  • 优点:并发收集低停顿,并发指与用户线程一起执行。

  • 缺点:

    • 并发执行,对CPU资源压力大
      • 由于并发进行,CMS在收集与应用程序会同时会增加对对内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。
    • 采用标记清除算法会产生大量碎片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkNaLcG8-1614657220096)(/home/zaq/.config/Typora/typora-user-images/image-20200712103829321.png)]

G1GC

在这里插入图片描述

背景:CMS垃圾收集器虽然减少了暂停应用的运行时间,但是他还是存在内存碎片的问题。于是为了除去内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,引出了G1GC。

他的主要改变是:Eden、Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region。每个region从1M到32M不等,一个region可能属于Eden,Survivor或者Tenured内存区域。

特点:

  • 充分利用CPU、多核环境的优势,尽量缩短STW
  • 整体采用标记整理算法,局部通过复制算法,不会产生内存碎片
  • 把内存划分为多个独立的子区域,在小范围内进行老年代和年轻代的分区,依然采用分代收集
  • 没个分区的角色会随运行不断变化。

底层

G1收集器其实也可以称为:Region区域化垃圾收集器。他的最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

核心思想是将堆内存划分为了大小相同的子区域(默认是2048个分区),在JVM启动的时候会自动设置子区域的大小。每个区域可以按照需要在年轻代和老年代之间进行切换。

他还属于分代收集器,这些Region中一部分包含新生代,这里的垃圾收集依然要STW,将存活的对象拷贝到老年代或者Survivor区。而老年代中,G1收集器会把对象从一个区域复制到另外一个区域,完成清理工作。这就不会造成内存碎片了
回收过程

  • 初始标记:只标记GCRoots直接关联的对象
  • 并发标记:进行GC跟踪的过程,就是从上次标记的对象开始标记可达对象
  • 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  • 筛选回收:根据时间来进行价值最大的回收
与CMS比较:
  • G1不会产生很多的内存碎片
  • G1的Stop The World更可控,G1在停顿时间上添加了预测机制,用户可以制定期望停顿时间

垃圾回收的方式
  • 串行回收(串行垃圾回收器Serial)
    • 他为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合 服务器环境
    • 会导致STW
  • 并行回收(并行垃圾收集器Parallel )
    • 多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等若交互场景
    • 会导致STW
  • 并发标记清除回收(并发垃圾回收器CMS)
    • 用户线程和垃圾线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。常用,适用对响应时间有要求的场景
  • G1
    • G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
如何选择垃圾收集器
  • 单CPU或小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后台计算型应用(与前台交互少,可以允许一些停顿)
    • -XX:+UseParallelGC或者-XX:+UseParallelOldGC(并且这两个是直接相关联的,是一个组合)
  • 多CPU,追求低停顿时间,需要快速响应如互联网应用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC(这两个是一个组合)

在这里插入图片描述

MinorGC和FullGC是一个动作他们使用的是GC算法,Serial等七种是做MinorGC或FullGC的工具(他们来执行MinorGC或FullGC这个动作),当eden区被放慢了会触发MinorGC,触发了以后会用Serial等进行单线程多线程的方式来回收垃圾。

13.三大范式

第一范式:确保每列原子性,即每列都不可再拆分

如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。

第二范式:确保表中的每列都和主键相关

第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。

第三范式:确保每列都和主键列直接相关,而不是间接相关

数据不能存在传递关系,即没个属性都跟主键有直接关系而不是间接关系。像:a–>b–>c 属性之间含有这样的关系,是不符合第三范式的

注意事项:

第二范式与第三范式的本质区别:在于有没有分出两张表。

第二范式是说一张表中包含了多种不同实体的属性,那么必须要分成多张表,第三范式是要求已经分好了多张表的话,一张表中只能有另一张标的ID,而不能有其他任何信息,(其他任何信息,一律用主键在另一张表中查询)。

14.B树

  • B树中所有节点的孩子节点数中的最大值称为B树的阶,记为M

  • 根节点的关键字数量范围:1<=k<=m-1,非根节点的关键字数量范围:m/2<=k<=m-1

  • 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。

  • 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。

  • 每个节点都存有索引和数据,也就是对应的key和value。(value指的就是这一行的数据或这行数据的地址(具体跟是否是聚簇索引有关))

15.B+树

与B树的相同点

  • 根节点只有一个元素
  • 非根节点的关键字数量范围:m/2<=k<=m-1

不同点

  • B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。
  • 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
  • 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。(有了这个指针我们就支持了范围查询了)

额外:B+ 树中,数据对象的插入和删除仅在叶节点上进行。

总结:B+树相对于B树有一些自己的优势,可以归结为下面几点。

  • 单一节点存储的元素更多(即,B+树中间节点没有Data数据,所以同样大小的磁盘页可以容纳更多的节点元素。所以数据量相同的情况下,B+树比B树更加“矮胖“),使得查询的IO次数更少(即便有千万条数据我们只需要进行3次IO就可以找到数据了),所以也就使得它更适合做为数据库MySQL的底层数据结构了。
  • 所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定(最好的情况是查询根节点,最坏查询叶子节点)。
  • B树范围查询只能通过中序遍历查询来定位最小和最大值,而B+树通过链表就能实现,查询更方便。

比起B树,B+树 ①IO次数更少 ②查询性能很稳定 ③范围查询更简便

16.HashMap的加载因子是多少?每次扩容多少,为什么?

加载因子是0.75,每次扩容都是2的倍数,因为HashMap底层中,扩容时会调用一个resize方法,这个方法中可以看出他会创建一个新的数组,并且将旧数组的元素经过e.hash&(newCap-1)的计算方法添加到新数组中,这个&是位运算,效率非常高,按位与的计算过程就是一假则假,当HashMap的容量为2的幂时,newCap-1的二进制就是全1的这种形式,这样与添加的元素的hash值进行位运算能够充分散列,让添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

17.线程状态变迁图

在新建状态时调用start()方法后线程进入就绪态,进入到等待队列等待CPU,得获得到CPU的执行权后线程进入到运行态。在运行态的线程等到时间片用完了或者调用yield方法后就会转为就绪态。在运行态调用sleep、wait、join或者等待同步锁的时候就会进入阻塞态,等到sleep时间到,join结束,某个线程调用notify或者notifyAll方法,或者获得到同步锁,线程就会进入就绪态。等线程的run方法或者Main执行结束,或者发生异常,线程进入死亡状态。

18.TCP状态迁移图

首先客户端和服务器都处于Closed状态,客户端主动打开,服务器被动打开进入LISTEN状态,客户端先发送一个SYN包给服务器,然后他的状态变为SYN_SENT状态,服务器接收到SYN后返回一个SYN+ACK包,然后自己的状态变为SYN_RECD状态,客户端接受到SYN+ACK后状态变ESTABLISHED状态,然后返回一个ACK确认收到,服务器收到后也变为ESTABLISHED状态,这是就将链接建立成功了。当客户端想要断开链接时会发送一个FIN请求,表示不会再发送数据了,然后状态变为FIN_WAIT_1,然后服务器接收到FIN后状态变为CLOSE_WAIT状态,并且返回一个ACK表示收到了请求,然后客户端接收到这个ACK后状态变为FIN_WAIT_2,等到服务器把自己要发送的数据都发送完了以后会返回一个FIN给客户端,自己的状态变为LAST_ACK,客户端接收到FIN后状态会变为TIme_Wait,然后返回一个ACK给服务器,服务器收到ACK后状态变为CLosed,客户端等待2MSL后自己的状态也变为CLOSED。

在这里插入图片描述

19.Callable和Runnable之间有什么区别

Callable接口的call方法具有返回值,支持泛型,和FutureTask配合可以获得异步执行的结果,而Runnable的run方法没有返回值

Callable的call方法可以抛出异常,而run方法不行

Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行(在这个线程未执行完之前都会阻塞),所以一般不要着急调用get方法。(即get()方法不要紧挨着star()方法)

总结:

  • 接口实现的方法不一样,一个是run方法,一个是call方法
  • 一个无异常,一个有异常
  • 一个无返回值,一个有返回值

20.偏向锁,轻量级锁,自旋锁,重量级锁

当一个线程A访问同步块并获取锁时,会在对象头上加入自己的线程id,此时同步锁就会偏向于这个线程A,现在锁的状态为偏向锁。线程A尚未执行完同步代码块,线程B发起了申请锁的申请,这时锁一定会转化为轻量级锁。在轻量级锁的状态下,没有抢占到同步对象的线程B再次访问共享资源,这时锁升级为自旋锁。这时线程B一直循环检测锁是否被释放,而不是进入线程挂起状态,在自旋到一定次数后(10)还是没有或得到锁就会升级为重量级锁。

  1. 如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;
  2. 如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;
  3. 如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;

猜你喜欢

转载自blog.csdn.net/ysf15609260848/article/details/114282310