Java面试必备八股文

文章目录

一、Java基础篇

1.1)Java有哪几种数据类型

  • 基本数据类型:byte(1字节) short(2字节) int(4字节) long(8字节) float(4字节) double(8字节) char(2字节) boolean(1字节)
  • 引用数据类型:String 类 接口 抽象类 枚举 数组

1.2)JVM、JRE和JDK的关系

JVM指的是Java的虚拟机,Java程序需要运行在虚拟机上,不同的平台都有自己的虚拟机,所以Java语言实现了跨平台。

JRE指的是Java的运行时环境,包括需要的大量的类库和Java的虚拟机。

JDK指的运行时候的需要的一些工具类和运行时环境,比如包括javac.exe ,javadoxc.exe 一系列用于编译字节码工具 打包文档的工具

1.3)Switch支持的数据类型?

jdk1.5之前 byte、short、int、char

jdk5 ~ jdk1.7 加入了enum

jdk1.7之后 加入了String

*注意 不支持long类型

1.4)为什么float=3.4报错

3.4 默认是浮点double类型的,如果赋值给float是向下转型,会出现精度缺失,,需要强制转换

1.5)final 有什么用?

用于修饰类,方法,变量(属性):

  • 如果修饰类,就是不能够继承
  • 如果修饰方法就是不能够重写
    • 修饰变量:修饰的变量是不能够修改的,如果是基本类型那么就是数值不能修改,如果是引用类型就是地址不能够修改。
    • 成员变量:必须实现赋值
    • 局部变量:声明的时候可以不赋值,后续允许一次赋值,赋值之后不能够修改

1.6)String有哪些特性

首先String是 private final char[]

  • 数组 所以长度不可变
  • final 不能够被继承
  • private 内容不能够修改

1.6)Stringbuffer和 Stringbuilder有什么不同?

首先他们都是继承AbstractStringBuilder,相对于String来说是可变的字符串,只不过Stringbuffer加了synchronized所以是线程安全的,而Stringbuilder是线程非安全的

1.7)== 和 equals 的区别

== 默认是比较两个对象的引用地址的,而equals默认情况是==比较规则,但是不同的类会重写掉Object类的equals从而改变了equals的比较规则,比如String类的equals方法就是比较他们两个的内容是否相等

1.8)hashCode和equals

两个对象相等他们的hashCode和equals一定相等,但是hashCode相等的两个对象未必相等

1.9)方法重载和方法重写区别

  • 实现方式:方法重载是在一个类当中;而方法重写是在父子类中实现的
  • 方法重载是方法名字相同,参数个数,参数类型不同。和访问修饰符 和 返回类型无关;方法重写是方法名字相同,参数个数,参数类型必须相同,子类的返回值,抛出的异常必须小于等于父类。子类的访问修饰符大于等于父类。

1.10)面向对象和面向过程的区别

面向过程:就是具体化的一步一步的去实现,优点 就是性能比较快因为不需要进行实例化。 缺点就是不容易进行维护,扩展和复用

面向对象:因为面向对象的三大特点就是 封装、继承和多态

  • 封装就是增加了代码的安全性,开发者不用在乎细节,只知道如何使用就可以了
  • 继承就是代码的复用,结果设计模式就可以达到已于扩展和复用的特点
  • 多态使用比较灵活,可以设计出低耦合的系统从而便于维护

1.11)ArrayList 和LinkedList 的区别

  • 数据结构:

    • ArrayList是底层是数组存储的,用于一大块连续的空间进行存储的,默认的初始容量是10 一般情况下当容量不够的时候会进行1.5 倍的一个扩容,因为是数组形式长度是不变的,所以在扩容的时候需要数据的搬迁,从而导致频繁的扩容会导致性能效率,所以我们在使用时候最好指定好大小。 因为有索引所以在查找、修改数据时候都是O(1)时间复杂度,而数据的增加删除确要移动后面的元素,所以时间复杂度是O(n)。
    • LinkedList是双向链表的形式存储的,可以充分用碎片化的空间进行存储,查找,修改数据的时间复杂度都是O(n),因为都是要遍历整个链表。插入和删除除非是在指定的指针后面下进行插入和删除不然还是要遍历整个链表。所以说我们通常使用ArrayList进行数据存储。

1.12)说一说HashMap数据结构

HashMap通常是key-value键值对的方式进行存储,在JDK1.7的时候就是通过数组+链表的形式存储,每个数组存储的是一个一个的Entity组成的链表,这些链表上的数字都有一个特点就是Hash值相同,当数据量比较大的时候链表的长度比较大,这个时候查找的时间复杂度就比较大所以。在JDK1.8的时候引入了红黑树概念,当一个链表上的长度大于8的时候并且数组长度大于64这个时候链表就自动转换成红黑树,如果数组长度没有达到64那么此时就对数组进行1倍的扩容。

而且在JDK1.7和1.8还有一个改动就是插入的方式,由于JDK1.7的时候插入方式是头插法,在多线程的方式下会出选循环链表情况,所以1.8改为了尾插法方式。

扩容有三个地方

  • 初始化数组长度为0
  • 元素个数达到了 最大个数*0.75
  • 单个链表的长度大于8并且数组长度大于64

HashMap是线程不安全的,如果要保证线程安全那么可以使用ConcurrentHashMap

1.13)ConcurrentHashMap原如何保证的线程安全?

JDK 1.7 时候是使用分成16个Seagment段,每个Seagment里面存储着一个HashMap和一个锁,所以说1.7能支持最大的并发量也就是16个线程

JDK1.8采用的CAS + Synchronized,每次插入的时候判断是否是第一次插入,是就通过CAS插入,然后判断f.hash是否=-1,如果是的那么其他线程在扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保障并发下删除元素的安全

1.14)抽象类和接口的区别

继承:抽象类是只能单继承的,接口是可以多实现的

属性:属性修饰符也不一样,抽象累可以是public protect 默认不写 还有final 修饰,但是接口只能是public static final修饰

构造方法:抽象类是可以进行实例化的,而接口是不能进行实例化

方法:抽象类既可以抽象的方法,也可以具体的方法,接口只有抽象的方法,而且子类必须实现

二、多线程篇

2.1) *简述线程、进程的基本概念。以及他们之间关系是什么

  • 进程:是程序的一次执行的过程,是系统运行的基本单位,其中包含着程序运行过程中一些内存空间和系统资源。进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
  • 线程:是进程的更小的单位。一个进程中包含着多个线程。和进程不同的是线程是共享着进程中的内存空间和系统资源的。所以在切换线程过程中开销要比进程小的多。

