目录

  • JDK、JRE、JVM之间的区别hashCode()与equals()之间的关系
  • String、StringBuffer、StringBuilder的区别泛型中extends和super的区别
  • ==和equals⽅法的区别重载和重写的区别
  • List和Set的区别
  • ArrayList和LinkedList区别
  • 谈谈ConcurrentHashMap的扩容机制
  • Jdk . 到Jdk . HashMap 发⽣了什么变化(底层)? 说⼀下HashMap的Put⽅法
  • 深 拷 ⻉ 和 浅 拷 ⻉ HashMap的扩容机制原理
  • CopyOnWriteArrayList的底层原理是怎样的什么是字节码?采⽤字节码的好处是什么? Java中的异常体系是怎样的
  • 在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常? Java中有哪些类加载器
  • 说说类加载器双亲委派模型JVM中哪些是线程共享区 你们项⽬如何排查JVM问题
  • ⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程? 怎么确定⼀个对象到底是不是垃圾?
  • JVM有哪些垃圾回收算法? 什么是STW?
  • JVM参数有哪些?
  • 说说对线程安全的理解对守护线程的理解
  • ThreadLocal的底层原理
  • 并发、并⾏、串⾏之间的区别Java死锁如何避免?
  • 线程池的底层⼯作原理
  • 线程池为什么是先添加列队⽽不是先创建最⼤线程? ReentrantLock中的公平锁和⾮公平锁的底层实现ReentrantLock中tryLock()和lock()⽅法的区别CountDownLatch和Semaphore的区别和底层原理Sychronized的偏向锁、轻量级锁、重量级锁Sychronized和ReentrantLock的区别
  • 谈谈你对AQS的理解,AQS如何实现可重⼊锁? 谈谈你对IOC的理解
  • 单例Bean和单例模式Spring事务传播机制
  • Spring事务什么时候会失效?
  • Spring中的Bean创建的⽣命周期有哪些步骤Spring中Bean是线程安全的吗ApplicationContext和BeanFactory有什么区别Spring中的事务是如何实现的
  • Spring中什么时候@Transactional会失效Spring容器启动流程是怎样的
  • Spring⽤到了哪些设计模式
  • Spring Boot中常⽤注解及其底层实现Spring Boot是如何启动Tomcat的Mybatis的优缺点
  • #{}和${}的区别是什么? 索引的基本原理
  • 索引设计的原则?
  • 事务的基本特性和隔离级别什么是MVCC
  • 简述MyISAM和InnoDB的区别
  • Explain语句结果中各个字段分表表示什么
  • 索引覆盖是什么
  • 最左前缀原则是什么Innodb是如何实现事务的
  • B树和B+树的区别,为什么Mysql使⽤B+树Mysql锁有哪些,如何理解
  • Mysql慢查询该如何优化? 什么是RDB和AOF
  • Redis的过期键的删除策略简述Redis事务实现
  • Redis 主从复制的核⼼原理
  • Redis有哪些数据结构?分别有哪些典型的应⽤场景? Redis分布式锁底层是如何实现的?
  • Redis主从复制的核⼼原理Redis集群策略
  • 缓存穿透、缓存击穿、缓存雪崩分别是什么Redis和Mysql如何保证数据⼀致
  • Redis的持久化机制
  • Redis单线程为什么这么快简述Redis事务实现
  • 什么是CAP理论什么是BASE理论什么是RPC
  • 数据⼀致性模型有哪些
  • 分布式ID是什么?有哪些解决⽅案?
  • 分布式锁的使⽤场景是什么?有哪些实现⽅案? 什么是分布式事务?有哪些实现⽅案?
  • 什么是ZAB协议
  • 为什么Zookeeper可以⽤来作为注册中⼼Zookeeper中的领导者选举的流程是怎样的? Zookeeper集群中节点之间数据是如何同步的Dubbo⽀持哪些负载均衡策略
  • Dubbo是如何完成服务导出的?
  • Dubbo是如何完成服务引⼊的? Dubbo的架构设计是怎样的? 负载均衡算法有哪些
  • 分布式架构下,Session 共享有什么⽅案如何实现接⼝的幂等性
  • 简述zk的命名服务、配置管理、集群管理 讲下Zookeeper中的watch机制Zookeeper和Eureka的区别
  • 存储拆分后如何解决唯⼀主键问题雪花算法原理
  • 如何解决不使⽤分区键的查询问题
  • Spring Cloud有哪些常⽤组件,作⽤是什么? 如何避免缓存穿透、缓存击穿、缓存雪崩? 分布式系统中常⽤的缓存⽅案有哪些
  • 缓存过期都有哪些策略? 常⻅的缓存淘汰算法
  • 布隆过滤器原理,优缺点分布式缓存寻址算法
  • Spring Cloud和Dubbo有哪些区别? 什么是服务雪崩?什么是服务限流?
  • 什么是服务熔断?什么是服务降级?区别是什么? SOA、分布式、微服务之间有什么关系和区别? 怎么拆分微服务?
  • 怎样设计出⾼内聚、低耦合的微服务? 有没有了解过DDD领域驱动设计?
  • 什么是中台?
  • 你的项⽬中是怎么保证微服务敏捷开发的? 如何进⾏消息队列选型?
  • RocketMQ的事务消息是如何实现的
  • 为什么RocketMQ不使⽤Zookeeper作为注册中⼼呢? RocketMQ的实现原理
  • RocketMQ为什么速度快
  • 消息队列如何保证消息可靠传输消息队列有哪些作⽤
  • 死信队列是什么?延时队列是什么? 如何保证消息的⾼效读写?
  • epoll和poll的区别
  • TCP的三次握⼿和四次挥⼿
  • 浏览器发出⼀个请求到收到响应经历了哪些步骤? 跨域请求是什么?有什么问题?怎么解决?
  • 零拷⻉是什么

