面试题(一)

JAVA基础

1.==、equals与HashCode的区别与联系

1、equals用于判断两个对象是否相等 == 判断的是地址是否相等(两个实例),具备自反性、一致性、传递性
2、hashCode 返回对象的哈希值int类型,用于再HashTable、HashSet计算存放下标使用(先通过计算hashCode获取下标,下标一致再根据equals判断是否相同的两个对象)

再使用HashCode的类散列表情况下 hashCode和equals具备以下关系:
1)、如果两个对象相等,那么它们的hashCode()值一定相同。
这里的相等是指,通过equals()比较两个对象时返回true。
2)、如果两个对象hashCode()相等,它们并不一定相等。
因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。所以这时候 一般我们修改 equals方法时,也需要修改 hashCode方法,不然即使equals方法返回TRUE,但是再HashMap里面因为hashCode的不同,所以不会调用equals方法,导致结果不正确。

2.深克隆与浅克隆

1.浅克隆:只复制基本类型(包含String类型)的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。
2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,复制后的对象与原对象之间完全不会影响。
3.使用序列化也能完成深复制的功能:对象序列化后写入流中,此时也就不存在引用什么的概念了,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。
4.使用clone实现的深克隆其实是浅克隆中嵌套了浅克隆,与toString方法类似

3.HashMap数据结构,HashTable数据结构

哈希表是一种组合的数据结构,它通常的实现方式是数组加链表,或者数组加红黑树。

HashMap
java基础-HashMap

HashTable
HashTable类继承自Dictionary类, 实现了Map接口。 大部分的操作都是通过synchronized锁保护的,是线程安全的,key、value都不可以为null, 每次put方法不允许null值,如果发现是null,则直接抛出异常。它的数据结构:主要是数组+链表

如果在非线程安全的情况下使用,建议使用HashMap替换,如果在线程安全的情况下使用,建议使用ConcurrentHashMap替换。

4.ConcurrentHashMap数据结构

ConcurrentHashMap数据结构

5.代理模式及动态代理详解

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

静态代理
代理对象与目标对象一起实现相同的接口或者继承相同父类,由程序员创建或特定工具自动生成源代码,即在编译时就已经确定了接口,目标类,代理类等。在程序运行之前,代理类的 .class 文件就已经生成。
静态代理优缺点:
优点:在不修改目标对象的功能前提下,可以对目标功能扩展。
缺点:假如又有一个目标类,也要做增强,则还需要新增相对应的代理类,导致我们要手动编写很多代理类。同时,一旦接口增加方法,目标对象与代理对象都要维护。

动态代理
代理类在程序运行时才创建的代理方式被称为动态代理。

  1. 基于JDK原生动态代理实现
    JDK动态代理是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法进行方法增强。在调用具体方法前通过调用 InvokeHandler 的 invoke 方法来处理。通过JDK源码分析其实是 Proxy 类的 newProxyInstance方法在运行时动态生成字节码生成代理类(缓存在Java虚拟机内存中),从而创建了一个动态代理对象。
    代理类继承了 Proxy 类,因为在Java中是单继承的,所以这就是为什么JDK动态代理中,目标对象一定要实现接口。
    它的特点是生成代理类速度很快,但是运行时调用方法操作会比较慢,因为是基于反射机制的,而且只能针对接口编程,即目标对象要实现接口。

  2. CGLIB动态代理
    Cglib(Code Generation Library)是一个强大的,高性能,高质量的Code生成类库,它是开源的。动态代理是利用 asm 开源包,将目标对象类的 class 文件加载进来,然后修改其字节码生成新的子类来进行扩展处理。即可以在运行期扩展Java类和实现Java接口。
    Cglib动态代理注意的2点:

  • 被代理类不能是 final 修饰的。
  • 需要扩展的方法不能有 final 或 static 关键字修饰,不然不会被拦截,即执行方法只会执行目标对象的方法,不会执行方法扩展的内容。

两种动态代理区别

  1. JDK动态代理是基于反射机制,生成一个实现代理接口的匿名类。而Cglib动态代理是基于继承机制,继承被代理类,底层是基于asm第三方框架对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
  2. JDK动态代理是生成类的速度快,后续执行类的方法操作慢;Cglib动态代理是生成类的速度慢,后续执行类的方法操作快。
  3. JDK只能针对接口编程,Cglib可以针对类和接口。在Springboot项目中,在配置文件中增加 spring.aop.proxy-target-class=true 即可强制使用Cglib动态代理实现AOP。
  4. 如果目标对象实现了接口,默认情况下是采用JDK动态实现AOP,如果目标对象没有实现接口,必须采用CGLIB库动态实现AOP。

线程

6.进程、线程、协程

进程与线程
进程是操作系统进行资源分配的基本单位,每个进程都有自己的独立内存空间。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程又叫做轻量级进程,是进程的一个实体,是处理器任务调度和执行的基本单位位。它是比进程更小的能独立运行的基本单位。线程只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

对于操作系统来说,一个任务就是一个进程(Process)。一个进程至少有一个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。

线程进程的区别体现在6个方面:
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一进程的线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。两者均可并发执行。

协程
协程,又称微线程,是一种用户态的轻量级线程,协程的调度完全由用户控制(也就是在用户态执行)。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到线程的堆区,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和线程切换相比,线程数量越多,协程的性能优势就越明显。不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。此外,一个线程的内存在MB级别,而协程只需要KB级别。

协程与线程的区别

  1. 一个线程可以有多个协程。
  2. 大多数业务场景下,线程进程可以看做是同步机制,而协程则是异步。
  3. 线程是抢占式,而协程是非抢占式的,所以需要用户代码释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  4. 协程并不是取代线程,而且抽象于线程之上。线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行。

7.linux中,java的线程模型

JVM 没有限定 Java 线程需要使用哪种线程模型来实现, JVM 只是封装了底层操作系统的差异,而不同的操作系统可能使用不同的线程模型,例如 Linux 和 windows 可能使用了一对一模型,solaris 和 unix 某些版本可能使用多对多模型。所以一谈到 Java 语言的多线程模型,需要针对具体 JVM 实现。

  1. 使用用户线程实现(多对一模型 M:1)
    多个用户线程映射到一个内核线程,用户线程建立在用户空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完成,对内核透明。

    优点:
  1. 线程的上下文切换都发生在用户空间,避免了模态切换(mode switch),减少了性能的开销。
  2. 用户线程的创建不受内核资源的限制,可以支持更大规模的线程数量。

缺点:

  1. 所有的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器可以被利用,浪费了其它处理器资源,不支持并行,在多处理器环境下这是不能够被接受的,如果线程因为 I/O 操作陷入了内核态,内核态线程阻塞等待 I/O 数据,则所有的线程都将会被阻塞。
  2. 增加了复杂度,所有的线程操作都需要用户程序自己处理,而且在用户空间要想自己实现 “阻塞的时候把线程映射到其他处理器上” 异常困难
  1. 使用内核线程实现(一对一模型 1:1)
    每个用户线程都映射到一个内核线程,每个线程都成为一个独立的调度单元,由内核调度器独立调度,一个线程的阻塞不会影响到其他线程,从而保障整个进程继续工作。

    优点:
  1. 每个线程都成为一个独立的调度单元,使用内核提供的线程调度功能及处理器映射,可以完成线程的切换,并将线程的任务映射到其他处理器上,充分利用多核处理器的优势,实现真正的并行。

缺点:

  1. 每创建一个用户级线程都需要创建一个内核级线程与其对应,因此需要消耗一定的内核资源,而内核资源是有限的,所以能创建的线程数量也是有限的。
  2. 模态切换频繁,各种线程操作,如创建、析构及同步,都需要进行系统调用,需要频繁的在用户态和内核态之间切换,开销大。
  1. 使用用户线程加轻量级进程混合实现(多对多模型 M:N)
    内核线程和用户线程的数量比为 M : N,这种模型需要内核线程调度器和用户空间线程调度器相互操作,本质上是多个线程被映射到了多个内核线程。

    综合了前面两种模型的优点:
  1. 用户线程的创建、切换、析构及同步依然发生在用户空间,能创建数量更多的线程,支持更大规模的并发。
  2. 大部分的线程上下文切换都发生在用户空间,减少了模态切换带来的开销。
  3. 可以使用内核提供的线程调度功能及处理器映射,充分利用多核处理器的优势,实现真正的并行,并降低了整个进程被完全阻塞的风险。

8.java线程状态, runnable、blocked,time_waiting,waiting

1.NEW(创建)
创建态:当一个已经被创建的线程处于未被启动时,即:还没有调用start方法时,就处于这个状态。

2.RUNNABLE(运行时)
运行态:当线程已被占用,在Java虚拟机中正常执行时,就处于此状态。

3.BLOCKED(排队时)
阻塞态:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态。当该线程持有锁时,该线程将自动变成RUNNABLE状态。

4.WAITING(休眠)
休眠态:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

5.TIMED_WAITING (指定休眠时间)
指定时间休眠态:基本同WAITING状态,多了个超时参数,调用对应方法时线程将进入TIMED_WAITING状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、锁对象.wait() 。