2.2)线程池

*线程池有七个参数?

corePoolSize: 线程池核心线程数最大值

maximumPoolSize: 线程池最大线程数大小

keepAliveTime: 线程池中非核心线程空闲的存活时间大小

unit: 线程空闲存活时间单位

workQueue: 存放任务的阻塞队列

threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。

handler: 线程池的饱和策略事件,主要有四种类型。

*线程池的执行过程?

当提交一个线程执行任务时候 ,先看有没有空闲的核心线程如果有就执行,如果没有就看阻塞队列中有没有满的状态,如果没有放满则放入队列中,否则就直接看线程数量是否大于非核心线程数量,如果没有就直接创建一个非核心线程,否则就是按照指定的拒绝策略给处理。。如果一个非核心线程在某个一段时间类是空闲的那么线程池就会把这个线程自动销毁掉。

四种拒绝策略
  • AbortPolicy(抛出一个异常,默认的)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  • CallerRunsPolicy(交给线程池调用所在的线程进行处理)
五种阻塞队列 (5)
  • ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
  • SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
五种创建线程的方式池的方式:
  • newFixedThreadPool (固定数目线程的线程池)

  • newCachedThreadPool(可缓存线程的线程池)

  • newSingleThreadExecutor(单线程的线程池)

  • newScheduledThreadPool(定时及周期执行的线程池)

  • new ThreadPoolExecutor() 自定义的方式创建

使用无界队列的线程池会导致内存飙升吗?

会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

2.2)线程、进程、协程的区别

进程:进程是操作系统分配系统资源和内存空间的最小单位。进程是独立的一块空间,所以资源和内存空间的切换是特别消耗资源的。

线程:线程也叫做轻量级的进程,是操作系统调用执行的最小单位。线程的资源是依赖于他的父进程,所以他的资源是共享的,线程的切换需要转换到内核态开销相对于小一些。

协程:协程是一种轻量级的线程,协程是直接在用户态就可以控制,具有对内核态来说是不可见的,所以协程的上下文切换更加的节约资源消耗。

2.3)什么是上下文的切换

上下文的切换指的是CPU寄存器和程序计数器存储的数据进行切换,而内存数据的改变只有在内核态才能进行。所以上下文切换对系统来说是巨大的成本。

  • 先是暂停原有的进程,保存好进程中寄存器中的数据在某个地方
  • 内存获取中下一个进程上下文,存储到寄存器中

2.4)Java有几种创建线程的方式

  • new 一个Thread
  • 继承Runnable类
  • 使用Callable
  • 使用线程池

2.5)sleep和wait区别

  • sleep 属于 Thread类,wait属于Object类
  • wait会释放掉锁,sleep不会释放锁
  • wait必须在同步代码方法和同步代码块中,sleep没有这一个限制
  • wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒

2.6)Java的内存模型

JMM屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台都能够达到一致的内存访问效果,它定义了Java如何将程序中的变量在主存中读取

具体定义:所有变量都在主存中,主存是线程的共享区域,每个线程都有自己独有的工作内存,线程想要操作变量必须从主存中copy一份到自己的工作区域,每个独立内存区域相互隔离。

所以这个时候读写存在延迟,且不是原子操作,所以就出现了一些列的线程安全操作。比如 原子性、可见性、有序性。

image-20220831215853964

2.6)保证并发安全的三大特性?

原子性:一次或多次操作在执行期间不被其他线程影响

可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道**(利用内存原子操作解决或者内存屏障或者lock前缀)**

有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排**(内存屏障)**

2.7)ThreadLocal原理

每一个线程创建变量的副本,不同线程间是不可见的,保证线程安全。每个线程都维护一个ThreadLocalMap,key为threadlocal实例,value是保存的副本。

但是使用ThreadLocal会存在内存泄漏问题,因为key 是弱引用,value是强引用,每次GC时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时的获取value。

第二个会出现内存溢出问题,如果使用的是线程池的方式去使用ThreadLocal话,那么就会出现存储的value一直存在,这样就一直堆积。

image-20220831220753564

2.8)什么是CAS锁

CAS锁可以保证原子性,思想是更新内存是会判断其内存的值是否被修改了,如果没有被修改就直接更新,如果被修改了,就得重新去获取值,知道更新为止。这样是有缺点的:

  • 只能支持一个变量的原子操作,不能保证整个代码块的原子操作
  • CAS频繁的失败会造成CPU的开销打
  • 会出现ABA问题

解决ABA问题,可以通过加入版本控制

2.9)Synchronized锁原理和优化

在JDK1.6以后Synchronized引入了偏向锁、轻量级锁、重量级锁、锁的粗化、锁消除的优化。并发性能基本和Lock持平的。

  • 偏向锁:是不存竞争情况下,从而在后续的代码块中没有加锁解锁的开销
  • 轻量级锁:轻量级锁所适应的场景是线程交替执行同步块的场合
  • 重量级锁:重量级锁首先会经过一定次数的CAS自旋操作获取锁,如果获取失败,存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。是在竞争激烈的情况下创建一个monitor对象,并且将线程挂起,而挂起就要切换到内核状态执行。从而开销非常大。

image-20220901114133488

轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会撤销掉偏向锁,然后升级为轻量级锁,这个线程会通过自旋方式不断获取锁,不会阻塞,提高性能

重量级锁:轻量级锁尝试去获取到锁,如果获取失败线程就会进入阻塞状态,该锁会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低

注意,锁只能升级不能降级

image-20220901115158489

image-20220901115635895

image-20220901115801136

2.10)synchronized和ReentrantLock的区别:

  • 实现方面:一个是JVM层次通过,(监视器锁)monitor实现的。一个是通过AQS实现的
  • 相应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 锁的类型不同:synchronized 是非公平的锁,而ReentrantLock即可以是公平的也可以是非公平的
  • 获取锁的方式不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。

2.11)为什么AQS使用的双向链表

因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候就会用到prev和next方便删除掉中间的节点

2.12)有哪些常见的AQS类

  • ReentrantLock 独占锁
  • Semaphore 共享锁(限流有限资源)
  • CountDownLatch 共享锁(合并汇总)
  • CyclicBarrier 独占锁 + 条件队列
  • ReentrantReadWriteLock 读写锁

2.13)死锁产生的4个必要条件

互斥条件:进程要求对所匹配的资源进行排他性控制,即是一段时间内某资源仅为一进程所占有

请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:进程已获得的资源在未使用完之前不,不能剥夺,只能在使用完时有自己释放