答案

JDK、JRE、JVM之间的区别

  • JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运⾏Java程序所需的各种⼯具和资源,包括Java编译器、Java运⾏时环境,以及常⽤的Java类库等
  • JRE( Java Runtime Environment) ,Java运⾏环境,⽤于运⾏Java的字节码⽂件。JRE中包括了JVM以及JVM⼯作所需要的类库,普通⽤户⽽只需要安装JRE来运⾏Java程序,⽽程序开发者必须安装JDK来编译、调试程序。
  • JVM(Java Virtual Mechinal),Java虚拟机,是JRE的⼀部分,它是整个java实现跨平台的最核⼼的部分,负责运⾏字节码⽂件。

我们写Java代码,⽤txt就可以写,但是写出来的Java代码,想要运⾏,需要先编译成字节码,那就需要编译器,⽽JDK中就包含了编译器javac,编译之后的字节码,想要运⾏,就需要⼀个可以执⾏字节码的程序,这个程序就是JVM(Java虚拟机),专⻔⽤来执⾏Java字节码的。

如果我们要开发Java程序,那就需要JDK,因为要编译Java源⽂件。

如果我们只想运⾏已经编译好的Java字节码⽂件,也就是*.class⽂件,那么就只需要JRE。 JDK中包含了JRE,JRE中包含了JVM。

另外,JVM在执⾏Java字节码时,需要把字节码解释为机器指令,⽽不同操作系统的机器指令是有可能不⼀样的,所以就导致不同操作系统上的JVM是不⼀样的,所以我们在安装JDK时需要选择操作系统。 另外,JVM是⽤来执⾏Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运

⾏,⽐如Apache GroovyScala and Kotlin 等等。

hashCode()与equals()之间的关系

在Java中,每个对象都可以调⽤⾃⼰的hashCode()⽅法得到⾃⼰的哈希值(hashCode),相当于对象的 指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可 以利⽤hashCode来做⼀些提前的判断,⽐如:

  • 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
  • 如果两个对象的hashCode相同,不代表这两个对象⼀定是同⼀个对象,也可能是两个对象
  • 如果两个对象相等,那么他们的hashCode就⼀定相同

在Java的⼀些集合类的实现中,在⽐较两个对象是否相等时,会根据上⾯的原则,会先调⽤对象的hashCode()⽅法得到hashCode进⾏⽐较,如果hashCode不相同,就可以直接认为这两个对象不相

同,如果hashCode相同,那么就会进⼀步调⽤equals()⽅法进⾏⽐较。⽽equals()⽅法,就是⽤来最终确定两个对象是不是相等的,通常equals⽅法的实现会⽐较重,逻辑⽐较多,⽽hashCode()主要就是得到⼀个哈希值,实际上就⼀个数字,相对⽽⾔⽐较轻,所以在⽐较两个对象时,通常都会先根据hashCode想⽐较⼀下。