6.TERMINATED (结束)
结束态:从RUNNABLE状态正常退出而死亡,或者因为没有捕获的异常终止了RUNNABLE状态而死亡。

9.线程池有哪些核心的参数

  1. 核心线程数:corePoolSize
    线程池中活跃的线程数,即使它们是空闲的,除非设置了allowCoreThreadTimeOut为true。allowCoreThreadTimeOut的值是控制核心线程数是否在没有任务时是否停止活跃的线程,当它的值为true时,在线程池没有任务时,所有的工作线程都会停止。

  2. 最大线程数:maximumPoolSize
    线程池所允许存在的最大线程数。

  3. 多余线程存活时长:keepAliveTime
    线程池中除核心线程数之外的线程(多余线程)的最大存活时间,如果在这个时间范围内,多余线程没有任务需要执行,则多余线程就会停止。(注意:多余线程数 = 最大线程数 - 核心线程数)

  4. 时间单位:unit
    多余线程存活时间的单位,可以是分钟、秒、毫秒等。

  5. 任务队列:workQueue
    线程池的任务队列,使用线程池执行任务时,任务会先提交到这个队列中,然后工作线程取出任务进行执行,当这个队列满了,线程池就会执行拒绝策略。

  6. 线程工厂:threadFactory
    创建线程池的工厂,线程池将使用这个工厂来创建线程池,自定义线程工厂需要实现ThreadFactory接口。

  7. 拒绝执行处理器(也称拒绝策略):handler
    当线程池无空闲线程,并且任务队列已满,此时将线程池将使用这个处理器来处理新提交的任务。

10.线程池空闲的线程是如何回收?

超过corePoolSize的空闲线程由线程池回收,线程池Worker启动跑第一个任务之后就一直循环遍历线程池任务队列,超过指定超时时间获取不到任务就remove Worker,最后由垃圾回收器回收。

Worker是线程池ThreadPoolExecutor的一个内部类,其有一个成员变量thread(线程),所以我们可以把一个Worker假以理解为一个线程。

ThreadPoolExecutor回收工作线程,一条线程getTask()返回null,就会被回收。
分两种场景。
1、未调用shutdown() ,RUNNING状态下全部任务执行完成的场景
线程数量大于corePoolSize,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。
2、调用shutdown() ,全部任务执行完成的场景
shutdown() 会向所有线程发出中断信号,这时有两种可能。
2.1)所有线程都在阻塞
中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。
2.2)任务还没有完全执行完
至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

11.java线程状态为 blocked 场景

BLOCKED 状态跟 I/O 的阻塞是不同的,它不是一般意义上的阻塞,而是特指被 synchronized 块阻塞,即是跟线程同步有关的一个状态。

一旦一个线程获取锁进入同步块,在其出来之前,如果其它线程想进入,就会因为获取不到锁而阻塞在同步块之外,这时的状态就是 BLOCKED。

简单来说,大致有两种情况可以让线程处于这个状态。

  1. 线程A想进入某个同步快,但是由于该同步锁被其他线程占用,所以自己只能等待该锁,此时线程A为BLOCKED状态。
  2. 线程A已经获取该锁,进入同步块,但调用了wait方法后释放了该锁,然后其他线程内执行了同一把锁对象的notify或者notifyAll后,此时线程A为BLOCKED状态。

12.sleep 和 wait 区别

sleep() 方法让当前线程停止运行一段时间,到期自动继续执行。
wait() 方法让线程停止运行,在 notify() 或 notifyAll() 后继续执行。

相同

  1. sleep() 和 wait() 调用都会暂停当前线程并让出 CPU

不同

  1. 定义位置不同:sleep() 是线程类(Thread)的方法;wait() 是顶级类 Object 的方法;
  2. 调用地方不同:sleep 方法可以在任何地方使用;wait 方法则只能在同步方法或同步块中使用;
  3. 锁资源释放方式不同:sleep 方法只让出了CPU,没有释放同步资源锁! wait方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。
  4. 恢复方式不同:sleep调用后停止运行期间仍持有同步锁,所以到时间会继续执行;wait调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,再次获得对象锁后才会进入运行状态,在没有获取对象锁之前不会继续执行;
  5. 异常捕获:sleep需要捕获或者抛出异常,而wait/notify/notifyAll则不需要。

13.reentrantLock 和 sychnozied 区别

相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待。

不同点:

  1. Synchronized是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
  2. Synchronized等待不可中断,reentrantLock等待可中断。
  3. synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
  4. ReentrantLock可以同时绑定多个Condition对象,只需多次调用newCondition方法即可。
    synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件。但如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。

14.sychnozied 锁升级设计思想,偏向锁,轻量级,重量级

每个java对象都有一个对象头,对象头由类型指针和标记字段组成。在64位虚拟机中,未开启压缩指针,标记字段占64位,类型指针占64位,共计16个字节。markword是java对象数据结构中的一部分,markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,00表示轻量级锁,01表示无锁或偏向锁,10表示重量级锁。

  1. 检测Mark Word里面是不是当前线程的ID,如果是则表示当前线程处于偏向锁;
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1;
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
  6. 如果自旋成功则依然处于轻量级状态;
  7. 如果自旋失败,则升级为重量级锁;

偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁。

15.sychnozied 偏向锁是怎么撤销的

偏向锁的撤销,需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的,stop-the-world,在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的会被执行,并升级为轻量级锁,最后唤醒暂停的线程。

16.threadLocals 使用场景,实现原理

场景一:代替参数的显式传递
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
场景二:全局存储用户信息
我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)
场景三:解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。

每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。

参考:深入分析ThreadLocal的实现原理

17.synchronize 使用场景,实现原理

Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

monitorenter和monitorexit指令的底层是lock和unlock指令。

18.wait、notify 使用场景,实现原理

Monitor(管程)的结构,其中有一块叫做waitSet的区域,里面存放的是状态为WAITING状态的线程。如下图虚线框中的内容:

调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后 当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

19.join 使用场景,实现原理

join()是Thread类的一个方法,等待该线程终止. 需要明确的是主线程等待子线程(假设有个子线程thread)的终止。即在主线程的代码块中,如果碰到了thread.join()方法,此时主线程需要等子线程thread结束了(Waits for this thread to die.),才能继续执行thread.join()之后的代码块。

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

当子线程执行结束的时候,jvm会自动唤醒阻塞主线程。

20.interrupted 使用场景,实现原理

java interrupt中断机制是当主线程向目标线程发起interrupt中断命令后,目标线程的中断标志位被置为true,目标线程通过查询中断标志位自行决定是否停止当前线程的执行。

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            //打断的主要方法,该方法的主要作用是设置一个打断标记
            interrupt0();
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

interrupted()是静态方法而isInterrupted()是实例方法,他们的实现都是调用同一个native方法。主要的区别是他们的形参ClearInterrupted传的不一样。interrupted()在返回中断标志位后会清除标志位,isInterrupted()则不清除中断标志位。

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);

使用场景

  1. ThreadPoolExecutor中的 shutdownNow 方法会遍历线程池中的工作线程并调用线程的 interrupt 方法来中断线程。
  2. FutureTask 中的 cancel 方法,如果传入的参数为 true,它将会在正在运行异步任务的线程上调用 interrupt 方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么 cancel 方法中的参数将不会起到什么效果。

21.volatile 使用场景,实现原理

volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏) ,内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;(每个线程都有自己的工作内存)
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

22.指令重排序可以解决什么问题

为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,并确保这一结果和顺序执行结果是一致的,但是这个过程并不保证各个语句计算的先后顺序和输入代码中的顺序一致。这就是指令重排序。

  1. 编译器优化
    编译器(包括 JVM、JIT 编译器等)出于优化的目的,例如当前有了数据 a,把对 a 的操作放到一起效率会更高,避免读取 b 后又返回来重新读取 a 的时间开销,此时在编译的过程中会进行一定程度的重排。不过重排序并不意味着可以任意排序,它需要需要保证重排序后,不改变单线程内的语义,否则如果能任意排序的话,程序早就逻辑混乱了。

  2. CPU 重排序
    CPU 同样会有优化行为,这里的优化和编译器优化类似,都是通过乱序执行的技术来提高整体的执行效率。所以即使之前编译器不发生重排,CPU 也可能进行重排,我们在开发中,一定要考虑到重排序带来的后果。

  3. 内存的“重排序”
    内存系统内不存在真正的重排序,但是内存会带来看上去和重排序一样的效果,所以这里的“重排序”打了双引号。由于内存有缓存的存在,在 JMM 里表现为主存和本地内存,而主存和本地内存的内容可能不一致,所以这也会导致程序表现出乱序的行为。

重排序通过减少执行指令,从而提高整体的运行速度。

23.volatile 指令重排序,可见性

可见性: volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性 写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

禁止指令重排序: volatile是通过内存屏障来禁止指令重排序