环路等待条件:在发生死锁时,必须存在一个进程–资源得环星链

2.14)预防死锁

资源一次性得分配:一次性分配所有得资源,这样就不会有请求了(破坏请求条件);只要一个资源得不到分配,也不给这个进程分配其他得资源(破坏请求保持条件)

可剥夺条件:即当某个进程获得部分资源,得不到其他得资源,则释放已有得资源(破坏补课剥夺得条件)

资源有序分配法:系统分配资源编号,每一个进程按编号递增的顺序请求资源,释放则反之(环路破坏)

2.15)解除死锁

**剥夺资源:**从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

**撤消进程:**可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

2.16)CPU密集型和IO密集型 的线程数设置

CPU 密集型:对于这样的任务最佳的线程数为CPU核心数的1~2倍,如果设置过多的线程,就会造成不必要的CPU上下文切换,性能相对于比较低

IO密集任务:由于IO读写是比较消耗时间的,并不会特别的消耗CPU的资源。对于这种任务最大的线程数一般大于CPU核心数的很多倍。

线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间

三、JVM篇

3.1)Java中都有哪些引用?

强引用 :发生GC时候不会被回收

软引用:发生内存满(内存溢出的时候)会被回收(通常用于缓存)

弱引用:发生下一次GC时候会被回收

虚引用:无法通过虚引用去获取对象。用途:用于gc时返回一个通知

3.2)JVM运行时数据区域(内存结构)

首先编译器通过把Java代码转换成字节码,类加载器把字节码加载到运行数据区域的方法区内,而字节码只是一套JVM规范指令,所以这个时候需要执行引擎将字节码翻译成底层系统指令,在交给CPU去执行,这个过程需要用到其他语言本地库接口

  • 线程私有:
    • 本地方法栈:为native修饰的本地方法提供的空间
    • Java虚拟机栈:每次调用一个方法都会产生一个栈帧,每个栈帧内部有 操作数栈,局部变量表 ,动态链接,方法出口等信息
    • 程序计数器:用来记录程序执行的位置记录,方便切回后能够继续执行
  • 线程共享:
    • 虚拟机堆空间:JVM进行对象的创建存储空间,也是垃圾回收的主要区域,分为新生代和老年代
    • 方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用的是永久代,之后用的是元空间

3.3)JVM类加载过程

主要分为 加载 -> 链接 - > 初始化 三步骤:

  • 加载:把字节码通过二进制的方式转化到方法区中的运行数据区

  • 链接:分为三步骤:

    • 验证:校验字节码的正确性

    • 准备:给静态变量分配内存空间 和 赋值操作(赋0值 ,比如String 赋值 null int 赋值 0 ……)

      注意 final修饰的变量不是这个时候赋值的而是在在编译期间就已经赋值了

    • 解析:解析是将常量池中的符号引用替换成直接引用的过程

    符号引用:以一组符号(可以是任何格式的字面量,无歧义即可)来描述所引用的目标。
    直接引用:可以直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

  • 初始化:初始化阶段可以看做是执行类的构造器()方法的过程。

注意:值得注意的是:加载阶段与连接阶段部分内容是交叉进行的,但开始时间有先后顺序。

3.4)对象的创建过程

1、首先进行类的加载检查

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等

2、分配内存

对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。

这个步骤有两个问题:

1.如何划分内存。

2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

划分内存的方法:
  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞)

    如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  • “空闲列表”(Free List)

    如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

解决并发问题的方法:
  • CAS(compare and swap)

    虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

    把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过 XX:+/UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启 XX:+UseTLAB), XX:TLABSize 指定TLAB大小。

3、初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4、设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)
和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

5、执行方法

执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

image-20220812152038703

3.4.2)如何定位一个对象多大的空间?

在HotSpot虚拟机里对象内存布局分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头:

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特, 官方称它为“Mark Word”。
  • 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个 java 数组,那么在对象头中还有一块用于记录数组长度的数据。

image-20220811170431701

案例:

1)创建一个maven项目,引入依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

2)新建一个类

@Data
public class BaseEntity  {

    private long id;

    private double amount;

    private int updateUserId;

    private float f;

    private char c;

    private byte b;

    private boolean bb;

    private short ss;

    private long[] list; //4byte

    private String s; //4byte

    private Long count; //4byte
}

3)测试

public class ObjectHeaderTest {

    public static void main(String[] args) {
        BaseEntity baseEntity = new BaseEntity();
        String toPrintable = ClassLayout.parseInstance(baseEntity).toPrintable();
        System.out.println(toPrintable);
    }
}

打印结果

site.sunlong.obj.BaseEntity object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int BaseEntity.updateUserId   0
 16   8               long BaseEntity.id             0
 24   8             double BaseEntity.amount         0.0
 32   4              float BaseEntity.f              0.0
 36   2               char BaseEntity.c               
 38   2              short BaseEntity.ss             0
 40   1               byte BaseEntity.b              0
 41   1            boolean BaseEntity.bb             false
 42   2                    (alignment/padding gap)   
 44   4             long[] BaseEntity.list           null
 48   4   java.lang.String BaseEntity.s              null
 52   4     java.lang.Long BaseEntity.count          null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total

4)测试结果解读

数据第1行(object header: mark):这个是Mark Word占用的字节数,64位机器上占用8个字节,32位机器上占用4个字节。

数据第2行(object header: class) :类型指针,在开启指针压缩的情况下占4个字节,未开启的情况下占8个字节,jdk1.6之后默认开启指针压缩。

数据第3-14行(除(alignment/padding gap)):BaseEntity对象属性类型占用字节数,一共占用39个字节。

其他数据行:对齐填充2个字节,由于Mark Word(8个字节)+类型指针(4个字节)+对象字节数(42个字节)=54个字节,54不是8的倍数,所以要填充2个字节凑够8的倍数。如果字节数之和刚好是8的倍数,则不需要对齐填充。

3.4.3)JVM类初始化顺序

父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法

3.5)什么是内存泄漏?

就是不再使用的对象或者变量一直占用着内存中,而且GC也无法去回收。这种情况是因为长生命周期的对象持有短生命周期的对象的引用就会发生内存泄漏情况。

3.5.2)什么情况下会内存溢出?

堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时

栈溢出:方法调用次数过多,一般是递归不当造成

3.6)简述一下Java的垃圾回收机制?

在Java中,程序员不需要去指定的释放对象。而是有JVM虚拟机自行的执行,有一个垃圾回收线程(守护线程)他的优先级比较低,他不会去主动的去执行,只有虚拟机空闲或者当前的堆空间满的时候,才会去触发,去扫描回收掉没有用的对象。