所以我们就需要注意,如果我们重写了equals()⽅法,那么就要注意hashCode()⽅法,⼀定要保证能遵守上述规则。

String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是可变的
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更⾼

泛型中extends和super的区别

  1. <? extends T>表示包括T在内的任何T的⼦类
  2. <? super T>表示包括T在内的任何T的⽗类

==和equals⽅法的区别

● ==:如果是基本数据类型,⽐较是值,如果是引⽤类型,⽐较的是引⽤地址

  • equals:具体看各个类重写equals⽅法之后的⽐较逻辑,⽐如String类,虽然是引⽤类型,但是String类中重写了equals⽅法,⽅法内部⽐较的是字符串中的各个字符是否全部相等。

重载和重写的区别

  • 重载(Overload): 在⼀个类中,同名的⽅法如果有不同的参数列表(⽐如参数类型不同、参数个数不同)则视为重载。
  • 重写(Override): 从字⾯上看,重写就是 重新写⼀遍的意思。其实就是在⼦类中把⽗类本身有的⽅法重新写⼀遍。⼦类继承了⽗类的⽅法,但有时⼦类并不想原封不动的继承⽗类中的某个⽅法,所 以在⽅法名,参数列表,返回类型都相同(⼦类中⽅法的返回值可以是⽗类中⽅法返回值的⼦类)的 情况下, 对⽅法体进⾏修改,这就是重写。但要注意⼦类⽅法的访问修饰权限不能⼩于⽗类的。

List和Set的区别

  • List:有序,按对象插⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使⽤Iterator取出所有元素,在逐⼀遍历,还可以使⽤get(int index)获取指定下标的元素
  • Set:⽆序,不可重复,最多允许有⼀个Null元素对象,取元素时只能⽤Iterator接⼝取得所有元素,在逐⼀遍历各个元素

ArrayList和LinkedList区别

  1. ⾸先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
  2. 由于底层数据结构不同,他们所适⽤的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
  3. 另外ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以LinkedList还可以当做队列来使⽤

谈谈ConcurrentHashMap的扩容机制

  • 版本1.7版本的ConcurrentHashMap是基于Segment分段实现的每个Segment相对于⼀个⼩型的HashMap每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似先⽣成新的数组,然后转移元素到新数组中扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值版本1.8版本的ConcurrentHashMap不再基于Segment实现当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进⾏扩容ConcurrentHashMap是⽀持多个线程同时扩容的扩容之前也先⽣成⼀个新的数组在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作

Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率
  2. 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要 判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
  3. 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希 算法,节省CPU资源

说⼀下HashMap的Put⽅法

先说HashMap的Put⽅法的⼤体流程:

  1. 根据Key通过哈希算法与与运算得出数组下标
  2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中 是Node对象)并放⼊该位置
  3. 如果数组下标位置元素不为空,则要分情况讨论
  • 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中
  • 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node

ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value

ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会 判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链 表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成 红⿊树

ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法

深拷⻉和浅拷⻉

深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。

  1. 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
  2. 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象

HashMap的扩容机制原理

  • 版本先⽣成新数组遍历⽼数组中的每个位置上的链表上的每个元素取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标将元素添加到新数组中去所有元素转移完了之后,将新数组赋值给HashMap对象的table属性版本先⽣成新数组遍历⽼数组中的每个位置上的链表或红⿊树如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置统计每个下标位置的元素个数如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

CopyOnWriteArrayList的底层原理是怎样的

  1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
  2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应

⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很⾼的场景

什么是字节码?采⽤字节码的好处是什么?

编译器(javac)将Java源⽂件(*.java)⽂件编译成为字节码⽂件(*.class),可以做到⼀次编译到处运⾏, windows上编译好的class⽂件,可以直接在linux上运⾏,通过这种⽅式做到跨平台,不过Java的跨平台有⼀个前提条件,就是不同的操作系统上安装的JDK或JRE是不⼀样的,虽然字节码是通⽤的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各⾃ 的JDK或JRE。

采⽤字节码的好处,⼀⽅⾯实现了跨平台,另外⼀⽅⾯也提⾼了代码执⾏的性能,编译器在编译源代码 时可以做⼀些编译期的优化,⽐如锁消除、标量替换、⽅法内联等。