public class Singleton {
	//volatile是防止指令重排
    private static volatile Singleton singleton;
    // 无参构造
    private Singleton() {}
    public static Singleton getInstance() {
        //第一层判断singleton是不是为null
        //如果不为null直接返回,这样就不必加锁了
        if (singleton == null) {
            //现在再加锁
            synchronized (Singleton.class){
                //第二层判断
                //如果A,B两个线程都在synchronized等待
                //A创建完对象之后,B还会再进入,如果不再检查一遍,B又会创建一个对象
                if (singleton == null) {
                    /*volatile主要是防止这里:
                    下面字节码会生成三个操作:
                    一是为Singleton对象在堆中分配空间
                    二是执行Singleton的构造函数
                    三是将新生成的Singleton对象的引用赋给singleton字段
                    而在重排序之后,上面的顺序有可能变成 一、三、二,那么这对象是残缺不全的--半对象
                    于是,在多线程情况下,别的线程可能会访问到一个singleton不为null却没有执行完构造函数的无效引用
                    */
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

24.cas 使用场景,aba的问题如何解决

25.aqs 是什么数据结构,volatile,cas,LockSupport.unpark/unpark

26.伪共享问题是如何发生的

缓存系统中的缓存是以缓存行(cache line)为单位存储的,Cache Line 是 CPU 和主存之间数据传输的最小单位,缓存行通常是 64 字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,因为都会导致同一个缓存行失效而会无意中影响彼此的性能,这就是伪共享(false sharing)。

为了避免由于 false sharing 导致 Cache Line 从 L1,L2,L3 到主存之间重复载入,我们可以使用数据填充追加字节的方式来避免,即单个数据填充满一个CacheLine,该方法本质上是一种空间换时间的做法。
JDK 8开始,提供了一个sun.misc.Contended 注解,用来解决伪共享问题,加上这个注解的类会自动补齐缓存行。

27.Disruptor 使用场景,数据结构,优势

计算机网络

1. 网络分层思想,链路层,网络层,传输层,应用层

1)物理层
该层负责比特流在节点间的传输,即负责物理传输。该层的协议既与链路有关,业余传输介质有关。通俗来讲就是把计算机连接起来的物理手段。
2)数据链路层
该层控制网络层与物理层之间的通信,其主要功能是如何在不可靠的物理线路上进行数据的可靠传递。为了保证传输,从网络层接收到的数据被分割成特定的可被物理层传输的帧。帧是用来移动数据的结构包,它不仅包括原始数据,还包括发送方和接收方的物理地址以及纠错和控制信息。其中的地址确定了帧将发送到何处,而纠错和控制信息则确保帧无差错到达。如果在传送数据时,接收点检测到所传数据中有差错,就要通知发送方重发这一帧。
3)网络层
该层决定如何将数据从发送方路由到接收方。网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中的节点 A 到另一个网络中节点 B 的最佳路径。
4)传输层
该层为两台主机上的应用程序提供端到端的通信。相比之下,网络层的功能是建立主机到主机的通
信。传输层有两个传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。其中,TCP是一个可靠的面向连接的协议,UDP是不可靠的或者说无连接的协议。
5)应用层
应用程序收到传输层的数据后,接下来就要进行解读。解读必须事先规定好格式,而应用层就是规定应用程序的数据格式的。它的主要协议有HTTP、FTP、Telnet、SMTP、POP3等。

2. 交换机是在链路层工作,MAC层协议中,MTU 最大传输单元

3. 路由器是在网络层工作,IP层协议中,TTL,IP分片

4. UDP是传输层协议,有哪些使用场景

UDP是无连接的,不可靠传输,尽最大努力交付数据,协议简单、资源要求少、传输速度快、实时性高的特点,适用于对传输效率要求高,但准确率要求低的应用场景,比如域名转换(DNS)、远程文件服务器(NFS)等。

5. UDP需要三次握手吗?

6.三次握手原理,为什么不是两次,挥手为什么是4次

答:建立连接的过程是利用客户服务器模式,假设主机A为客户端,主机B为服务器端。

(1)TCP的三次握手过程:主机A向B发送连接请求;主机B对收到的主机A的报文段进行确认;主机A再次对主机B的确认进行确认。
(2)采用三次握手是为了防止失效的连接请求报文段突然又传送到主机B,因而产生错误。失效的连接请求报文段是指:主机A发出的连接请求没有收到主机B的确认,于是经过一段时间后,主机A又重新向主机B发送连接请求,且建立成功,顺序完成数据传输。考虑这样一种特殊情况,主机A第一次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。
(3)如果不采用三次握手,我们考虑以下场景:
采用一次握手:
首先A发送一个(SYN)到B,意思是A要和B建立连接进行通信;
如果是只有一次握手的话,这样肯定是不行的,A压根都不知道B是不是收到了这个请求。
采用二次握手:
B收到A要建立连接的请求之后,发送一个确认(SYN+ACK)给A,意思是收到A的消息了,B这里也是通的,表示可以建立连接;
如果只有两次通信的话,这时候B不确定A是否收到了确认消息,有可能这个确认消息由于某些原因丢了。
采用三次握手:
A如果收到了B的确认消息之后,再发出一个确认(ACK)消息,意思是告诉B,这边是通的,然后A和B就可以建立连接相互通信了;
这个时候经过了三次握手,A和B双方确认了两边都是通的,可以相互通信了,已经可以建立一个可靠的连接,并且可以相互发送数据。

因为TCP有个半关闭状态,假设A.B要释放连接,那么A发送一个释放连接报文给B,B收到后发送确认,这个时候A不发数据,但是B如果发数据A还是要接收,这叫半关闭。然后B还要发给A连接释放报文,然后A发确认,所以是4次。

7.TCP数据结构关键字段:序号,滑动窗口

Sequence Number(序列号):占 4 个字节,范围是[0, 232 - 1],序号增加到 232 - 1 后,下一个序号就又回到 0。在 TCP 连接中传送的字节流中的每一个字节都要按顺序编号,起始序号在连接建立时就完成设置。因此序列号可以用来解决网络包乱序(reordering)问题。

例如,一个报文段的序号是 301,而携带的数据共有 100 个字节。这就表明:本报文段的数据的第一个字节的序号是 301,最后一个字节的序号是 400。显然下一个报文段的数据序号要从 401 开始。

Window(窗口):占 2 个字节,窗口值是一个 [0, 216 - 1] 之间的整数。窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口)。窗口值用于告诉对方:从本报文段首部中的确认号算起,接受方目前允许对方发送的数据量(以字节为单位)。之所以要有这个限制,是因为接受方的数据空间是有限的。

例如:发送了一个报文段,其确认号是 701,窗口字段值为 1000。这就告诉对方:“从 701 序号开始算起,我(发送此报文段的一方)的接收缓存空间还可以接收 1000 个字节数据,字节序号是 701 - 1700,你在给我发送数据时,必须要考虑到这一点”。窗口字段值明确的指出了现在允许对方发送的数据量,窗口值通常是在不断的动态变化着。

8.TCP相对UDP有哪些优点:顺序发送,超时重试,流量控制,拥塞控制

9.TCP顺序发送:如何解决顺序发送问题? 自增序号,三次握手确定序号

10.TCP超时重试:有哪些重试的方法? 快速重传,接受地图

11.TCP拥塞控制:如何解决拥塞问题

12.TCP:粘包/拆包是如何发生的

粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。

对于粘包和拆包问题,常见的解决方案有四种:

  1. 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
  2. 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
  3. 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  4. 通过自定义协议进行粘包和拆包的处理。

13.DNS是如何工作的,本地电脑发起一个百度的请求

14.Linux操作系统的,用户态和内核态

15.零拷贝怎么理解,mmap、sendfile。虚拟内存

16.DMA是什么

17.IO多路复用主要解决什么问题?

同步阻塞(BIO):
服务端采用单线程,当 accept 一个请求后,在 recv 或 send 调用阻塞时,将无法 accept 其他请求(必须等上一个请求处理 recv 或 send 完 )(无法处理并发)
服务端采用多线程,当 accept 一个请求后,开启线程进行 recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写实际的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费。

同步非阻塞(NIO):
服务器端当 accept 一个请求后,加入 fds 集合,每次轮询一遍 fds 集合 recv (非阻塞)数据,没有数据则立即返回错误,每次轮询所有 fd (包括没有发生读写实际的 fd)会很浪费 CPU。

IO多路复用:
服务器端采用单线程通过 select/poll/epoll 等系统调用获取 fd 列表,遍历有事件的 fd 进行 accept/recv/send ,使其能支持更多的并发连接请求。

18.IO多路复用,EPOLL原理,等待队列,红黑树,就绪队列

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是**事件驱动(每个事件关联上fd)**的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))。

epoll函数接口
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

#include <sys/epoll.h>

// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

// API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为红黑树元素个数)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

19.HTTPS是如何工作的

20.HTTP2.0有哪些升级?合并请求头,多路复用,主动推送