当创建一个对象的时候GC就会进行监控这个对象的地址、大小以及使用情况 。GC通过有向图的方式记录管理堆,通过这种方式来判断那些可达,哪些不可达,从而来判定是否回收。

程序员可以手动的进行System.gc()来通知gc运行,但是GC并不能保证GC一定会执行

3.7)如何判断是否能够回收?

两种方式:

  • 引用计数器法:但是不能避免循环引用问题
  • 可达性分析算法:当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

3.8)垃圾回收算法有哪几种?

  • 标记清除: 标记需要清除的对象,对标记的对象进行清楚操作。缺点:产生了大量的不连续碎片化对象,不利于分配新的对象。

  • 标记整理:标记需要清除的对象,对标记的对象进行清楚操作过程中,也将没有清除的对象进行整理到内存的一端。优点:解决了标记-清理算法存在的内存碎片问题 缺点:需要局部的移动对象,降低的效率

  • 复制算法:将内存空间分称等大的两部分,每次遍历一个区域将存货的对象放入另一个区域,然后清除掉掉这个区域所有的对象,以此类推循环。 优点:效率高速度快,也没有内存碎片的问题 缺点:堆内存大小要求高

  • 分代算法:就是把内存空间分配成不同的年轻代、老年代和永久代,根据不同的年代使用不同的算法。

    比如说 年轻代空间大而且垃圾回收发生频繁 所以采用复制算法,老年代就采用标记整理算法

3.9)垃圾收集器

image-20220812225110308

Serial收集器:色而瑞

​ 新生代采用复制算法,老年代采用标记-整理算法。单线程,所以在发生GC时候需要暂停所有线程的工作。

Parallel Scavenge收集器:帕尔雷尔

​ 新生代采用复制算法,老年代采用标记-整理算法。多线程,Parallel Scavenge收集器关注点是**吞吐量(**高效率的 利用CPU)

ParNew收集器:帕尔new(jdk8默认设置的垃圾收集器)

​ 新生代采用复制算法,老年代采用标记-整理算法。和Parallel 相似主要用于配合CMS收集器使用。

CMS收集器:

​ CMS收集器是一种 “标记-清除”算法实现的。

​ CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户 体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用 户线程(基本上)同时工作。

  • 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
  • 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但
    不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对
    象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
  • 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑
    色不做任何处理(见下面三色标记算法详解)。
  • 并发重置:重置本次GC过程中的标记数据。

image-20220920163512395

3.10)内存对象的分配策略?

对象的内存分配通常是在 Java上分配(随着虚拟机优化技术的诞生,某些场 景下也会在上分配,
后面会详细介绍),对象主要分配在新生代的 Eden 区, 如果启动了本地线程缓冲,将按照线程优先在
TLAB 上分配。少数情况下也会直 接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取
决于哪一种 垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵 循以下几种

  • 对象优先在 Eden 区分配

    多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行 分配时,虚拟机将会发
    起一次 Minor GC。如果本次 GC后还是没有足够的空 间,则将启用分配担保机制在老年代中分配内存。

    年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间,如果空间不够的话就直接进行FullGC

    image-20220812172559990

  • 大对象直接进入老年代

    所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导 致在内存还有不少空间
    的情况下提前触发 GC 以获取足够的连续空间来安置新对 象。

    前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象 直接在新生代分配就会导
    致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。

  • 长期存活对象将进入老年代

    虚拟机采用分代收集的思想来管理内存,那 么内存回收时就必须判断哪些对象应 该放在新生代,哪些对
    象应该放在老年代。因此虚拟机给每个对象定义了一个对 象年龄的计数器,如果对象在 Eden 区出生,
    并且能够被 Survivor 容纳,将被 移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区
    中每「熬 过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升 到老年代。

  • 空间分配担保

  • 动态年龄判定

3.11)说一说如何理解双亲委派模型

类加载器主要有:

  • 引导类加载器(BootstrapClassloader):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如
    rt.jar、charsets.jar等
  • 扩展类加载器(ExtceptClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR
    类包
  • 应用程序类加载器(ApplicationClssloader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那
    些类

image-20220902173951286

双亲委派模型就是子加载器向父级加载器向上委托它加载,如果父加载器能加载那么子加载器就不用加载直接返回,如果不能加载就有子加载器进行加载。

这样做的好处就是:

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

**如何打破双亲委派模型:**重写loadClass(String, boolean)方法

四、MYSQL篇

4.1)数据库三大范式是什么

第一范式:每个列都不可以再拆分。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我 们经常会为了性能而妥协数据库的设计。

4.2)说一说B和B+树的区别?

B树的数据即存在非叶子节点上也存在于叶子节点上,而B+树所有的数据都是存在叶子节点。就是因为这个原因所以B+树存储的数据比较多,因为一个页的大小是16KB而B树的非叶子节点又存储了数据data,所以一个页锁存储的节点少一些,故如果使用B树那么层数会相应提高很多。

B+树的叶子节点还采用了双向链表的结构,这样也可以可以方便使用范围查找和所有数据的遍历。而B树有的数据存储在非叶子节点就做不到这一点,必须经过不断的回溯IO大大降低了效率

  • 二叉树(形成一条链表的形式)
  • 红黑树(树的高度太深)
  • Hash(不支持范围的查找)

4.3)MyISAM 和 InnoDB的区别

  • 存储的文件格式: MyISAM 存储索引MYI 存储数据MYD 存储表的结构frm

    ​ InnoDB存储的索引+数据都是IDB 存储表的结构frm

  • 索引存储方式:MyISAM 存储的是那条记录的地址(非聚簇索引)

    ​ InnoDB存储的是那条记录的所有值(聚簇索引)

  • 事务:InnoDB支持事务,MyISAM 不支持事务

  • 外键:InnoDB支持外键,MyISAM 不支持外键

  • 锁:InnoDB支持行锁和表锁,MyISAM 只支持表锁

4.4)什么是聚簇索引和非聚簇索引和覆盖索引

聚簇索引:就是将索引和值放在一起,根据索引可以直接获取值,

非主键索引:叶子节点存放的是数据行地址,先根据索引找到数据地址,再根据地址找数据

覆盖索引:覆盖索引就是所查找的结果集,在所查找的索引树当中已经存在了不用再进行回表操作

联合索引:多个字段联合起来的索引