Java中的异常体系是怎样的

  • Java中的所有异常都来⾃顶级⽗类Throwable。
  • Throwable下有两个⼦类Exception和Error。
  • Error表示⾮常严重的错误,⽐如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError, 通常这些错误出现时,仅仅想靠程序⾃⼰是解决不了的,可能是虚拟机、磁盘、操作系统层⾯出现 的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不⼤,因为程序可能已经 根本运⾏不了了。
  • Exception表示异常,表示程序出现Exception时,是可以靠程序⾃⼰来解决的,⽐如NullPointerException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。
  • Exception的⼦类通常⼜可以分为RuntimeException和⾮RuntimeException两类
  • RunTimeException表示运⾏期异常,表示这个异常是在代码运⾏过程中抛出的,这些异常是⾮检查

异常,程序中可以选择捕获处理,也可以不处理。这些异常⼀般是由程序逻辑错误引起的,程序应该从逻辑⻆度尽可能避免这类异常的发⽣,⽐如NullPointerException、IndexOutOfBoundsException等。

  • ⾮RuntimeException表示⾮运⾏期异常,也就是我们常说的检查异常,是必须进⾏处理的异常,如果

不处理,程序就不能检查异常通过。如IOException、SQLException等以及⽤户⾃定义的Exception异常。

在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?

异常相当于⼀种提示,如果我们抛出异常,就相当于告诉上层⽅法,我抛了⼀个异常,我处理不了这个异常,交给你来处理,⽽对于上层⽅法来说,它也需要决定⾃⼰能不能处理这个异常,是否也需要交给它的上层。

所以我们在写⼀个⽅法时,我们需要考虑的就是,本⽅法能否合理的处理该异常,如果处理不了就继续 向上抛出异常,包括本⽅法中在调⽤另外⼀个⽅法时,发现出现了异常,如果这个异常应该由⾃⼰来处理,那就捕获该异常并进⾏处理。

Java中有哪些类加载器

JDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

  • BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的 jar包和class⽂件。
  • ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的jar包和class类。
  • AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。

说说类加载器双亲委派模型

JVM中存在三个默认的类加载器:BootstrapClassLoader

  1. ExtClassLoader  


AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是BootstrapClassLoader。

JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。

所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰进⾏加载。

JVM中哪些是线程共享区

堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的

你们项⽬如何排查JVM问题

对于还在正常运⾏的系统:

  1. 可以使⽤jmap来查看JVM中各个区域的使⽤情况
  2. 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了
  4. 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
  5. ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表 示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻 代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效
  6. 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存

对于已经发⽣了OOM的系统:

  1. ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base
  2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
  3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
  4. 然后再进⾏详细的分析和调试

总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?

  1. ⾸先把字节码⽂件内容加载到⽅法区
  2. 然后再根据类信息在堆区创建对象
  3. 对象⾸先会分配在堆区中年轻代的Eden区,经过⼀次Minor GC后,对象如果存活,就会进⼊

Suvivor区。在后续的每次Minor GC中,如果对象⼀直存活,就会在Suvivor区来回拷⻉,每移动⼀次,年龄加1

  1. 当年龄超过15后,对象依然存活,对象就会进⼊⽼年代
  2. 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉

怎么确定⼀个对象到底是不是垃圾?

  1. 引⽤计数算法: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
  2. 可达性算法: 这种⽅式是在内存中,从根对象向下⼀直找引⽤,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM有哪些垃圾回收算法?

  1. 标记清除算法:
  • 标记阶段:把垃圾内存标记出来
  • 清除阶段:直接将垃圾内存回收。
  • 这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
  1. 复制算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩ 相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽ 且,他的效率跟存活对象的个数有关。
  2. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清 除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将边界以外的所有内存直接清除。

什么是STW?

STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将JVM内存冻结的⼀种状态。在STW 状态下,JAVA的所有线程都是停⽌执⾏的-GC线程除外,native⽅法可以执⾏,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

JVM参数有哪些?

JVM参数⼤致可以分为三类:

  1. 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
  2. ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
  3. 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。

说说对线程安全的理解

线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到 正常的结果,⽐如i++,i初始化值为0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那 么最终的结果应该就是⼀个线程的结果为1,⼀个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。

所以线程安全,主要指的是⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果。

对守护线程的理解