21. rpc和http的区别,使⽤用场景?

  1. 区别: 传输协议
    RPC,可以基于TCP协议,也可以基于HTTP协议
    HTTP,基于HTTP协议
  2. 传输效率
    RPC,使⽤用⾃自定义的TCP协议,可以让请求报⽂文体积更更⼩小,或者使⽤用HTTP2协议,也可以很好的减少报⽂文的体积,提⾼传输效率
    HTTP,如果是基于HTTP1.1的协议,请求中会包含很多⽆无⽤用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为⼀个RPC来使⽤用的,这时标准RPC框架更更多的是服务治理理
  3. 性能消耗,主要在于序列列化和反序列列化的耗时
    RPC,可以基于thrift实现⾼高效的⼆进制传输
    HTTP,⼤大部分是通过json来实现的,字节⼤大⼩小和序列列化耗时都⽐比thrift要更消耗性能
  4. 负载均衡
    RPC,基本都⾃自带了了负载均衡策略略
    HTTP,需要配置Nginx,HAProxy来实现
  5. 服务治理理(下游服务新增,重启,下线时如何不不影响上游调⽤用者)
  6. RPC,能做到⾃自动通知,不不影响上游
  7. HTTP,需要事先通知,修改Nginx/HAProxy配置

总结:RPC主要⽤用于公司内部的服务调⽤用,性能消耗低,传输效率⾼高,服务治理理⽅方便便。HTTP主要⽤用于对外的异构环境,浏览器器接⼝口调⽤用,APP接⼝口调⽤用,第三⽅方接⼝口调⽤用等。

JVM

1.class二进制文件,前面几位魔术是什么?

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。之所以使用魔数而不是文件后缀名来进行识别主要是基于安全性的考虑,因为文件后缀名是可以随意更改的(当然魔术也可以改,只不过比起改后缀名来说更复杂)。
Class 文件的魔数值固定为「0xCAFEBABE」。

2.多态是如何实现的,invokevirtual,invokeInterface

C++中只有直接调用、间接调用,而JVM通过不同的invoke指令来实现不同属性的方法调用。
那什么是多态呢,满足下面这几个条件就可以称为多态:
1、继承了某个类、实现了某个接口
2、重写父类的方法、实现接口中的方法
3、父类引用指向子类对象

C++中的间接调用与直接调用,JVM抽象成了4个指令来完成:
1、invokevirtual:invokevirtual指令用于调用声明为类的方法;这个指令用于调用public、protected修饰,且不被static、final修饰的方法。跟多态机制有关。
2、invokeinterface:invokeinterface指令用于调用声明为接口的方法;跟invokevirtual差不多。区别是多态调用时,如果父类引用是对象,就用invokevirtual。如果父类引用是接口,就用这个。
3、invokespecial:只用于调用私有方法,构造方法。跟多态无关
4、invokestatic:调用静态方法;

以 invokevirtual 指令为例,在执行时,大致可以分为以下几步:
1、先从操作栈中找到对象的实际类型 class;
2、找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
3、如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
4、如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;
可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。

3.类加载器有哪些类型?

VM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

  1. 启动类加载器(引导类加载器,Bootstrap ClassLoader)
  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并作为他们的父类加载器(当他俩的爹)。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
  1. 扩展类加载器(Extension ClassLoader)
  • java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为启动类加载器。
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
  1. 应用程序类加载器(系统类加载器,AppClassLoader)
  • Java语言编写,由sun.misc.LaunchersAppClassLoader实现。
  • 派生于ClassLoader类。
  • 父类加载器为扩展类加载器。
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
  • 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器。
  1. 用户自定义加载器

4.类的加载过程

类加载的过程主要分为三个部分:加载、链接、初始化。
而链接又可以细分为三个小部分:验证、准备、解析。
加载
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。这里有两个重点:

  1. 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译。
  2. 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器以及用户的自定义类加载器。

加载的3个阶段

  1. 通过类的全限定名获取二进制字节流(将 .class 文件读进内存);
  2. 将字节流的静态存储结构转化为运行时的数据结构;
  3. 在内存中生成该类的 Class 对象;HotSpot 虚拟机把这个对象放在方法区,非 Java 堆。

验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
包括对于文件格式的验证,比如常量中是否有不被支持的常量?是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数” 0xCAFEBABE?文件中是否有不规范的或者附加的其他信息?
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
特别需要注意,初值,不是代码中具体写的初始化的值,而是 Java 虚拟机根据不同变量类型的默认初始值。
比如 8 种基本类型的初值,默认为 0;引用类型的初值则为null;常量的初值即为代码中设置的值,例如final static tmp = 456, 那么该阶段 456 就是tmp的初值。

解析
将常量池内的符号引用替换为直接引用的过程。两个重点:

  1. 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  2. 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
    举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。换句话说,只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

5.双亲委派机制,作用是什么,为什么又要打破

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  2. 如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终达到顶层的启动类加载器。
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

优点

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改

为什么要打破呢?因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件。自定义类加载器加载一个类需要:继承ClassLoader,重写findClass,如果不想打破双亲委派模型,那么只需要重写findClass;如果想打破双亲委派模型,那么就重写整个loadClass方法,设定自己的类加载逻辑。

6.JVM内存是如何划分的,堆,栈,方法区,直接内存

  1. 程序计数器
    程序计数器(Program Counter Register),也有称作为PC寄存器。虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示执行哪条指令的。

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相**扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

  1. Java栈
    Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。

Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了,这部分空间的分配和释放都是由系统自动实施的。

局部变量表就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈,一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
动态链接,指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。

  1. 本地方法栈
    本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。


  2. Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。

  3. 方法区
    方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

7.栈数据结构:栈帧结构,局部变量表,操作数栈,动态链接,方法返回地址

8.分代设计思想,堆是如何划分

一般商业的虚拟机,大多数都遵循了分代收集的设计思想,分代收集理论主要有两条假说。
第一个是强分代假说,强分代假说指的是 JVM 认为绝大多数对象的生存周期都是朝生夕灭的;
第二个是弱分代假说,弱分代假说指的是只要熬过越多次垃圾收集过程的对象就越难以回收(看来对象也会长心眼)。
就是基于这两个假说理论,JVM 将堆区划分为不同的区域,再将需要回收的对象根据其熬过垃圾回收的次数分配到不同的区域中存储。

JVM 根据这两条分代收集理论,把堆区划分为新生代(Young Generation)和老年代(Old Generation)这两个区域。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区(From Survivor 和 To Survivor)。在新生代中,每次垃圾收集时都发现有大批对象死去,剩下没有死去的对象会直接晋升到老年代中。
上面这两个假说没有考虑对象的引用关系,而事实情况是,对象之间会存在引用关系,基于此又诞生了第三个假说,即跨代引用假说(Intergeneration Reference Hypothesis),跨代引用相比较同代引用来说仅占少数。正常来说存在相互引用的两个对象应该是同生共死的,不过也会存在特例,如果一个新生代对象跨代引用了一个老年代的对象,那么垃圾回收的时候就不会回收这个新生代对象,更不会回收老年代对象,然后这个新生代对象熬过一次垃圾回收进入到老年代中,这时候跨代引用才会消除。

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时(指新生代的垃圾收集),只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

JVM中大家是否还记得对象在Suvivor中每熬过一次MinorGC,年龄就增加1,当它的年龄增加到一定程度后就会被晋升到老年代中,这个次数默认是15岁,有想过为什么是15吗?在Mark Word中可以发现标记对象分代年龄的分配的空间是4bit,而4bit能表示的最大数就是2^4-1 = 15。

9.分类,强、软、弱、虚引用

  1. 强引用(StrongReference)
    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:
Object strongReference = new Object();

如果强引用对象不使用时,需要弱化从而使GC能够回收,如显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。
在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

  1. 软引用(SoftReference)
    如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,JVM首先将软引用中的对象引用置为null,然后等待垃圾回收器回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因。

软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

  1. 弱引用(WeakReference)
    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    str = null;

JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收。
同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  1. 虚引用(PhantomReference)
    虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

10.堆外内存如何回收?

堆外内存的申请和释放
JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。
最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。

堆外内存的回收机制
JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对用的Cleaner对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

11.对象内存中数据接口:对象头(hash,锁信息,生代年龄,kclass)、实例数据、对齐填充

在 JVM 中,Java对象保存在堆中时,由以下三部分组成:

对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。虽然它们在不同位数的JVM中长度不一样,但是基本组成内容是一致的。

  1. 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  2. biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  3. 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  4. 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
  5. 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  6. epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  7. ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  8. ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

32位JVM中Mark Word存储格式

64位JVM中Mark Word存储格式

Klass Pointer:即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。

为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。

12.对象在内存中生命周期,从new到灭亡

Java对象在JVM中的运行周期大致上分为七个阶段,创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)

13.逃逸分析是什么?栈上分配,锁消除,标量替换

逃逸分析
逃逸分析是一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。逃逸分析不是直接优化代码的手段,而是为其它优化手段提供依据的分析技术。逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。简单的说,逃逸分析指的是分析变量能不能逃出它的作用域。
如果能证明一个对象不会逃逸到方法或者线程之外,也就是别的方法和线程无法通过任何途径访问到这个方法,则可能为这个变量进行一些高效优化。
 