4.5)除了索引我们写SQL时候还有那些优化?

  • 要遵循最左前缀法则

  • 使用覆盖索引,就是查询字段需要指定明确,切忌不要使用 *

  • in 后面中的 字段尽量少,不宜过多,发现数据较少

  • 注意范围查询语句,一般返回查询语句的后面字段会索引失效

  • 不建议使用%前缀模糊查询

  • 避免在where子句中,对字段进行null判断,,这样会造成不走索引

  • 避免where子句中对字段表达式函数的操作,字符串不加单引号

  • 尽量避免where子句使用or 判断,因为or左右只有有一方没有索引,就会导致全表扫描

  • 尽量使用inner join 避免使用left join因为是小表为驱动表,而left join是左表驱动右表

4.6)NLJ BNL 算法

被驱动表:

驱动表:

通常使用小表使用驱动表,被驱动表字段如果不存在索引使用BNL算法,否则时候NLJ算法,因为他们扫描次行数相等,这个时候BNL算法的磁盘扫描而且扫描次数更少,他是先从磁盘中放入join_buffer中去,而且计算快得多。

4.7)为什么小表作为驱动表

驱动表首先会根据where字段进行过滤,然后再去跟非驱动表进行join关联的。

大表 1000 小表 100行记录

关联过程:

NLJ算法: 这个通常是在被驱动表关联字段存在索引情况下触发,首先将驱动表查询好的字段取出来(100次),然后因为有索引所以每一行查询好关联的记录去被驱动表(100)次。总共 100+100 = 200次

BNL算法: 把驱动表筛选出来的数据读取到join_buffer中去(100次),将大表的每一行取出来拿出来join_buffer每一行进行对比(100 * 1000)。如果join_buffer存不下去,那么分多次磁盘的IO读写进行和被驱动表对比。

4.7)索引下推:

就是当一个联合索引,第一个是模糊查询,导致后面的字段不能够使用索引 ,这个时候将索引查询到的数据进行回表操作,这个时候的回标任务比较大。所以MySQL自动优化采用了索引下推的机制,在筛选模糊查询的时候也进一步去筛选模糊查询后面的字段,这样可以大大的减少了回表的成本。

4.8)MySQL的事务特性

原子性:就是一个事务一系列的操作命令要么全部执行,要么全部执行失败。通过Undolog日志解决

持久性:事务一旦提交就不可改变,而且必须持久化到磁盘当中。通过Redolog日志解决

一致性:再一个事务对数据的查询结果必须是一致的。MVCC通过Undolog日志解决

隔离性:事务和事务之间互不影响。这个我们可以通过加锁来解决

  • 读未提交:再这个隔离级别下,所有的事务都可以看到其他事务没有提交的执行结果。就会出现脏读不可重复读幻读
  • 读已提交:这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。就会出现不可重复读幻读
  • 可重复读:事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。幻读
  • 串行化:确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。

4.9)使用索引就一定比没有使用索引好吗?

空间上的代价:每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。

**时间上的代价:**每次对表中的数据进行 增、删、改 操作时,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。

4.10)redolog好处?

防止实例挂了 或者宕机,这样就会丢失掉那一段时间的数据。有了redolog日志就直接降低了刷盘的频率。

三种输盘策略:redo log buffer -> page cache -> 磁盘

  • 1 提交事务立马刷盘
  • 0 不刷盘,每个1s进行刷盘操作
  • 2 不刷盘,由OS操作系统决定刷盘

4.11)结合redolog和undolog 说一说一条sql 的执行过程

  1. 从磁盘中加载数据到buffer pool中
  2. 记录Undolog日志
  3. 更新buffer pool中的数据
  4. 记录redolog日志就是将更新的操作写入redolog buffer中
  5. redolog buffer 写入 到redolog文件
  6. 记录binlog日志(刷盘)

image-20220903112317973

image-20221016171756487

4.12)MySQL锁的分类

基于属性:

  • 共享锁(读锁):又称读锁,一个事务为表加了读锁,其它事务只能加读锁,不能加写锁
  • 排他锁(写锁):又称写锁,一个事务加写锁之后,其他事务不能再加任何锁,避免脏读问题

基于粒度:

  • 表锁(这个是串行化):对整张表加锁,粒度大并发小
  • 行锁(可以解决可重复读):对行加锁,粒度小并发大
  • 间隙锁(可以解决幻读):间隙锁,锁住表的一个区间,间隙锁之间不会冲突只在可重复读下才生效,解决了幻读
  • 临键锁(可以解决幻读 和 可重复读):是行锁和间隙锁的一种结合方式

4.13)什么是MVCC

MVCC是多版本控制,我们在解决隔离级别产生问题的时候,一般有两种方式

  • 是通过加锁的方式
  • 通过MVCC版本控制,这种因为是没有加锁,所以并发量高,性能更好,不用去阻塞其他线程和事务的操作

在每次的事务当中都会生成一个新版本ReadView的数据:

  • 在读应提交的隔离级别下,每次的读都会生成一个新的ReadView规则,然后遵循ReadView版本链去获取读到的数据,,这种情况可以解决可重复读的问题
  • 在可重复读的隔离级别下,每次读 只有在第一次的读过程中才会产生一个ReadView规则的undolog版本链,这种情况就可以解决幻读的情况。

4.14)select 的执行原理(顺序)

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT...

这条sql 的执行步骤

FROM   ->  JOIN  ->  ON  ->  WHERE  ->  GROUP BY  ->  HAVING   ->  
SELECT   ->  DISTINCT  ->  ORDER BY  -> LIMIT

客户端 -> 连接器 -> 词法分析器 -> 优化器 -> 执行器 -> 存储引擎

连接器:如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。

4.15)char和varchar区别

  • char是固定长度的不可变的,varchar是可变的字符串
  • char存储短的字符会造成空间的浪费,但是性能优于varchar,,varchar类型是长度变化的所以存入和取出来的时候性能上还要进一步的去判断长度大小所以性能效率上相对于会低。

char列的长度是固定的,能存取的最大的长度是0~255之间,如果存储的是utf8那么就是3*255的字节;如果是GBK那么就是2*255

varchar分为两个部分存储,一个是存储数据一个是存储长度范围的,以前的最大长度是255 ,mysql5版本后就是0~65535(2^16)个字节 ,例外还需要1-2个字节(长度超过255时需要2个字节)来存储字符串的实际长度。

4.16)binlog日志的用途

  • 数据的备份(防止数据丢失)
  • 数据的复制(主从机的复制)
  • Statement
    每一条会修改数据的sql都会记录在binlog中。优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
  • Row
    它不记录sql语句上下文相关信息,仅保存哪条记录被修改。优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
  • Mixed
    从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