线程分为⽤户线程和守护线程,⽤户线程就是普通线程,守护线程就是JVM的后台线程,⽐如垃圾回收 线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭。我们可以通过设置thread.setDaemon(true)来把⼀个线程设置为守护线程。

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
  4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)

并发、并⾏、串⾏之间的区别

  1. 串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
  2. 并⾏(Parallelism):两个任务同时执⾏
  3. 并发(Concurrency):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后

⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的

Java死锁如何避免?

造成死锁的⼏个原因:

  1. ⼀个资源每次只能被⼀个线程使⽤
  2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
  3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
  4. 若⼲线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决

线程池的底层⼯作原理

线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

  1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
  3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数

线程池为什么是先添加列队⽽不是先创建最⼤线程?

当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之 后,才会新开线程。这就相当于,⼀个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中,然后这 10个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员⼯了。

ReentrantLock中的公平锁和⾮公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使

⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。

不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。


ReentrantLock中tryLock()和lock()⽅法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。

Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

谈谈你对AQS的理解,AQS如何实现可重⼊锁?

  1. AQS是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。
  2. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下, 有不⽤的意义。
  3. 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释放锁state就减1。

谈谈你对IOC的理解

通常,我们认为Spring有两⼤特性IoC和AOP,那到底该如何理解IoC呢?

对于很多初学者来说,IoC这个概念给⼈的感觉就是我好像会,但是我说不出来。

那么IoC到底是什么,接下来来说说我的理解,实际上这是⼀个⾮常⼤的问题,所以我们就把它拆细了来回答,IoC表示控制反转,那么:

  1. 什么是控制?控制了什么?
  2. 什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
  3. 为什么要反转?反转之前有什么问题?反转之后有什么好处? 这就是解决这⼀类⼤问题的思路,⼤⽽化⼩。

那么,我们先来解决第⼀个问题:什么是控制?控制了什么? 我们在⽤Spring的时候,我们需要做什么:

  1. 建⼀些类,⽐如UserService、OrderService
  2. ⽤⼀些注解,⽐如@Autowired

但是,我们也知道,当程序运⾏时,⽤的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象⾥的属性是什么时候赋的值?谁赋的?所有这些都是我们程序 员做的,以为我们只是写了类⽽已,所有的这些都是Spring做的,它才是幕后⿊⼿。

这就是控制:

  1. 控制对象的创建
  2. 控制对象内属性的赋值

如果我们不⽤Spring,那我们得⾃⼰来做这两件事,反过来,我们⽤Spring,这两件事情就不⽤我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(⽐如某个属性上加@Autowired),⽽这其实就是第⼆个问题的答案,这就是反转,表示⼀种对象控制权的转移。

那反转有什么⽤,为什么要反转?

如果我们⾃⼰来负责创建对象,⾃⼰来给对象中的属性赋值,会出现什么情况?

⽐如,现在有三个类:

  1. A类,A类⾥有⼀个属性C c;
  2. B类,B类⾥也有⼀个属性C c;
  3. C类

现在程序要运⾏,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:

A a = new A(); B b = new B(); C c = new C();

a.c = c;

b.c = c;

这五⾏代码是不⽤Spring的情况下多出来的代码,⽽且,如果类在多⼀些,类中的属性在多⼀些,那相应的代码会更多,⽽且代码会更复杂。所以我们可以发现,我们⾃⼰来控制⽐交给Spring来控制,我们 的代码量以及代码复杂度是要⾼很多的,反⾔之,将对象交给Spring来控制,减轻了程序员的负担。

总结⼀下,IoC表示控制反转,表示如果⽤Spring,那么Spring会负责来创建对象,以及给对象内的属 性赋值,也就是如果⽤Spring,那么对象的控制权会转交给Spring。

单例Bean和单例模式

单例模式表示JVM中某个类的对象只会存在唯⼀⼀个。

⽽单例Bean并不表示JVM中只能存在唯⼀的某个类的Bean对象。

Spring事务传播机制

多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法A是⼀个事务的⽅法,⽅法A执⾏过程中调

⽤了⽅法B,那么⽅法B有⽆事务以及⽅法B对事务的要求不同都会对⽅法A的事务具体执⾏造成影响, 同时⽅法A的事务对⽅法B的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类 型所决定。

  1. REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存在事务,则加⼊这个事务
  2. SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏
  3. MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。
  4. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。
  5. NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务
  6. NEVER:不使⽤事务,如果当前事务存在,则抛出异常
  7. NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则REQUIRED的操作⼀样(开启⼀个事 务)