逃逸分析可以细分为四种场景:
第一:全局变量赋值逃逸。
第二:方法返回至逃逸。
第三:实例引用逃逸。
第四:线程逃逸。当赋值给类变量或者赋值给其他线程里面可以访问的实例变量就会发生线程逃逸。

public class SomeClass {
    public void printClassName(EscapeDemo1 escapeDemo1){
        System.out.println(escapeDemo1.getClass().getName());
    }
}

public static  SomeClass someClass;
//全局变量赋值逃逸
public void globalVariablePointerEscape(){
    someClass=new SomeClass();
}

//方法返回值逃逸
public void someMethod(){
    SomeClass someClass=methodPointerEscape();
}
public SomeClass methodPointerEscape(){
    return new SomeClass();
}

    //方法返回值逃逸
public void someMethod(){
    SomeClass someClass=methodPointerEscape();
}
public SomeClass methodPointerEscape(){
    return new SomeClass();
}

标量替换
所谓的标量指的是不能进一步分解的量。像 Java 的基础数据类型(int、long等数值类型以及 reference 类型等)以及对象的地址引用都是标量,因为它们是没有办法继续分解的。与标量对应的是聚合量,聚合量指的是可以进一步分解的量,比如字符串就是一个聚合量,因为字符串是用字节数组实现的,可以分解。又比如我们自己定义的变量也都是聚合量。
那么什么是标量替换呢?根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。

栈上分配
Java 虚拟机中,绝大多数对象都是存放在堆里面的,Java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收掉堆中不再使用的对象,但回收动作无论是筛选可回收对象,还是回收和整理内存都需要耗费时间。
 
但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样就无需在堆上分配内存,也无须进行垃圾回收了。

那么什么是栈上分配呢?指的是如果通过逃逸分析确认对象不会被外部访问到的话。那么就直接在栈上分配对象,那么在栈上分配对象的话,这个对象占用的空间就会在栈帧出站的时候被销毁了,所以通过栈上分配可以降低垃圾回收的压力。

同步消除
如果逃逸分析能确定一个变量不会逃逸出线程,无法被其它线程访问,那这个变量的读写就不会有多线程竞争的问题,因而变量的同步措施也就可以消除了。

14.对象存活算法。引用计数器与可达性分析

引用计数算法
引用计数器就是: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时刻计数器为 0 的对象就是不可能再被使用的,可以此时进行回收。
但是引用计数法有一个很大的缺陷,就是它很难解决对象之间相互循环引用的问题。

可达性分析算法
可达性分析算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

在Java语言中,可作为GCRoots对象包含为以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
  2. 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
  3. 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
  4. 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
(4)最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

15.为什么需要stw?引用关系不发生变化,GC、中断、取消偏向锁

在发生GC时会停下所有的用户线程,从而导致Java程序出现全局停顿的无响应情况,而这种情况则被称为STW(Stop The World)世界暂停。在发生STW之后,所有的Java代码会停止运行,不过native代码是可以继续执行的,但也不能和JVM交互。一般发生STW都是由于GC引起的,但在某几种少数情况下,也会导致STW出现,如线程Dump、死锁检查、堆日志Dump等

GC发生时为什么都必须要STW呢?

一个是尽量为了避免浮动垃圾产生,就是刚刚标记完成一块区域中的对象,但转眼用户线程又在该区域中产生了新的“垃圾”。
第二个则是为了确保一致性,分析工作必须在一个能确保一致性的快照中进行,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。

JVM在发生GC时,主要作用的区域有三个:新生代、年老代以及元数据空间,当然,程序运行期间,绝对多数GC都是在回收新生代。一般而言,GC可以分为四种类型,如下:

①新生代收集:只针对新生代的GC,当Eden区满了时触发,Survivor满了并不会触发。
②年老代收集:针对年老代空间的GC,不过目前只有CMS存在单独回收年老代的行为。
③混合收集:指收集范围覆盖整个新生代空间及部分年老代空间的GC,目前只有G1存在该行为。
④全面收集:覆盖新生代、年老代以及元数据空间的GC,会对于所有可发生GC的内存进行收集。

偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

16.安全点是什么?安全区域是什么。引用关系不发生变化

当GC发生时,必然会出现程序停顿,也就是需要停止所有用户线程。但问题在于:用户线程停止的时机必须合理,不然在恢复线程后,有可能会导致最终的执行结果出现不一致,因此用户线程必然需要在一个安全的位置暂停。
而在JVM中,存在两个概念:安全点安全区域,当用户线程执行到安全点或安全区域的代码处,此时发生停止是安全的,后续再次唤醒线程工作时,执行结果也不会因为线程暂停而受到任何影响。

安全点(SafePoint)
无论是在GC中还是并发编程中,都会经常出现安全点这个概念,因为当我们需要阻塞停止一条线程时,都需要在安全点停止,简单说安全点就是指当线程运行到这类位置时,堆对象状态是确定一致的,线程停止后,JVM可以安全地进行操作,如GC、偏向锁撒销等。

而JVM中对于安全点的定义主要有如下几种:
①循环结束的末尾段
②方法调用之后
③抛出异常的位置
④方法返回之前

当JVM需要发生GC、偏向锁撤销等操作时,如何才能让所有线程到达安全点阻塞或停止?
①主动式中断(JVM采用的方式):不中断线程,而是设置一个标志,而后让每条线程执行时主动轮询这个标志,当一个线程到达安全点后,发现中断标志为true时就自己中断挂起。
②抢断式中断:先中断所有线程,如果发现线程未执行到安全点则恢复线程让其运行到安全点位置。

安全区域(SafeRegion)
当Java程序需要停下所有用户线程时,某些线程可能处于中断或者休眠状态,从而无法响应JVM的中断请求走到安全点位置挂起了,所以出现了安全区域的概念。

安全区域是指一条线程执行到一段代码时,该区域的代码不会改变堆中对象的引用。在这区域内JVM可以安全地进行操作。当线程进入到该区域时需要先标识自己进入了,这样GC线程则不会管这些已标识的线程,当线程要离开这个区域时需要先判断可达性分析是否完成,如果完成了则往下执行,如果没有则需要原地等待到GC线程发出安全离开信息为止。

17.GC回收算法:复制,标记清除,标记整理

标记-清除算法
标记清除算法是现代GC算法的基础,标-清算法会将回收工作分为标记和清除两个阶段。在标记阶段会根据可达性分析算法,通过根节点标记堆中所有的可达对象,而这些对象则被称为堆中存活对象,反之,未被标记的则为垃圾对象。然后在清除阶段,会对于所有未标记的对象进行清除。

初始GC标志位都为0,也就是未标记状态,假设此时系统堆内存出现不足,那么最终会触发GC机制。GC开始时,在标记阶段首先会停下整个程序,然后GC线程开始遍历所有GC Roots节点,根据可达性分析算法找出所有的存活对象并标记为1,标记阶段完成后,会找出并标记所有存活对象,接下来就会执行清除阶段,清楚所有未被标记的对象,在清除操作完成之后,会将前面存活对象的GC标志位复位,也就是会将标记从1为还原成未标记的0。

GC标记到底在哪儿?在对象头中存在一个markword字段,而GC标志位就存在其内部。同时,清除阶段并不是简单的置空内存,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。

标记-清除算法是最初的GC算法,因为在标记阶段需要停下所有用户线程,也就是发生STW,而标记的时候又需要遍历整个堆空间中的所有GcRoots,所以耗时比较长,对于客户端而言,可能会导致GC发生时,造成很长一段时间内无响应。同时,因为堆空间中的垃圾对象是会分散在内存的各个角落,所以一次GC之后,会造成大量的内存碎片,也就是通过标-清算法清理出来的内存是不连续的,为了解决这个问题,JVM就不得不再额外维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象或大对象时,连续的内存空间资源又会变得很匮乏。

复制算法
复制算法会将JVM中原有的堆内存分为两块,在同一时刻只会使用一块内存用于对象分配。在发生GC时,首先会将使用的那块内存区域中的存活对象复制到未使用的这块内存中。等复制完成之后,对当前使用的这块内存进行全面清除回收,清除完成之后,交换两块内存之间的角色,最后GC结束。

复制算法带来的好处是显而易见的,因为每次GC都是直接对半边区域进行回收,所以回收之后不需要考虑内存碎片的复杂情况,在内存分配时直接可以使用简单高效的 指针碰撞 方式分配对象。

但这种算法最大的问题在于对内存的浪费,因为在实际内存分配时只会使用一块内存,所以在实际分配时,内存直接缩水一半,这是比较头疼的事情。同时,存活的对象在GC发生时,还需要复制到另一块内存区域,因此对象移动的开销也需要考虑在内,所以想要使用这种算法,最起码对象的存活率要非常低才行。

一般都采用复制算法来收集新生代空间,因为新生代中95%左右的对象都是朝生夕死的。在HotSpot中,新生代会被划分为Eden1、Survivor2三个区域,但比例并非1:1,因为经过一次GC后能够依旧活着的对象是十不存一的,所以需要转移的对象并不多,所以在HotSpotVM中,三个区域的比例默认为8:1:1。当每次新生代发生GC时,就将Eden区和一块Survivor区的存活对象移动到另外一块Survivor区中,最后对Eden区和原本那块Survivor区进行全面回收。所以也就是说,HotSpot中新生代的内存最多浪费10%,最大容量为80%+10%=90%。