4.17)如果我们发现一条Sql执行特别慢 我们该怎么去处理

  1. 我们先观察服务器的状态情况看是否是在周期性的波动情况,如果是那么看加缓存能不能解决掉
  2. 如果不能解决那么我们开启慢查询,分析慢查询的语句。看是SQL的执行时间长问题,还是SQL语句的等待过长原因。
  3. 如果是等待过长就调优服务器的参数,如果是SQL语句的原因那么我们就优化SQL语句,比如加索引优化啊数据库表的设计优化
  4. 如果上面还是不能解决,看SQL查询是否达到了瓶颈,如果是就读写分离(主从架构)分库分表(垂直分表,垂直分库,水平分表)

查看系统性能参数 SHOW [GLOBAL|SESSION] STATUS LIKE '参数';

统计SQL的查询成本 SHOW STATUS LIKE 'last_query_cost';

开启慢查询日志参数 set global slow_query_log='ON';

设置慢查询的时间阈值 set global long_query_time = 1;

查询有多少条慢查询的记录 SHOW GLOBAL STATUS LIKE '%Slow_queries%';

查看 SQL 执行成本 SHOW PROFILE

分析查询语句 EXPLAIN

4.18)哪些情况索引会失效

(1)where条件中有or,除非所有查询条件都有索引,否则失效

(2)like查询用%开头,索引失效

(3)索引列参与计算,索引失效

(4)违背最左匹配原则,索引失效

(5)索引字段发生类型转换,索引失效

(6)mysql觉得全表扫描更快时(数据少),索引失效

五、Redis系列

5.1)Redis有哪几种基本的数据类型

  • 字符串类型:用于存储文章的访问量
  • Hash:用来存储key-value的数据结构,当单个元素比较小和元素数量比较少的时候 ,底层是用ziplist存储。通常可以用来存储一些对象之类的
  • List: 底层采用的quicklist 和 ziplist进行存储的,quicklist就是类似于双端的链表,这样做的好处就是提高存取的效率。通常用作微信公众号的推送消息队列
  • Set:底层就是hashtable的value为空的数据结构,通常用作点赞 + 收藏 之类
  • ZSet: 底层使用的跳表和字典,调表方便于对排好序的数据查询 , 通常用作类似于微博的热搜
  • bitmap:一般用于签到功能或者是统计是否登录的功能上

5.2)Redis持久化机制

  • 快照持久化RDB

    在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。

​ redis的默认持久化机制,通过父进程fork一个子进程,子进程可以共享主线程的所有内存数据。bgsave 子进程 运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作, 那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制 一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程 仍然 可以直接修改原来的数据。

  • 追加持久化AOF

​ 以日志形式记录每一次的写入和删除操作,策略有每秒同步、每次操作同步、不同步,优点是数据完整性高, 缺点是运行效率低,恢复时间长

5.3)Redis是单线程吗?

redis单线程仅局限于 他的网络IO流,和他的键值对读写操作。但是redis对于其他的功能,比如持久化、异步的删除、集群同步等操作,都是额外的线程操作的。

5.4)为什么Redis单线程还这么快

因为redis所有数据都是在内存中,所有的运算都是内存级别的,而且单线程避免了多线程切换的性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

5.5)Redis单线程还能处理那么多的并发客户端连接

Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

5.6)什么是缓存穿透

缓存穿透就是查询到一些不存在的数据,本来缓存层就是为了保护存储层的,而这样直接穿透到存储层就起不到保护的作用。

**如何解决:**在缓存存这是访问的key 为 一个null的值,并设置一个过期时间。。或者提前在接口层做好校验工作

5.7)什么是缓存击穿

由于大批量的缓存在同一时刻失效,大量请求过来就穿透缓存到达存储层,可能造成存储层压力过大甚至挂掉。

**如何解决:**设置一个随机的过期时间,而不是同一时刻都过期。。或者加上互斥锁。。

5.8)什么是缓存雪崩

缓存雪崩指的是缓存层支撑不住或宕掉后,流量会像奔逃的野牛一样, 打向后端存储层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。

1)预防和解决缓存雪崩问题
  • 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
  • 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
  • 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定

5.9)Redis如何实现key的过期删除?

采用的定期过期 + 惰性过期

定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。

惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。

5.10)缓存与数据库双写不一致

在大并发下,同时操作数据库与缓存会存在数据不一致性问题

1)双写不一致情况

image-20220720130545417

2)读写并发不一致

image-20220720130709159

3)解决方案
  • 1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
  • 2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
  • 3、如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。
  • 4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

5.11)Redis集群方案

(1)主从模式:多个master节点,多个slave节点,master节点宕机slave自动变成主节点

(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点

(3)分片集群:主从模式和哨兵模 式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定

5.12)Redis集群主从同步原理

主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave。

主从复制(全量复制)流程图:

image-20221016181203213

后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据

主从复制(部分复制,断点续传)流程图:

image-20221016181303002

5.13)Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

5.14)Redis集群选举原理分析

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

1.slave发现自己的master变为FAIL

2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息

3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack

4.尝试failover的slave收集master返回的FAILOVER_AUTH_ACK

5.slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)

6.slave广播Pong消息通知其他集群节点。

六、Spring系列

6.1)你怎么理解Spring?

Spring是个轻量级的框架,简化了应用的开发程序,提高开发人员的系统维护性,不过配置消息比较繁琐,所以后面才出选了SpringBoot的框架。

Spring的核心组件 : Spring Core 、 Spring Context 、 Spring Beans

IOC是Bean的一个容器,Context是每个Bean之间依赖的关系。正是因为各种Bean建立的关系我们才构成了IOC容器为我们服务,而Core就是维护、建立和维护所需要的的工具。

6.2)Spring中的Bean 是线程安全的吗?

非安全的

原型Bean

对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

单例Bean

对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

但是如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 "singleton"改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据 controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。
  • 所以其实任何无状态单例都是线程安全的。

6.3)Spring单例,为什么controller、service和dao确能保证线程安全?

Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。

实际上大部分时间Bean是无状态的(比如Dao) 所以说在某种程度上来说Bean其实是安全的。

6.2)什么是IOC和AOP,你知道他们的原理吗?

**IOC:**IoC就是控制反转,是一种思想而不是技术实现,A类对象中需要B类对象,传统是自己手动new就是主动控制,IoC思想就是交给IoC容器管理,不再自己手动new,容器管理好对象以及依赖关系,需要的时候从容器里拿就行。