Spring事务什么时候会失效?

spring事务的原理是AOP,进⾏了切⾯增强,那么失效的根本原因是这个AOP不起作⽤了!常⻅情况有如下⼏种

1、发⽣⾃调⽤,类⾥⾯使⽤this调⽤本类的⽅法(this通常省略),此时这个this对象不是代理类,⽽是UserService对象本身!

解决⽅法很简单,让那个this变成UserService的代理类即可!

2、⽅法不是public的:@Transactional 只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在

⾮ public ⽅法上,可以开启 AspectJ 代理模式。3、数据库不⽀持事务

4、没有被spring管理

5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

Spring中的Bean创建的⽣命周期有哪些步骤

Spring中⼀个Bean的创建⼤概分为以下⼏个步骤:

  1. 推断构造⽅法
  2. 实例化
  3. 填充属性,也就是依赖注⼊
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitializingBean接⼝
  7. 初始化后,进⾏AOP

当然其实真正的步骤更加细致,可以看下⾯的流程图


Spring中Bean是线程安全的吗

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果Bean是⽆状态的,那么Bean则是线程安全的
  2. 如果Bean是有状态的,那么Bean则不是线程安全的

另外,Bean是不是线程安全,跟Bean的作⽤域没有关系,Bean的作⽤域只是表示Bean的⽣命周期范围,对于任何⽣命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。

ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的

Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制的

  • ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
  • 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
  • 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
  • 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重要的⼀步
  • 然后执⾏当前⽅法,⽅法中会执⾏sql
  • 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
  • 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  • Spring事务的隔离级别对应的就是数据库的隔离级别
  • Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
  • Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql

Spring中什么时候@Transactional会失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是不会失效的。

同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效

Spring容器启动流程是怎样的

  • 在创建Spring容器,也就是启动Spring时:
  • ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
  • 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建

    • 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化 后这⼀步骤中
    • 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
    • Spring启动结束
    • 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的
    • 在Spring启动过程中还会去处理@Import等注解

    Spring⽤到了哪些设计模式


  • 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化 后这⼀步骤中
  • 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
  • Spring启动结束
  • 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的
  • 在Spring启动过程中还会去处理@Import等注解

Spring⽤到了哪些设计模式

Spring Boot中常⽤注解及其底层实现

  • @SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的组合,这三个注解是:
  • @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配置类
  • @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
  • @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前⽬录
  • @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注 解的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
  • @Controller、@Service、@ResponseBody、@Autowired都可以说

Spring Boot是如何启动Tomcat的

  1. ⾸先,SpringBoot在启动时会先创建⼀个Spring容器
  2. 在创建Spring容器过程中,会利⽤@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会⽣成⼀个启动Tomcat的Bean
  3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端⼝等,然后启动Tomcat

Mybatis的优缺点

优点:

  1. 基于 SQL 语句编程,相当灵活,不会对应⽤程序或者数据库的现有设计造成任何影响,SQL 写在XML ⾥,解除 sql 与程序代码的耦合,便于统⼀管理;提供 XML 标签, ⽀持编写动态 SQL 语句, 并可重⽤。
  2. 与 JDBC 相⽐,减少了 50%以上的代码量,消除了 JDBC ⼤量冗余的代码,不需要⼿动开关连接;
  3. 很好的与各种数据库兼容( 因为 MyBatis 使⽤ JDBC 来连接数据库,所以只要JDBC ⽀持的数据库 MyBatis 都⽀持)。
  4. 能够与 Spring 很好的集成;
  5. 提供映射标签, ⽀持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, ⽀持对象关系组件维护。

缺点:

  1. SQL 语句的编写⼯作量较⼤, 尤其当字段多、关联表多时, 对开发⼈员编写SQL 语句的功底有⼀定要求。
  2. SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。

#{}和${}的区别是什么?

#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调⽤ PreparedStatement 来赋值; Mybatis在处理${}时,会将sql中的${}替换成变量的值,调⽤ Statement 来赋值;

使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。

索引的基本原理

索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。 索引的原理:就是把⽆序的数据变成有序的查询

  1. 把创建了索引的列的内容进⾏排序
  2. 对排序结果⽣成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据
需要相关知识资料的小伙伴,点击这里即可