但凡事没有绝对,因为在运行时,谁也不能保证每次存活的对象总量都小于新生代空间的10%,所以有时候可能会出现:另外一块Survivor区10%的空间放不下新生代的存活对象这种情况,所以此时就需要空间分配担保机制介入了。

空间分配担保机制机制是指:当Survivor空间不够用时,需要依赖于年老代进行分配担保,对Survivor空间空间中的存活对象进行动态晋升判定,把一些符合条件的对象提前转入到年老代空间中存储,以确保新生代能够空出足够的空间,确保新生代GC的正常工作。

标记-整理算法
标记-整理算法也被称为标记-压缩算法,标-整算法适用于存活率较高的场景,它是建立在标-清算法的基础上做了优化。标-整算法也会分为两个阶段,分别为标记阶段、整理阶段:

①标记阶段:和标-清算法一样。在标记阶段时也会基于GcRoots节点遍历整个内存中的所有对象,然后对所有存活对象做一次标记。
②整理阶段:在整理阶段该算法并不会和标-清算法一样简单的清理内存,而是会将所有存活对象移动(压缩)到内存的一端,然后对于存活对象边界之外的内存进行统一回收。

经过标-整算法之后的堆空间会变成整齐的内存,因为被标记为存活的对象都会被压缩到内存的一端。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,也就是保留一根指针指向已用内存和空闲内存的分割点,也就是可以直接采用指针碰撞的方式进行内存分配,这比维护一个空闲列表显然少了许多开销。

标-整算法唯一的美中不足在于:它的整体收集效率并不高。因为标-整算法不仅仅要标记对象,同时还要移动存活对象,所以整个GC过程下来,它所需要耗费的时间资源开销必然是不小的。

不过一般年老代空间都是采用标-整算法,因为一方面年老代GC次数方面远没有新生代频繁,同时,晋升年老代的对象一般来说体积不会很小,所以在晋升时需要足够的内存大小分配,如果采用标-清算法会导致大对象无法进行分配,如若采用复制算法则没有新的空间为年老代提供。

如上三种GC算法则是JVM虚拟机的基础GC算法,综合对比来看:

收集速度:复制算法 > 标-清算法 > 标-整算法
内存整齐度:复制算法 = 标-整算法 > 标-清算法
内存利用率:标-整算法 > 标-清算法 > 复制算法

18.CMS、G1垃圾收集器的对比

G1 在压缩空间方面有优势。
G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题。Eden, Survivor, Old 区不再固定、在内存使用效率上来说更灵活。
G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
G1 在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在 STW(stop the world)的时候做。
G1 会在 Young GC 中使用、而 CMS 只能在 O 区使用。
吞吐量优先:G1
响应优先:CMS
CMS 的缺点是对 cpu 的要求比较高。G1 是将内存化成了多块,所有对内段的大小有很大的要求。
CMS 是清除,所以会存在很多的内存碎片。G1 是整理,所以碎片空间较小。

19.GC类型:YongGC、OldGC、MixedGC、FullGC

Mixed GC
Mixed GC 是 G1 中特有的概念,其实说白了,主要就是说在 G1 中,一旦老年代占据堆内存的 45%(-XX:InitiatingHeapOccupancyPercent:设置触发标记周期的 Java 堆占用率阈值,默认值是 45%。这里的Java 堆占比指的是 non_young_capacity_bytes,包括 old + humongous),就要触发 Mixed GC,此时对年轻代和老年代都会进行回收。Mixed GC 只有 G1 中才会出现。

20.CMS垃圾收集器的特点

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程如下:
1)初始标记
2)并发标记
3)重新标记
4)并发清除
初始标记、重新标记这两个步骤仍然需要“stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。

CMS是一款优秀的收集器,主要优点:并发收集、低停顿。

缺点:

1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。
3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

21.CMS垃圾收集器收集过称,初始标记,并发标记,重新标记,并发清理

CMS 处理过程有七个步骤:

  1. 初始标记(CMS-initial-mark) ,会导致stw;
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,会导致swt;
  6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。可以通过JVM启动参数:-XX:+UseConcMarkSweepGC来开启CMS。

初始标记
这一步的作用是标记存活的对象,有两部分:

  1. 标记老年代中所有的GC Roots对象
  2. 标记年轻代中活着的对象引用到的老年代的对象

并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象;因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;

JVM会通过Card(卡片)的方式将发生改变的老年代区域标记为“脏”区,这就是所谓的卡片标记(Card Marking)

并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。 由于这个阶段是和用户线程并发的,可能会导致concurrent mode failure。

重新标记
最终标记是此阶段GC事件中的第二次(也是最后一次)STW停顿。目标: 重新扫描堆中的对象,因为之前的预清理阶段是并发执行的,有可能GC线程跟不上应用程序的修改速度。扫描范围: 新生代对象+GC Roots+被标记为“脏”区的对象。如果预清理阶段没有做好,这一步扫描新生代的时候就会花很多时间。

并发清理
此阶段与应用程序并发执行,不需要STW停顿。JVM在此阶段删除不再使用的对象,并回收他们占用的内存空间。因为重新标记已经把所有还在使用的对象进行了标记,因此此阶段可以与应用线程并发的执行。

22.CMS垃圾收集器会产生内存碎片吗?如何处理?

由于CMS采用的是"标记-清除"算法,那么在运行到一定时间后,会产生一些内存碎片。当有新的对象要进入老年代时,可能会造成内存不够分配的情况。这个时候可以通过参数-XX:CMSFullGCsBeforeCompaction进行内存整理。比如配置-XX:CMSFullGCsBeforeCompaction=5,那么每执行5次Full GC就会对老年代进行内存空间整理。

23.并发标记:三色标记法。浮动垃圾、漏标

三色标记法
并发标记过程中允许用户线程正常执行,采用三色标记算法从初始标记的对象开始遍历整个老年代,进行存活对象标记,这个过程相对过长,但对于用户来说是几乎无感知的。

三色标记是CMS采用的标记对象算法,可以缩短STW时间并达到标记存活对象的效果。按照对象是否被垃圾收集器访问过这个条件,标记的颜色有下面三种:

白色:表示该对象还没有被访问过。在可达性分析开始阶段,除GC Root对象外,所有的对象节点都是白色的。如果在可达性分析执行完后,还有白色状态的对象,即对象不可达,那么这些就可以被认定为垃圾对象。
黑色:表示该对象已经被访问过,并且该对象的引用对象也全部被访问过,该对象可达,为存活对象。
灰色:表示该对象已经被访问过,但是存在引用对象还没有被访问过。比如在访问A对象时,A对象内部引用了B和C对象,当访问B对象时,发现内部没有引用其他对象,那么此时B对象已经被GC访问过了。但对于A对象来说,尽管B对象已经被访问,C对象还没有被访问,所以A的对象标记为灰色。

浮动垃圾
在并发清除过程中,由于用户线程也在不断的运行,如果出现漏标情况,就会产生一些垃圾对象,这些垃圾对象叫做浮动垃圾。这些垃圾对象只能等待下次GC时才能被回收,在没被回收之前仍然占用内存空间。

错标
在并发标记过程中不会触发STW,会导致对象引用关系的变更。那么就会产生一些问题。比如A对象被垃圾回收器访问后被标记成了黑色,但是用户线程的执行A对象和H对象产生了引用关系,但是A已经被标记为白色了,不会在被重新访问,那就意味着H对象被误认为垃圾对象了,当H对现象被回收后,那将会有严重的错标问题了。

漏标
除错标问题外,还有另外一种情况。当扫描到C对象时,由于C对象还有引用对象没有被扫描,此时C对象会被标为灰色。但是由于用户线程的运行,A对象和C对象的引用关系被取消,垃圾收集器会继续由C对象向下进行可达性分析。原则上C对象将会是垃圾对象,但是实际上这些对象仍然会被标记为存活对象,这种情况称为漏标。

24.G1垃圾收集器的特点,大内存友好,可预计的暂停时间

特点

  1. 并行和并发
    并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  2. 分代收集
    从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。和之前的各类回收器不同,它同时兼顾年轻代和老年代。
  3. 空间整合
    G1将内存划分为一个个的region。 内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记一压缩(Mark一Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  4. 可预测的停顿时间模型

可预计暂停时间
可以通过-XX:MaxGCPauseMillis参数指定预期的停顿时间,G1 GC的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。也就是说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

25.如何达到可预测的暂定时间?Remembered Set

见24。

已记忆集合

在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(Remembered Set),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区。

对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了)而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用

26.G1垃圾收集器发生fullGC正常吗?

G1的初衷就是要避免Fu1l GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World) ,使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC呢? 比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc, 这种情况可以通过增大内存解决。
导致G1Full GC的原因可能有两个: .

  1. 回收的时候没有足够的to-space来存放晋升的对象
  2. 并发处理过程没完成空间就耗尽了