**IOC原理:**就是在给每一个bean进行实例化后,都要对他的属性进行填充,大多数我们都是使用@Autowire直接的填充依赖注入的,在实例化和实例化后之间会调用一个MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition一个寻找注入点的方法,然后在属性填充时候,首先是按照注入点的类型去寻找如果有多个选择@Qualifier注解寻找,如果还是有多个按照@Primary,@Priority,最后还是没有筛选出来一个唯一的注入就按照名字来查找

**AOP:**是面向切面的编程,AOP就是可以将那些与业务不相关但是很多业务都要调用的代码抽取出来封装起来,减少重复代码,思想就是不侵入原有代码的情况下对功能进行增强。

**原理:**SpringAOP是基于动态代理的,其中SpringAOP的底层的动态代理有两种方式,一种是对于实现接口的JDK Proxy方式,另外一种就是CgLib

  1. Aspect:表示切面,比如被@Aspect注解的类就是切面,可以在切面中去定义Pointcut、Advice等等
  2. Join point:表示连接点,表示一个程序在执行过程中的一个点,比如一个方法的执行,比如一个异常的处理,在Spring AOP中,一个连接点通常表示一个方法的执行。
  3. Advice:表示通知,表示在一个特定连接点上所采取的动作。Advice分为不同的类型,后面详细讨论,在很多AOP框架中,包括Spring,会用Interceptor拦截器来实现Advice,并且在连接点周围维护一个Interceptor链
  4. Pointcut:表示切点,用来匹配一个或多个连接点,Advice与切点表达式是关联在一起的,Advice将会执行在和切点表达式所匹配的连接点上
  5. Introduction:可以使用@DeclareParents来给所匹配的类添加一个接口,并指定一个默认实现
  6. Target object:目标对象,被代理对象
  7. AOP proxy:表示代理工厂,用来创建代理对象的,在Spring Framework中,要么是JDK动态代理,要么是CGLIB代理
  8. Weaving:表示织入,表示创建代理对象的动作,这个动作可以发生在编译时期(比如Aspejctj),或者运行时,比如Spring AOP

6.3)bean 的生命周期?

  • 首先通过扫描包路径,通过ASM技术获取到类的信息,封装成BeanDefintion注册到Spring容器当中(这个操作其实就是BeanFactory的后置处理器操作)
  • 然后就可以进行类的加载,进行类的实例化前操作、推断构造方法实例化实例化后填充属性。(值得注意的是再实例化和实例化后之间有个BeanDefinction的后置处理器,这个后置处理器就是我们@Autowire的寻找注入点的迁前置操作。依赖注入就是再实例化后的操作)
  • 最后就进行执行Aware回调初始化前初始化初始化后(代理逻辑)操作
  • 最后就是bean的销毁操作

6.4)Bean实例化的时候是如何解决循环依赖?

循环依赖就是在创建A实例的时候里面包含着B属性实例,所以这个时候就需要去创建B实例,而创建B实例过程中也包含着A实例。 这样A实例还在创建的过程当中,所以就导致AB实例都创建不出来。

一级缓存 就是我们所说的Bean存储的经过完整的生命周期的单例池

二级缓存 缓存未经过完整的声明周期的Bean

三级缓存 缓存的是ObjectFactory其实存储的是一个动态代理类的一个实现类的拉姆达表达式,执行这个拉姆达表达式就会生成这个类的代理类。

有了这三级缓存我们就来看一看,他是如何解决的。

如果出现了循环依赖的问题,我们在创建A的过程中,需要创建B。这个时候就需要将A放入三级缓存并且不会执行那个拉姆达表达式,所以这个时候需要去创建B在创建B时候需要去依赖A,就直接去三级缓存中查找,并且判断需不需要进行AOP处理,如果需要就执行那个拉姆达表达式得到代理对象,如果不需要就不需要执行,直接取出原始的对象。将取出的对象放入二级缓存中,因为这个时候A还未经过完整的生命周期所以不能放入一级缓存。这个时候其他需要依赖A对象的直接从二级缓存中去获取即可。当B创建完成,A继续执行生命周期,当A完成了属性的注入后,就可以放入一级缓存了。

6.5)Bean实例化的三级缓存了解吗?

6.6)SpringMVC的工作原理

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用并且找到对应的HandlerMapping,解析请求对应的 Handler
  3. 然后利用对应的适配处理器,并且调用处理器开始执行,执行过程中包括解析参数,返回ModelAndView
  4. Model 是返回的数据对象,View 是个逻辑上的 View
  5. ViewResolver 会根据逻辑 View 查找实际的 View
  6. DispaterServlet 把返回的 Model 传给 View(视图渲染)。

image-20220905083745006

6.7)Spring的事务实现方式?事务隔离级别?事务传播机制?

spring事务是在启动类上加一个@Configuration 和 @EnableTransactionManagement注解,然后再Service的方法上面加一个@Transactional注解就可以了。Spring使用的事务隔离级别默认是使用MySQL数据库的隔离级别。

事务的传播机制:

当前不存在事务:

  • requiresd 新建一个事务

  • requires_new 新建一个事务

  • nested 新建一个事务

  • mandatory 抛异常

当前存在事务:

  • requiresd 加入这个事务

  • requires_new 把当前事务挂起,然后新建一个事务

  • nested 记录一个SavePoint,创建一个事务作为当前事务的嵌套事务来运行 外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。

  • mandatory 加入该事务

  • never 以非事务方式运行,如果当前存在事务,则抛出异常。

6.8)讲一下SpringBoot里面比较常用的注解?

@configuration @handmapping @getmapping @postmapping @EnableTransactionManagement

@mapperScaner @commpoent @requestBody @restcontroller

6.9)MyBatis的 # 和 $ 有什么区别?

${}是properties文件的变量占位符,用于标签属性值和SQL内部,比如${driver}会被替换成com.mysql.jdbc.Driver。

#{}是SQL的参数占位符,实际上会被替换成 ?

6.10)SpringBoot 是如何实现自动装配的?

@SpringBootApplication下有三个注解:

  • @SpringBootConfiguration:启用 SpringBoot 的配置类,相当于是Spring中的一个配置文件,

    比如说可以在这个类去注册bean,或者导入其他的配置类

  • @ComponentScan:用于扫描启动类包下面的所有类的注解信息,并且注册

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制,获取需要自动装配的所有配置类,读取所有的Start中的META-INF/spring.factories,按需加载到IOC容器中。

6.11)什么情况下会出现事务失效场景?

1、数据库引擎不支持事务