27.G1垃圾收集器收集过称,年轻代GC,老年代并发标记过程(45%),混合回收,fullGC

设计理念

  1. 区域划分
    • Region区域:将Java堆划分为多个大小相等的Region,每个Region都可以是新生代、老年代。G1收集器根据角色的不同采用不同的策略去处理。在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
    • Humongous区域(Region中的一部分):专门用来存储大对象(超过Region容量一半的对象即为大对象),超过整个Region区域的会放在多个连续的Humongous区。 G1把Humongous当做老年代的一部分。
  2. 垃圾收集
    G1的Collector一侧就是两个大部分,并且这两个部分可以相对独立执行。全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)。

垃圾回收过程
年轻代GC (Young GC)
回收时机
(1). 当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程
(2). 年轻代垃圾回收只会回收Eden区和Survivor区

回收过程

  1. 根扫描:一定要考虑remembered Set,看是否有老年代中的对象引用了新生代对象
    根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口)
  2. 更新RSet:处理dirty card queue中的card,更新RSet。 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用
  3. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
  4. 复制对象:此阶段,对象树被遍历,Eden区 内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间
  5. 处理引用:处理Soft,Weak, Phantom, Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

老年代并发标记过程 (Concurrent Marking)

  1. 初始标记阶段:
    标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC
  2. 根区域扫描(Root Region Scanning):
    G1 GC扫描Survivor区**直接可达的老年代区域对象,**并标记被引用的对象。这一过程必须在young GC之前完成(YoungGC时,会动Survivor区,所以这一过程必须在young GC之前完成)
  3. 并发标记(Concurrent Marking):
    在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  4. 再次标记(Remark):
    由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot一at一the一beginning (SATB).
  5. 独占清理(cleanup,STW):
    计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。(这个阶段并不会实际上去做垃圾的收集)
  6. 并发清理阶段:识别并清理完全空闲的区域

混合回收(Mixed GC)
Mixed GC并不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。

由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

混合回收并不一定 要进行8次。有一个阈值**-XX :G1HeapWastePercent**,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

28.工作经验,性能有问题如何分析,OOM怎么处理?MAT工具,Arthas工具

框架

1. Spring - IOC体系结构设计

2. Spring IOC初始化流程

3. spring ioc中bean的生命周期

Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的;容器中存放的是Bean的定义即BeanDefinition放到beanDefinitionMap中,本质上是一个ConcurrentHashMap<String, Object>;并且BeanDefinition接口中包含了这个类的Class信息以及是否是单例等。

4. BeanFactory 和 FactoryBean 的区别?

BeanFactory是接口,提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范,它定义了getBean()、containsBean()等管理Bean的通用方法,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,Spring的容器都是它的具体实现如:

  • DefaultListableBeanFactory
  • XmlBeanFactory
  • ApplicationContext
public interface BeanFactory {

	//对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,
	//如果需要得到工厂本身,需要转义
	String FACTORY_BEAN_PREFIX = "&";

	//根据bean的名字,获取在IOC容器中得到bean实例
	Object getBean(String name) throws BeansException;

	//根据bean的名字和Class类型来得到bean实例,增加了类型安全验证机制。
	<T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;

	Object getBean(String name, Object... args) throws BeansException;

	<T> T getBean(Class<T> requiredType) throws BeansException;

	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

	//提供对bean的检索,看看是否在IOC容器有这个名字的bean
	boolean containsBean(String name);

	//根据bean名字得到bean实例,并同时判断这个bean是不是单例
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

	//得到bean实例的Class类型
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;

	//得到bean的别名,如果根据别名检索,那么其原名也会被检索出来
	String[] getAliases(String name);
}

FactoryBean也是接口,为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,我们可以在getObject()方法中灵活配置。一般情况下,Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。

BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean。

public interface FactoryBean<T> {

	//从工厂中获取bean
	@Nullable
	T getObject() throws Exception;

	//获取Bean工厂创建的对象的类型
	@Nullable
	Class<?> getObjectType();

	//Bean工厂创建的对象是否是单例模式
	default boolean isSingleton() {
		return true;
	}
}

不同于普通Bean的是:它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。

他们两个都是个工厂,但FactoryBean本质上还是一个Bean,也归BeanFactory管理。BeanFactory是Spring容器的顶层接口,FactoryBean更类似于用户自定义的工厂接口。

5. springboot应用启动流程,有哪些扩展点

链接:《SpringBoot启动流程

6. @Value之类的标签是如何实现的

Spring中@Autowire,@Value 注解实现原理基本一致。

在spring中是由AutowiredAnnotationBeanPostProcessor解析处理@Value注解。AutowiredAnnotationBeanPostProcessor是一个BeanPostProcessor,所以每个类的实例化都过经过AutowiredAnnotationBeanPostProcessor类。当post-processor处理bean时,会解析bean Class的所有属性,在解析时会判断属性上是否标有@Value注解,有就解析这个@Value的属性值,将解析后结果放入AutowiredFieldElement类型InjectionMetaData.checkedElements中,当给属性赋值时会使用checkedElements,从而得到@Value注解的Filed属性,调用AutowiredFieldElement.inject()方法进行解析,解析时会使用DefaultListableBeanFactory(用于解析${})和TypeConverter(用于类型转换),从而得到属性的值,最后调用field.set(bean, value),从而获取的值赋给bean的field。

具体步骤为:将class的标注@Value的所有信息转存InjectionMetadata.InjectedElement集合中。入口为AutowiredAnnotationBeanPostProcessor.buildAutowiringMetadata(Class clazz)。这个方法用于遍历和解析clazz的所有filed和method,解析其上的@Value、@Autowired、@Inject注解,然后放入类型为InjectionMetadata.InjectedElementelements中,elements再放入metadata(=new InjectionMetadata(clazz, elements))中。再将metadata放入缓存injectionMetadataCache中,后面会从缓存中取值

7. spring的动态代理实现有哪些方式?从源码来看是如何实现的?

  1. JDK动态代理
    JDK是通过代理类跟目标类实现同一个接口来实现代理的

  2. CGLIB代理
    CGlib是通过继承目标类来实现代理的

在spring中的实现具体为:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    //这里初步判断代理的创建方式,如果不满足则直接使用 JDK 动态代理,如果满足条件,则进一步在判断是否使用 JKD 动态代理还是 CGLIB 代理
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("......");
        }
        // 如果代理的是接口或者设置代理的类就是当前类,则使用 JDK 动态代理
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        // 否则使用 CGLIB 代理
        return new ObjenesisCglibAopProxy(config);
    }
    // 条件不满足CGBLIB的方式直接使用JDK动态代理
    else {
        return new JdkDynamicAopProxy(config);
    }
}

这里的 if 条件有三个:

  1. config.isOptimize() : 用来控制通过 CGLIB 创建的代理是否使用激进的优化策略,目前仅用于 CGLIB 代理
  2. config.isProxyTargetClass() : 在 Spring AOP 注解方式源码解析 中了解到,我们可以强制 Spring 完全使用 CGLIB 进行代理,只要在配置文件配置 proxy-target-class 属性为true即可,如:<aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"/>,如果配置个该属性,则会使用 CGLIB 来创建代理
  3. hasNoUserSuppliedProxyInterfaces(config) : 是否存在代理接口,如果不存在代理接口,则使用 CGLIB 进行代理

如果这三个条件有一个满足,则会再进一次判断,需要代理的类是否是接口或者是否设置的就是代理当前类,如果是,则还是会使用 JDK 动态代理,否则的话才会使用 CGLIB 代理。

接下来看下 JDK 动态代理和 CGLIB 代理的创建过程:

JDK 动态代理

return new JdkDynamicAopProxy(config);
// JdkDynamicAopProxy.java
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
    this.advised = config;
}

通过 JDK 动态代理来获取代理的方法 getProxy():

public Object getProxy(ClassLoader classLoader) {
    // 获取代理类的接口
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    // 处理 equals , hashcode 方法
    findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    // 创建代理
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

Spring 使用 JDK 创建代理和我们使用的 JDK 来创建代理是没有区别的,都是使用 Proxy.newProxyInstance的方式来创建;我们知道 JDK 动态代理有个invoke方法,用来执行目标方法,而 JdkDynamicAopProxy 实现了 InvocationHandler接口,所有它也会重写该方法,在该方法中植入增强:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    MethodInvocation invocation;
    Object oldProxy = null;
    boolean setProxyContext = false;
    // 目标类
    TargetSource targetSource = this.advised.targetSource;
    Object target = null;

    try {
        // 如果接口没有定义 equals 方法且当前方法是 equals 方法,则不会增强,直接返回
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            return equals(args[0]);
        }
        else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            // 如果接口没有定义 hashCode方法且当前方法是 hashCode方法,则不会增强,直接返回
            return hashCode();
        }
        else if (method.getDeclaringClass() == DecoratingProxy.class) {
            return AopProxyUtils.ultimateTargetClass(this.advised);
        }
        else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                method.getDeclaringClass().isAssignableFrom(Advised.class)) {
            // 如果方法所在的类和Advised是同一个类或者是父类子类关系,则直接执行
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }
        // 返回值
        Object retVal;

        // 这里对应的是expose-proxy属性的应用,把代理暴露处理
        // 目标方法内部的自我调用将无法实施切面中的增强,所以在这里需要把代理暴露出去
        if (this.advised.exposeProxy) {
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);

        // 获取该方法的拦截器
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

        //如果方法的拦截器为空,则直接执行目标方法,避免创建 MethodInvocation 对象
        if (chain.isEmpty()) {
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            // 执行目标方法:method.invoke(target, args)
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
            // 把所有的拦截器封装在ReflectiveMethodInvocation中,以便于链式调用 
            invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // 执行拦截器链
            retVal = invocation.proceed();
        }
        // ..........
        return retVal;
    }
    finally {
      // .            
    }
}

在执行拦截器方法 proceed 中执行增强方法,比如前置增强在方法之前执行,后置增强在方法之后执行,proceed 方法如下:

public Object proceed() throws Throwable {
    //当执行完所有增强方法后执行目标方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // method.invoke(target, args)
        return invokeJoinpoint();
    }
     // 获取下一个要执行的拦截器
    Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // 动态匹配
        InterceptorAndDynamicMethodMatcher dm = interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        // 如果能够匹配,则执行拦截器的方法,
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            // 比如 @After @Before 对应的增强器(拦截器)的方法
            // 比如 @After 对应的增强器 AspectJAfterAdvice 的invoke方法为:MethodInvocation.proceed();
            return dm.interceptor.invoke(this);
        }
        else {
            // 如果动态匹配失败,则跳过该拦截器,执行下一个拦截器
            return proceed();
        }
    }
    else {
        // 普通拦截器,直接调用
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

在该方法中完成了增强的植入,主要逻辑就是,每个方法都会有一个拦截器链,在 AOP 中我们称之为增强,然后循环执行每个拦截器链,当执行完所有的拦截器后,才会执行目标方法。比如 @After对应的增强器AspectJAfterAdvice@Around对应的增强器AspectJAroundAdvice等。

以上就是 Spring 通过 JDK 动态代理来实现 AOP 的一个过程。

CGLIB 代理
ObjenesisCglibAopProxy继承于CglibAopProxy

return new ObjenesisCglibAopProxy(config)

public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
    this.advised = config;
    this.advisedDispatcher = new AdvisedDispatcher(this.advised);
}
public Object getProxy(ClassLoader classLoader) {
    // 代理的目标类
    Class<?> rootClass = this.advised.getTargetClass();
    Class<?> proxySuperClass = rootClass;
    if (ClassUtils.isCglibProxyClass(rootClass)) {
        proxySuperClass = rootClass.getSuperclass();
        Class<?>[] additionalInterfaces = rootClass.getInterfaces();
        for (Class<?> additionalInterface : additionalInterfaces) {
            this.advised.addInterface(additionalInterface);
        }
    }
    // 创建并配置 CGLIB Enhancer
    Enhancer enhancer = createEnhancer();
    if (classLoader != null) {
        enhancer.setClassLoader(classLoader);
        if (classLoader instanceof SmartClassLoader &&((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
            enhancer.setUseCache(false);
        }
    }
    enhancer.setSuperclass(proxySuperClass);
    enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
    enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
    enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

    // 设置拦截器
    Callback[] callbacks = getCallbacks(rootClass);
    Class<?>[] types = new Class<?>[callbacks.length];
    for (int x = 0; x < types.length; x++) {
        types[x] = callbacks[x].getClass();
    }
    enhancer.setCallbackFilter(new ProxyCallbackFilter(this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
    enhancer.setCallbackTypes(types);

    //生成代理类和创建代理
    return createProxyClassAndInstance(enhancer, callbacks);
}

// 生成代理类和创建代理
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    enhancer.setInterceptDuringConstruction(false);
    enhancer.setCallbacks(callbacks);
    return (this.constructorArgs != null && this.constructorArgTypes != null ?
            enhancer.create(this.constructorArgTypes, this.constructorArgs) :
            enhancer.create());
}

从上述的方法可知,Sping 使用 CGLIB 来创建代理类和代理对象和我们使用的一样,都是使用 Enhancer.create() 来创建,这里主要的是设置拦截器,通过 getCallbacks () 方法来实现的,如下:

private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
    //expose-proxy 属性
    boolean exposeProxy = this.advised.isExposeProxy();
    boolean isFrozen = this.advised.isFrozen();
    boolean isStatic = this.advised.getTargetSource().isStatic();

    // 将拦截器封装在 DynamicAdvisedInterceptor 中
    Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

    //暴露代理
    Callback targetInterceptor;
    if (exposeProxy) {
        targetInterceptor = (isStatic ?
                new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
                new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()));
    }
    else {
        targetInterceptor = (isStatic ?
                new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
                new DynamicUnadvisedInterceptor(this.advised.getTargetSource()));
    }
    // 将拦截器 aopInterceptor 进入到 Callback 中 
    Callback[] mainCallbacks = new Callback[] {
            aopInterceptor,  // for normal advice
            targetInterceptor,  // invoke target without considering advice, if optimized
            new SerializableNoOp(),  // no override for methods mapped to this
            targetDispatcher, this.advisedDispatcher,
            new EqualsInterceptor(this.advised),
            new HashCodeInterceptor(this.advised)
    };
    // ..............
    return callbacks;
}

使用 CGLIB 来实现代理功能的时候,当代理执行的时候,会调用intercept方法,和 JKD 动态代理的 invoke 方法类似;Spring 中 CGLIB 的 intercept 方法如下,该方法在 DynamicAdvisedInterceptor中,从上面的代理知道,使用它来封装拦截器,它是 CglibAopProxy的一个子类:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy){
    Object oldProxy = null;
    boolean setProxyContext = false;
    Object target = null;
    // 目标类
    TargetSource targetSource = this.advised.getTargetSource();
    // 处理 expose-proxy 属性,暴露代理
    if (this.advised.exposeProxy) {
        oldProxy = AopContext.setCurrentProxy(proxy);
        setProxyContext = true;
    }
    target = targetSource.getTarget();
    Class<?> targetClass = (target != null ? target.getClass() : null);

    // 获取拦截器链
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

    // 返回值
    Object retVal;

    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        // 如果拦截器为空则直接执行目标方法
        retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
        //封装拦截器链并执行
        retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    // 处理返回值类型
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
    // .....................
}

CGLIB 使用CglibMethodInvocation来封装拦截器链,它是 CglibAopProxy 的一个内部类:

private static class CglibMethodInvocation extends ReflectiveMethodInvocation {

    @Nullable
    private final MethodProxy methodProxy;

    public CglibMethodInvocation(Object proxy, Object target, Method method,Object[] arguments, Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) {

        super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers);

        this.methodProxy = (Modifier.isPublic(method.getModifiers()) &&
                method.getDeclaringClass() != Object.class && !AopUtils.isEqualsMethod(method) &&
                !AopUtils.isHashCodeMethod(method) && !AopUtils.isToStringMethod(method) ?
                methodProxy : null);
    }

    // proceed 方法会调用该方法来执行
    @Override
    protected Object invokeJoinpoint() throws Throwable {
        if (this.methodProxy != null) {
            return this.methodProxy.invoke(this.target, this.arguments);
        }
        else {
            return super.invokeJoinpoint();
        }
    }
}

当调用proceed 方法时,和 JDK 的处理是一样的,只不过当执行完所有的拦截器后,执行目标方法调用的是 CglibMethodInvocation 的 invokeJoinpoint来执行而已;

因为 CglibMethodInvocation 继承于 ReflectiveMethodInvocation ,而 JDK 使用的就是 ReflectiveMethodInvocation 来执行的,ReflectiveMethodInvocation 的 invokeJoinpoint 方法为 : method.invoke(target, args)。

8. Mybatis一级缓存是如何实现的?SqlSession级别

一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是 SqlSession 之间的缓存数据区(HashMap)是互相不影响。

SqlSession 是一个接口,提供了一些 CRUD 的方法,而 SqlSession 的默认实现类是 DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,也就是上图的 Local Cache。
当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。一级缓存的底层数据结构就是 HashMap,每一个 SqlSession 都会存放一个 map 对象的引用。每次执行 update 前都会清空 localCache。

二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度更加的细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。

9. Mybatis声明interface加上注解即可被service注入使用,是怎么实现的

  1. 使用@MapperScan注解扫描Mapper接口,这个注解由mybatis-spring提供,配合MapperScannerRegistrar和MapperScannerConfigurer,可以实现编码方式为Mapper接口创建代理,并注册到Spring容器。
  2. 导入MapperScannerRegistrar类,MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,在registerBeanDefinitions方法中,他将MapperScannerConfigurer类注册到了Spring容器中,而MapperScannerConfigurer类实现了BeanDefinitionRegistryPostProcessor接口,而MapperScannerConfigurer就是在这个postProcessBeanDefinitionRegistry方法中扫描所有的Mapper接口并且注册FactoryBean bean definition的。

一条小咸鱼