2、传播机制不支持 事务,就是不以事务的情况运行

3、没有配置事务管理器数据源

2、没有被 Spring 管理

3、方法不是 public 的

​ @Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

4、自身调用问题

5、异常被捕获了

6、异常的类型不匹配,默认是运行时的异常

七、分布式

7.1) 什么是CAP BASE理论

CAP:
一致性: 在分布式环境下,一致性是指数据在多个副本之间是否能够保持一致性的特性,等同于所有的节点访问同一份最新数据的副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证数据仍然一致性的状态。
可用性:每次请求都能够保证正确的响应,但是不保证获取的数据为最新的数据。
分区容错性:分布式情况下在遇到任何的网络分区额故障的时候,仍然需要能够保证对外提供一致性和可用性的服务,除非是整个网络环境发生了故障。
BASE
基本可用:在分布式系统出现故障,允许损失部分的可用性(服务降级、页面降级)
软可用:允许分布式出现中间状态。而且中间状态不影响系统的可用性。这里的中间状态指的是data replication(数据备份节点)之间的数据更新可以出现延迟的最终一致性。
最终一致性:data replications 经过一段时间达到一致性。

7.2)重点协议CP(ZAB RAFT) AP( Distro)

强一致性:

1、任意时刻,所有的节点数据都是一样的。
2、一个集群需要对外部提供强一致性,所以只要集群内部的某一个数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能对外部提供访问。
3、保证了强一致性,务必会损耗可用性

弱一致性:

1、系统中的某个数据被更新后,后续对该数据的读取操作可能会得到更新后的数值,也可能会得到更新前的数值
2、即使过来一段时间窗口,后续的读取也不能保证一致性

最终一致性

1、弱一致性的特殊形式,不保证在任意时刻节点上的同一份数据是相同的,但是随着时间的迁移,不同节点的同一份数据总是在趋向一致性变化
2、存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值

顺序一致性

1、任意一次读都能读到某个数据的最近一次写的数据
2、对其他节点之前的修改是可见的(已同步)且确定的,并且新的写入建立在已经达到同步的基础上

高并发、高可用、高性能

七、计算机网络

7.1)TCP/IP和UDP模型

分为应用层,传输层,网络层,数据链路层

网络的七层架构 :应用层,表示层,会话层,传输层,网络层,数据链路层,物理层

TCP/IP是面向连接的协议,发送数据前要先建立好连接,TCP提供了可靠的服务,也就是说通过TCP连接传输的数据是不会丢失,没有重复,并且按照顺序达到的。

UDP是无连接的协议,发送数据前是不需要建立连接的,没有可靠的协议。所以传输的过程中可以以任意的路径进行传输,并且传输的过程中是否能达到和到达的时间都是没有保障的。

7.2)从输入址到获得页面的过程?

  • 首先浏览器会进行DNS查询,查询步骤是 浏览器DNS缓存、操作系统DNS缓存、读取本地host文件、本地配置的DNS 服务器查询得到IP地址。
  • 浏览器向IP服务器发送了三次握手连接
  • 连接建立起来后,浏览器向服务器发送HTTP请求
  • 后面就是SpringMVC的过程
  • 浏览器渲染视图

7.3)TCP进行三次握手

image-20220906153520841

  1. 第一次握手:Client将SYN置1,随机产生一个初始序列号seq发送给Server,进入SYN_SENT状
    态;
  2. 第二次握手:Server收到Client的SYN=1之后,知道客户端请求建立连接,将自己的SYN置1,ACK
    置1,产生一个acknowledge number=sequence number+1,并随机产生一个自己的初始序列
    号,发送给客户端;进入SYN_RCVD状态;
  3. 第三次握手:客户端检查acknowledge number是否为序列号+1,ACK是否为1,检查正确之后将
    自己的ACK置为1,产生一个acknowledge number=服务器发的序列号+1,发送给服务器;进入
    ESTABLISHED状态;服务器检查ACK为1和acknowledge number为序列号+1之后,也进入
    ESTABLISHED状态;完成三次握手,连接建立。

7.4)采用两次握手行吗?

不可以,如果采用两次握手的话,那么只要服务端确认就建立了连接。如果client发送了一个请求过来,延迟了一个小时,一个小时候服务端接受到消息确认,他们建立了连接,但是这个时候client已经关闭了。但是服务端不知道。就会造成服务端一直发送数据,等待客户端的回消息,白白浪费许多资源。所以必须需要第三次握手的客户端确认。

7.5)可以采用四次握手吗?为什么?

这个肯定可以。三次握手都可以保证连接成功了,何况是四次,但是会降低传输的效率。

7.6)TCP的四次挥手

image-20220906161925322

  1. 第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;

  2. 第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;
    进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。

  3. 第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;

  4. 第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个
    acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为
    CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入
    CLOSED状态。完成四次挥手。

7.7)http和https的区别?

其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护
区别:

  1. https需要拿到ca证书,需要s钱的
  2. 端口不一样,http是80,https443
  3. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是
    由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)

7.8)cookie与session区别

  1. cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高
  2. 单个cookie保存的数据不能超过4K,session无此限制
  3. session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应
    当使用cookie。

什么是cookie
cookie是由Web服务器保存在用户浏览器上的文件(key-value格式),可以包含用户相关的信
息。客户端向服务器发起请求,就提取浏览器中的用户信息由http发送给服务器
什么是session
session 是浏览器和服务器会话过程中,服务器会分配的一块储存空间给session。
服务器默认为客户浏览器的cookie中设置 sessionid,这个sessionid就和cookie对应,浏览器在向
服务器请求过程中传输的cookie 包含 sessionid ,服务器根据传输cookie 中的 sessionid 获取出
会话中存储的信息,然后确定会话的身份信息。

7.9)TCP如何保证可靠性的?

1、校验和(数据准确性)

2、序列号和确认应答(数据确认达到和有序)

3、超时重传(防止数据丢失)

  (1)数据在传输过程中由于网络原因等直接全体丢包,接收方根本没有接收到。

 (2)接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。

TCP在解决这个问题的时候引入了一个新的机制,叫做超时重传机制。**简单理解就是发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。**如果是刚才第一个原因,接收方收到二次重发的数据后,便进行ACK应答。如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。

4、连接管理

​ 连接管理就是三次握手与四次挥手的过程,保证可靠的连接,是保证可靠性的前提。

消息手动确认消费

RabbitMQ 持久化 消息持久化(发布确认方式) + 队列持久化(直接设置参数)

5、流量控制

6、拥塞控制

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>