我是靠谱客的博主 平淡云朵,最近开发中收集的这篇文章主要介绍Java面试题多线程相关虚拟机相关计算机网络框架相关Redis相关消息队列相关数据库相关,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

根据我以往的面试经验,总结一些有深度的、常问的面试题供大家参考,基本上纯手敲,如果对你有用希望多多点赞多多支持哈

多线程相关

1.什么是上下文切换

多线程编程中一般线程的个数都会大于CPU的核心数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并流转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其它线程使用,这个过程就属于一次上下文切换。

2.讲一下synchronized关键字的底层原理

synchronized关键字的底层原理属于JVM层面,synchronized同步语句块的实现是monitorenter和monitorexit指令,其中monitorenter指向同步代码块开始的位置,monitorexit指向同步代码块结束的位置。当执行monitorenter指令时,线程试图获取monitor(monitor对象存在于每个Java对象的对象头中)的持有权。当计数器为0则可成功获取,获取后将锁计数器设置为1.相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那么当前线程就要阻塞等待,直到锁被另一个线程释放为止。

synchronized修饰方法取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。

3.说一说Java的happens-before原则

  • 程序顺序原则:在一个线程内必须保证语义串行性,也就是按照代码顺序执行
  • 锁规则:解锁操作必然发生在后续的同一个锁的加锁操作之前
  • volitile原则:volitile变量的写先发生于读
  • 线程启动规则:线程的start()方法先于它的每一个动作
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程终止规则:线程的所有操作先于线程的终结
  • 线程中断规则:对线程interrupt()方法的调用县发生与中断线程的代码检测到中断事件的发生,可通过Thread.interrupt()方法检测到线程是否发生中断
  • 对象终结规则:一个对象的初始化完成先发生与该对象的finalize()方法的开始

4.ThreadLocal 原理

Thread类中有一个threadLocals和一个inheritableThreadLocals变量,它们都是ThreadLocalMap类型的变量,我们可以把ThreadLocalMap类实现的定制化的Hashmap。默认情况下这两个变量都是null,只有当前线程调用ThreadLocal的set或get方法时才会创建他们,实际上我们调用这两个方法时,我们调用的是ThreadLocalMap类对应的get()、set()方法。

5.ThreadLocal 内存泄露问题

ThreadLocalMap中使用的key为ThreadLocal的弱引用。所以当ThreadLocal没有被外部强引用的情况下,在垃圾回收时,key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这时候就可能发生内存泄漏问题。因此,使用完ThreadLocal方法后,最好手动调用remove()方法。

6. AQS 原理分析

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

看个 AQS(AbstractQueuedSynchronizer)原理图:

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作


//返回同步状态的当前值
protected final int getState() {
    return state;
}
//设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

 AQS定义了两种资源共享方式,Exclusive(独占):只有一个线程能够执行,Share(共享):多个线程可同时执行。

虚拟机相关

1.详细说一下对象的创建过程(new一个对象内存里都发生了什么)?

虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池中定义到一个类的符号引用,以此来检查这个类是否被加载、解析和初始化过,如果没有,那么必须先执行类加载的过程。

接下来虚拟机将会为对象分配内存。对象所需内存大小在类加载以后便可以完全确定。假如Java堆中的内存是完全规整的,所有用过的内存放在一边,未用过的放在另一边,中间放着一个指针作为分界点的指示器,那么内存分配就仅仅是把指针向空闲空间那边移动一段与对象大小相等的距离,这种分配方式被称为“指针碰撞”。如果java堆内存是不规整的,已使用的和空闲的空间互相交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的内存分配给对象实例,并更新列表的记录,这种分配方式被称为“空闲列表”。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否有压缩整理的功能决定。

还有一个需要考虑的问题是对象分配内存是非常频繁的操作,因此必须要考虑线程安全问题。有两种解决方案,一是采用CAS配合重试机制保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程先在Java堆中开辟一小块内存,称为本地线程分配缓冲(简称TLAB)。优先在TLAB上分配内存,只有TLAB用完分配新的TLAB时才会进行同步锁定。

内存分配完成后,虚拟机要将分配到的内存空间都初始化为0值(不包括对象头),接下来设置对象的对象头(对象头中存放着类的元数据信息、哈希码、对象的GC分代年龄和锁标志位),至此,从虚拟机的角度看一个新的对象已经产生了。

2.你知道哪些常用的垃圾回收算法?

标记-清除:该算法分为“标记”和清除两个阶段,首先标记处所有需要回收的所有对象,在标记完成后统一回收所有被标记的对象。

它的不足有两个,一个是效率问题,标记和清除的效率都不高;另一个是空间问题,标记清除以后会留下大量空间碎片,影响分配对象空间的效率。

标记-整理:标记过程依然与标记-清除算法一样,然后所有存活的对象都向一段移动,然后直接清理掉另一端的空间。

复制:复制算法将内存容量分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将所有存活的对象复制到另外一块上面,然后再把已经使用过的空间一次性清理掉。

复制算法的优缺点非常明显,一是对整个半区进行内存回收,内存分配时就不用再考虑内存碎片等情况,运行效率也高,缺点是浪费空间。

分代收集算法:当前主流的垃圾收集器均采用此算法。在新生代中每次收集都有大批对象死去,那就采用复制算法,只需要付出少量的复制成本就可以完成收集。而老年代中对象存活效率高、没有额外空间对它进行分配担保,就使用“标记-清除”或“标记-整理”算法来进行回收。

3.如何判断对象是否已经死亡(判断一个对象是否应该被垃圾收集器回收)?

引用计数法:给对象添加一个引用计数器,每当有一个对象引用它,计数器就加一,当引用失效时便减一;如果引用计数器为0则判断该对象已死。

引用计数法的主要缺点是它很难解决对象的循环引用问题导致对象无法被回收。

可达性分析算法:该算法的基本思想是通过一些列被称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root没有任何引用链则判断该对象已死。

GC Root对象主要有以下这几类:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

4.说一说Java都有哪些引用类型?

强引用(Strong Reference):就是最常见的引用,通过new创建的对象都属于强引用,这种引用的特点是只要引用一直存在那么它永远不会被回收;

软引用(Soft Reference):软引用是用来描述一些还有用但是非必须的对象,对于软引用关联的对象,在系统将要发出内存溢出异常之前,就会把这些对象列入回收范围进行二次回收。

弱引用(Weak Reference):它的引用比软引用更弱一些,被弱引用关联的对象只能存活到下一次垃圾回收发生之前。

虚引用(Phantom Reference):也被称为幽灵引用或幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用的唯一目的便是能在这个对象被回收时收到一个通知。

5.介绍下 Java 内存区域(运⾏时数据区)

程序计数器

程序计数器是一块较小的内存空间,线程私有,可以看作当前线程执行字节码的行号指示器。其主要有两个作用:

a.字节码解释器工作时通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理等功能都依赖这个计数器来完成

b.在多线程情况下,程序计数器用于记录当前线程的位置,从而当前线程被切换回来的时候能知道该线程上次运行到哪了,这也就是为何该区域线程私有。

注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建⽽创建,随着线程的结束⽽死亡。

Java虚拟机栈

该区域也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java内存可以粗糙的区分为堆内存和栈内存,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中拥有局部变量表、操作数栈(操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间)、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后(return语句或抛出异常),都会有一个栈帧被弹出。

局部变量表中主要存放了编译器可知的各种数据类型(booleanbytecharshortintfloat、 long、double )、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java虚拟机栈会出现两种异常信息:StackOverFlowError OutOfMemoryError

  • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态拓展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就会抛出StackOverFlowError异常
  • OutOfMemoryError:若Java虚拟机栈的内存大小允许动态拓展,且当线程请求栈时内存用完了,无法再动态拓展了,就会抛出OutOfMemoryError异常。

本地方法栈

和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。 本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数栈、动态链接、出⼝信息。 ⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和OutOfMemoryError 两种异常。

Java虚拟机堆

Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例和数组都是在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。从垃圾回收的角度,由于现在收集器基本都采用分带垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From survivor、To survivor空间等。进一步划分是为了更好的回收内存和更快的分配内存。

方法区(JDK1.8之后变为元空间):

方法区和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

方法区和永久代之间的关系很像Java中接口和类的关系,而永久代就是hotspot虚拟机对虚拟机规范中方法区的一种实现方式。

运行时常量池也是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)。字面量主要包括文本字符串、声明为final的常量值、基本数据类型的值等数据,符号引用主要有类和结构的完全限定名、字段名称和描述符、方法名称和描述符

直接内存

直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致OutOfMemoryError 异常出现。

JDK1.4 中新加⼊的 NIO(New Input/Output) ,引⼊了⼀种基于通道(Channel缓存区(Buffer I/O ⽅式,它可以直接使⽤ Native 函数库直接分配堆外内存,然后通过⼀个存储在Java 堆中的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样就能在⼀些场景中显著提⾼性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

6.如何判断⼀个类是⽆⽤的类?

判定⼀个常量是否是废弃常量⽐较简单,⽽要判定⼀个类是否是⽆⽤的类的条件则相对苛刻许多。

类需要同时满⾜下⾯3个条件才能算是 ⽆⽤的类

  • 该类的所有实例都已经被回收,也就是Java堆中不存在任何该类的实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.long.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

7.常⻅的垃圾回收器有那些?

Serial收集器

Serial(串行)收集器是最基本的垃圾收集器,单线程,这意味着它在进行垃圾收集工作的时候必须暂停其他所有工作线程直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

ParNew收集器

Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等)和Serial收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在server模式下虚拟机的首选,除了Serial收集器外,只有它能与CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等收集器的关注点更多是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间和CPU总消耗时间的比值。

Serial Old收集器

Serial 收集器的⽼年代版本 ,它同样是⼀个单线程收集器。它主要有两⼤⽤途:⼀种⽤途是在 JDK1.5 以及以前的版本中与Parallel Scavenge 收集器搭配使⽤,另⼀种⽤途是作为 CMS 收集器的后备⽅案。

Parallel Old收集器

Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS收集器是Hotspot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器与用户线程基本上同时工作。

CMS收集器是基于标记-清除算法实现的,它的运作过程分为四个步骤:

  • 初始标记:暂停所有的其他线程,并记录下直接与GC Root相连的对象,速度很快;
  • 并发标记:同时开启收集和用户线程,用一个闭包结构去记录可达对象,这个闭包结构并不能保证当前所有的可达对象。因为用户线程可能会不停的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法会跟踪记录这些发生引用更新的地方;
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的停顿稍长,远远比并发标记阶段时间段;
  • 并发清除:开启用户线程,同时GC线程开始为标记的区域做清扫。

优点:并发收集、低停顿,用户体验最好,缺点:对CPU资源敏感、无法处理浮动垃圾、会产生大量的空间碎片

G1收集器

G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器 , 主要针对配备多颗处理器及⼤容量内存的机器 .
以极⾼概率满⾜ GC 停顿时间要求的同时 , 还具备⾼吞吐量性能特征 .
被视为 JDK1.7 HotSpot 虚拟机的⼀个重要进化特征。它具备⼀下特点:
  • 并⾏与并发G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPUCPU或者CPU核⼼)来缩Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
  • 分代收集 :虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合 :与 CMS 标记 -- 清理 算法不同, G1 从整体来看是基于 标记整理 算法实现的收集
    器;从局部上来看是基于 复制 算法实现的。
  • 可预测的停顿 :这是 G1 相对于 CMS 的另⼀个⼤优势,降低停顿时间是 G1 CMS 共同的关注点, 但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M毫秒的时间⽚段内。
    G1收集器的运作⼤致分为以下⼏个步骤:
  • 初始标记
  • 并发标记最终标记
  • 初始标记
  • 并发标记最终标记
  • 筛选回收
G1 收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的 Region( 这也就是它的名字Garbage-First 的由来 ) 。这种使⽤ Region 划分内存空间以及有优先级的区域回收⽅式, 保证了GF 收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

8.介绍⼀下类⽂件结构吧!

1.魔数:确定这个文件是否为一个能被虚拟机接收的Class文件。

2.Class文件版本:Class文件的版本号,保证编译正常执行。

3.常量池:主要存放两大常量:字面量和符号引用。

4.访问标志:标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否是public或abstract类型,如果是类是否声明为final等。

5.当前类索引,父类索引

6.接口索引集合:用来描述这个类实现了哪些接口,这些接口将按implements后的顺序从左到右排列在接口索引结合中。

7. 字段表集合 :描述接⼝或类中声明的变量。字段包括类级变量以及实例变量,但不包括在⽅法

内部声明的局部变量。
8. ⽅法表集合 :类中的⽅法。
9. 属性表集合 : 在 Class ⽂件,字段表,⽅法表中都可以携带⾃⼰的属性表集合。

9.知道类加载的过程吗?

类加载的过程分为三步:加载>连接>初始化,连接过程又可分为三步:验证>准备>解析。

加载

加载时主要完成下面三件事:

1.通过全限定名获取此类的二进制字节流

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

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

一个非数组类的加载阶段是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式。数组类型由Java虚拟机直接创建。

连接

验证阶段主要分为以下4步:

1.文件格式验证:验证字节流是否符合Class文件格式规范,例如:是否以0xCAFEBEBE开头、主次版本号是否在当前虚拟机的处理范围内,常量池中的常量是否有不被支持的类型。

2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求

3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作。

4.符号引用验证:确保解析动作能够正确执行。

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区内分配。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

初始化阶段是执行初始化方法<clinit> ()的过程,是类加载的最后一步,这一步虚拟机才开始真正执行类中定义的Java程序代码(字节码)。

对于<clinit> () 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit> () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。

对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 new 、 getstaticputstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("...")newInstance() 等等。如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

10.双亲委派模型知道吗?能介绍⼀下吗?

每一个类都有一个对应它的类加载器。系统中的ClassLoader在协同工作的时候会默认使用双亲委派模型。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载过的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委托给父类加载器的loadClass()处理,因此最终所有的请求都应传送到顶层的启动类记载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时会使用BootstrapClassLoader作为父类加载器。

双亲委派模式保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的时两个不同的类),也保证了Java核心API不被篡改

如果我们不想使用双亲委派机制,我们可以自定义一个类加载器,并继承ClassLoader,之后重载loadClass()方法即可。

11.说一说JVM调优的经验?

1.参数-Xms和-Xmx,通常设置为相同的值,避免运行时要不断扩展JVM内存,建议扩大至3-4倍FullGC后老年代空间的占用。

2.-Xmn建议设置为1.5倍FullGC后老年代空间的占用,避免新生代设置过小,当新生代设置过小时,会带来两个问题:一是minor GC次数频繁;二是可能导致minor GC对象直接进入老年代。当老年代空间不足时会触发Full GC。避免新生代设置过大,新生代过大也会带来两个问题,一是老年代变小,可能导致Full GC频繁执行;二是minor GC执行回收的时间大幅度增加。

3.方法去设置:基于jdk1.7版本,永久代:参数-XX:PermSize和-XX:MaxPermSize; 基于jdk1.8版本,元空间:参数 -XX:MetaspaceSize和-XX:MaxMetaspaceSize; 通常设置为相同的值,避免运行时要不断扩展,建议扩大至1.2-1.5倍FullGc后的永久带空间占用

4.如何选择垃圾收集器?

a.如果你想要最小化的使用内存和并行开销,请选择Serial+Serial Old

b.如果你想要最大化应用程序的吞吐量,请选择Parallel+Parallel Old

c.如果你想最小化GC的中断或停顿时间,请选择CMS+ParNew

d.G1适合面向服务端的应用,针对具有大内存、多处理器的机器

计算机网络

1.TCP,UDP 协议的区别

  • TCP是面向连接的,UDP是无连接的,即发送数据之前不需要建立连接
  • TCP提供可靠服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  • TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络发生拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP段话,实时视频会议等)
  • 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  • TCP首部开销20字节;UPD首部开销只有8个字节
  • TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

2.select、poll、epoll 区别

1.支持一个进程所能打开的最大连接数

select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是1024,当然我们可以修改其大小,然后重新编译内核,但是性能可能受到影响。

poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接。

2.FD剧增后带来的IO效率问题

select:因为每次调用都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度以线性指数下降的性能问题。

poll:poll本质上和select没有区别

epoll:因为epoll内核中实现是根据fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者性能的线性下降问题。

3.消息传递方式

select:内核需要将消息传递到用户空间,都需要内核拷贝动作

poll:同上

epoll:epoll通过内核和用户空间共享一块内存来实现的。

3.Session和Cookie的区别?

1.cookie数据存放在客户的浏览器上,session数据存放在服务器上

2.cookie不是很安全,别人可以分析放在本地的cookie并进行cookie欺骗,主要考虑安全应该使用session,session会比较占用服务器性能,当访问增多时应用cookie

3.单个cookie在客户端的限制是3K,也就是说一个站点在客户端存放的cookie不能超过3K

4.TCP长连接和短连接知道吗?

我们知道TCP在进行读写之前,server和client之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。

所谓短连接说的就是server端与client端建立连接之后,读写完成之后就关闭掉连接,如果下一次再需要互相发送消息,就要重新连接。短连接的优缺点很明显,优点是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。

长连接说的就是client和server双方建立连接后,即使client和server完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接可以省去较多的TCP建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户端来说,非常适合用长连接。

5.为什么需要心跳机制?

在TCP保持长连接的过程中,可能会出现断网等网络异常情况,异常发生的时候,client与server之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题,我们就需要引入心跳机制。

心跳机制的工作原理是:在client与server之间在一定时间内没有收到数据交互时,即处于idle状态时,客户端或者服务器就会发送一个特殊的数据包给对方,当接收方收到这个数据报文后,也立即发送一个特殊的数据报文,回应发送方,此即一个PING-PONG交互。这样就知道了彼此依然在线,这就确保了TCP连接的有效性。

TCP实际上自带的就有长连接选项,本身是也有心跳包机制,也就是TCP的选项:SO_KEEPALIVE。但是TCP协议层面的长连接不够灵活。所以说一般情况下我们都是在应用协议层上实现自定义心跳机制的。同过Netty实现心跳机制的话,核心类是IdleStateHandler

6.说一说TCP连接如何建立和关闭?(三次握手和四次挥手)

TCP连接的建立

1.客户端发送一个SYN报文段,并指明自己想要连接的端口号和客户端自己的初始序列号(记为ISN(c))。通常客户端还会借此发送一个或多个选项。

2.服务器也发送自己的SYN报文段作为响应,并包含了服务器自身的初始序列号(记为ISN(s))。此外,为了确认客户端的SYN,服务器将其包含的ISN(c)数值加1后作为返回的ACK数值,因此每发送一个SYN,序列号就会自动加1.这样如果出现丢包的情况,该SYN将会重传。

3.为了确认服务器的SYN,客户端将ISN(s)的数值加1后作为返回的ACK数值。

TCP连接的终止

1.连接的主动关闭者发送一个FIN段指明接收者,向接收者发送自己当前的序列号。FIN段还包含了一个ACK段用于确认对方最近一次发来的数据(假设为L)。

2.连接的被关闭者将序列号的数值加1作为响应的ACK值,以表明它已经成功接收到主动关闭者发送的FIN。此时上层的用用程序被告知连接的另一端已经提出了关闭请求。通常,这将导致应用程序发起自己的关闭操作。接着,被动关闭者将身份转变为主动关闭者,并发送自己的FIN,该报文段的序列号为L。

3.为了完成连接的关闭,最后发送的报文段还包含一个ACK用于确认上一个FIN。如果出现FIN丢失的情况,那么发送方将重新传输直到接收到一个ACK确认为止。

框架相关

1.谈谈⾃⼰对于 Spring IoC AOP 的理解

IOC是一种设计思想,就是就是将原本在程序中手动创建对象的控制权,交由Spring框架管理。IOC容器是Spring用来实现IOC的载体,IOC实际上就是个map,map中存放的是各种对象。

将对象之间的依赖关系交给IOC来处理,并由IOC来完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是怎么被创建出来的

AOP能够将那些与业务无关、却被业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来。便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy去创建代理对象,而对于没有实现接口的对象,Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个代理对象的子类来处理。

2.为什么要用Netty?

  • 统一的API,支持多种传输类型,阻塞和非阻塞的。
  • 简单而强大的线程模型
  • 自带解码器解决TCP粘包拆包问题
  • 自带各种协议栈
  • 真正的无连接数据包套接字支持
  • 比直接使用Java NIO有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
  • 安全性不错,有完整的SSL/TLS支持。

3.说一下Netty的应用场景?

1.作为RPC框架的网络通信工具。

2.实现一个自己的HTTP服务器。

3.实现一个即时通信系统。

4.实现消息推送系统,市面上有很多消息推送系统都是基于Netty来做的。

4.什么是TCP拆包/粘包?有什么解决办法呢?

TCP拆包/粘包就是基于TCP发送数据的时候,出现了多个字符串被粘在了一起或者一个字符串被拆开的问题。

解决办法:

  • LineBasedFrameDecoder : 发送端发送数据包的时候,每个数据包之间以换⾏符作为分 隔, LineBasedFrameDecoder 的⼯作原理是它依次遍历 ByteBuf 中的可读字节,判断是 否有换⾏符,然后进⾏相应的截取。
  • DelimiterBasedFrameDecoder : 可以⾃定义分隔符解码器, LineBasedFrameDecoder 实际上是⼀种特殊的 DelimiterBasedFrameDecoder 解码器。
  • FixedLengthFrameDecoder : 固定⻓度解码器,它能够按照指定的⻓度对消息进⾏相应的拆 包。
  • ⾃定义序列化编解码器

5.说一下Spring事务传播机制?

REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务。如果当前存在事务,则加入这个事务。

SUPPORTS:如果当前存在事务,则加入该事务。如果当前方法没有事务,则以非事务方法运行。

MANDATORY:当前存在事务,则加入该事务。当前不存在事务,则抛出异常。

REQUIRES_NEW::创建一个新事物,如果存在当前事务,则挂起该事务。

NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起该事务。

NEVER:不使用事务,如果当前存在事务,则抛出异常。

NESTED:如果当前事务存在,则在嵌套事务中执行,否则和REQUIRED操作一样。

6.Spring框架中使用了哪些设计模式及应用场景?

1.工厂模式:在各种BeanFactory和ApplicationContext创建中都用到了

2.模板模式:在各种BeanFactory和ApplicationContext创建中都用到了

3.代理模式:Spring AOP利用了AspectJ AOP实现的,AspectJ AOP底层使用了动态代理

4.策略模式:加载资源文件的方式,使用了不同的方法,比如,ClassPathResource,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的接口Resource;在AOP的实现中采用了两种不同的方式,JDK动态代理和CGLib代理

5.单例模式:在创建Bean的时候

6.观察者模式:spring中的ApplicationEvent,ApplcationListener,ApplicationEventPublisher

7.适配器模式:MethodBeforeAdviceAdapter,ThrowsAdviceAdapter,AfterReturningAdapter

8.装饰者模式:源码中带Wrapper或Decorator的都是

7.你的项目里都用到了哪些设计模式?

模板方法模式

我在X游戏接入平台中使用了模板方法模式,场景:研发的游戏需要推向不同的渠道,每个渠道的用户登录和充值接口的实现都不太一样,而X游戏平台作为一个游戏接入不同渠道的服务平台需要保证游戏接入的一致性,以及消除到不同渠道的差异性。
使用模板方法的好处:

  1. 固定了游戏接入的流程:都有登录,充值接入;
  2. 解耦了协议和实现:每个渠道按照对应的对接协议差别的接入,在上层抹掉差差异。
  3. 扩展性更好;可以轻松的增加新渠道;
  4. 可以更灵活处理游戏接入;

观察者模式

我在优化登录代码的过程中,使用了观察者模式。比如可以使用Spring的事件机制或者guava提供的EventBus;
之前的登录代码是面条式的,一行代码处理一个对应的逻辑,比如,登录完成之后,记录日志,通知积分服务增加积分,通知统计服务增加登录次数,变更日活;
使用观察者模式之后,发一个登录成功的消息,在监听者中处理不同的逻辑操作。简化了代码,可维护性,可扩展性得到了提高。

还有放款失败、银行卡校验、进行预审任务等都用到了观察者模式。

代理模式

使用Aspect+自定义注解,系统中大量使用了代理模式,比如检查短信验证码、检查验证码、方法鉴权、消息推送

Redis相关

1.说说什么是 Redis?

Redis是一个开源、内存存储的数据结构服务器可用作数据库、高速缓存和消息队列代理。它支持字符换、哈希表、列表、集合、有序集合、位图等数据类型。内置复制、lua脚本、LRU回收、事务,以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

2.Redis 的数据类型有哪些?

  • String:最简单的类型(底层使用动态字符串,效率较高),就是普通的set和get,做简单的KV缓存。
  • Hashe:这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没有嵌套其它对象)给缓存在redis里,然后每次读写缓存的时候,就可以操作hash里的某个字段。
  • List:有序列表。可以通过list存储一些列表型的数据结构。
  • Sets:无序集合,自动去重。你当然也可以使用Java的hashset进行去重,但是如果你的某个系统部署在多个机器上呢?得基于redis进行全局得set去重。
  • Sorted Set:有序得set。写进去一个分数,自动根据分数排序,底层基于跳表(SkipList)。可以用来做排行榜相关功能。

3.Redis 为什么设计成单线程的?

  • 绝大部分请求是纯粹的内存操作
  • 采用单线程,避免了不必要的上下文切换和竞争条件
  • 非阻塞IO,内部采用epoll,epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,避免IO代价。

4.Redis 和 Memcached 的区别有哪些?

  • Redis和Memcached都是将数据存放在内存中,都是内存数据库。不过Memcache还可以缓存其它东西,例如图片、视频等等。
  • Memcached仅支持key-value结构的数据类型,Redis支持KV、list、set等多种数据类型
  • Memcached单个value最大1m,Redis的单个value最大512m
  • Memcached挂掉以后,数据没了;Redis数据丢失后可以通过aof恢复
  • Memcached没有原生的集群模式,需要依赖客户端实现,Redis原生就支持集群模式Redis Cluster

5.Redis 有几种持久化方式?

Redis提供了两种方式,实现数据的持久化到硬盘。

1.【全量】RDB持久化,是指在指定时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

优点:

  • 可以灵活设置备份频率和周期
  • 非常适合冷备份,对于灾难恢复而言,RDB是非常不错的选择,我们可以轻松的将一个单独的文件压缩后再转移到其它存储介质上。
  • 性能最大化。RDB对Redis对外提供读写服务的影响非常小,可以让Redis保持高性能。
  • 恢复更快。相比于AOF机制,RDB的恢复速度锋快,特别是在数据集非常大的时候。

缺点:

  • 如果你想保证数据的高可用性,即最大限度的避免数据丢失, 那么 RDB 将不是一个很好的选择。因为系统一旦在定时持久化 之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
  • 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因 此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。

2.【增量】AOF持久化,以日志形式记录服务器所处理的每一个写操作,以文本的方式记录。

优点:

  • 该机制可以带来更高的数据安全性。Redis中提供了三种同步策略,每秒同步、每修改同步和不同步
  • 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏文件中已经存在的内容
  • 如果AOF文件过大,Redis可以自动启用rewrite机制。即使出现后台重写操作,也不会影响客户端读写。
  • AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事 实上,我们也可以通过该文件完成数据的重建。

缺点:

  • 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB在恢复大数据集时速度比AOF的恢复速度要快。
  • 根据同步策略的不同,AOF的运行效率往往会慢于RDB。

6.两种持久化方式该如何选择?

bgsave做镜像全量持久化, AOF做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量的数据丢失,所以需要AOF来配合使用。在Redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完全恢复重启之前的状态。

7.Redis 有哪几种数据“淘汰”策略?

Redis一共有6种数据淘汰策略:

1.noeviction:当到达内存限制时返回错误,并且客户端会发出尝试使用更多内存的命令。

2.allkeys-lru:尝试回收最少使用的键,使得新添加的数据有空间存放。

3.volitile-lru:尝试回收最少使用的键,但仅限于在过期集合的键,使得新添加的数据有空间存放。

4.allkeys-random:回收随机的键,使得新添加的数据有空间存放。

5.volitile-random:回收随机的键,但仅限于在过期集合的键,使得新添加的数据有空间存放。

6.volitile-ttl:回收在过期集合的键,并且优先回收存活时间较短的键,使得新添加的数据有空间存放。

8.什么是Redis事务?

可以一次性执行多条命令,本质上是一组命令的集合。一个事务中的所有命令都会序列化,然后顺序地串行化执行,而不会被插入其他命令。

Redis事务不支持回滚,如果事务中有错误的操作,无法回滚到处理前的状态。在执行完当前事务内的所有指令前,不会同时执行其他客户端的请求。

9.什么是缓存穿透?怎么解决?

大量的请求瞬时涌入系统,而这个数据在Redis中不存在,所有的请求到落到数据库上而导致数据库崩溃。造成这种情况的可能有1)系统设计不合理2)缓存更新不及时3)爬虫恶意攻击。

解决方案有:

1.使用布隆过滤器,将查询参数都存储到一个bitmap里,查询前先看该参数是否存在,如果存在了则进行查询,不存在则进行拦截。

2.缓存空对象,如果从数据库查询到结果为空,依然把这个结果进行缓存。

10.什么是缓存雪崩?怎么解决?

缓存雪崩是指大量缓存失效时,大量的请求直接访问数据库而导致数据库挂掉。

解决方案:

1.合理规划缓存的失效时间,可以给缓存时间加一个随机数,防止同一时间过期。

2.合理评估数据库的负载压力。

3.对数据库进行过载保护或者在应用层进行限流。

4.可以考虑多级缓存的设计,实现缓存的高可用。

11.Redis 高可用方案有哪些?

Redis 单副本

Redis 单副本,采用单个 Redis 节点部署架构,没有备用节点实时同步数据, 不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。

Redis 多副本(主从)

Redis 多副本,采用主从(replication)部署结构,相较于单副本而言最大的特 点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例 部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提 供服务和读写分离策略。

Redis Sentinel(哨兵)

Redis Sentinel 是社区版本推出的原生高可用解决方案,其部署架构主要包括 两部分:Redis Sentinel 集群和 Redis 数据集群。 其中 Redis Sentinel 集群是由若干 Sentinel 节点组成的分布式集群,可以实现 故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel 的节点数 量要满足 2n+1(n>=1)的奇数个。

Redis Cluster

Redis Cluster 是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分 布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。 Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供 读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。 Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整 数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

消息队列相关

1.为什么要使用消息队列?

异步处理:相比于传统的串行、并行的方式,提高了系统吞吐量。

应用解耦:系统间通过消息通信,不用关心其它兄通的处理。

流量削峰:可以通过消息队列长度控制请求量;可以缓解短时间内的并发请求。

日志处理:解决大量日志传输。

消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯消息通讯。比如点对点消息队列,或者聊天室等。

2.ActiveMQ、RabbitMQ、RocketMQ、Kafka的区别?

3.RabbitMQ交换机有哪几种?

fanout:广播模式 队列直接绑定该交换机(不需要指定routing key)。

direct:消息队列由那些binding key与routing key完全匹配到Queue中。

topic:前面提到的direct规则是严格意义上的匹配,换言之Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue,那么topic这个规则就是模糊匹配,可以通过通配符满足一部分规则就可以传送。

headers:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。

4.如何保证消息的幂等性?

1.可在内存中维护一个set,只要从消息队列里获取到一个消息,先查询这个消息在不在set里边,如果在表示已经消费过,直接丢弃;如果不在再进行消费并存入set中。

2.如果要写数据库,可以先拿唯一键先去数据库查询一下,如果不存在则消费消息,否则直接丢弃。

3.如果是写到redis里则没有问题,每次都是set,天然的幂等性。

4.让生产者消费消息时,每条消息加上一个全局的唯一id,然后消费时,将该id保存到redis里。消费时先查询一下该id在redis里有没有,没有再消费。

5.数据库设置唯一键,这样插入只会报错而不会插入重复数据。

5.RabbitMQ由哪些部分组成?(RabbitMQ的工作流程)

Message

消息由消息头和消息体组成。消息体是不透明的,而消息头则有一系列的可选属性组成,这些属性包括Routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(是否持久化)等。

Publisher

消息的生产者,也是一个向交换器发布消息的客户端应用程序。

Exchange

交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

交换机有4中类型:direct(默认)、fanout、topic、header,不同的交换机发送消息的策略有所区别。

Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可以投入一个或多个队列。消息一直在队列里边,等待消费者连接到这个队列将其取走。

Binding

用于消息队列和交换机之间的关联。一个绑定就是基于路由键将交换机和消息队列连接起来的路由规则,所以可以将交换机理解为一个由绑定构成的路由表。

Connection

网络连接,比如TCP连接。

Channel

信道,多路复用中的一条独立的双向数据流通道。信道是建立在真实TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的,不管发送消息、订阅队列、接收消息这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

Consumer

消息的消费者,表示从消息队列中取得消息的客户端应用程序。

Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加 密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥 有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时 指定,RabbitMQ 默认的 vhost 是 / 。

Broker

表示消息队列的服务器实体。

6.RabbitMQ如何防止消息丢失?

消息丢失分为以下几种情况:

1.消息发送出去,由于网络问题未抵达服务器

做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式

2.消息抵达Broker,此时Broker上未将消息持久化,宕机

publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。

3.自动ACK状态下,消费者收到消息但没来得及消费然后宕机

一定要开启手动ACK,消费成功才移除,失败或没来得及处理就noACK并重新入对。

数据库相关

1.MyISAM和InnoDB区别

1.是否支持行级锁:MyISAM只有表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁。

2.是否支持事务和崩溃后的安全恢复:MyISAM强调的是性能,每次查询具有原子性,其执行速度比InnoDB更快,但是不提供事务支持。但是InnoDB提供事务支持,外部键等高级数据库功能。

3.是否支持外键:MyISAM不支持,而InnoDB支持。

4.是否支持MVCC:仅InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效,MVCC仅在READ COMMITTEDREPEATED READ两个隔离级别下工作,MVCC可以使用乐观锁和悲观锁来实现。

2.事务隔离级别有哪些?MySQL的默认隔离级别是?

READ-UNCOMMITTED(读未提交):最低的隔离级别,允许读取尚未提交的数据变更脏读、幻读或不可重复读。

READ-COMMITTED(读提交):允许读取并发事务已提交的数据,可以防止脏读,但是幻读和不可重复读仍有可能发生。

REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非事务是被本身事务所修改,幻读仍可能发生,可重复读是Mysql默认的事务隔离级别。

SERIALIZABLE(串⾏化):最高的隔离级别。所有事物依次逐个执行,这样事务之间就完全没有互相干扰,可以防止脏读、幻读、不可重复读。

3.表级锁和⾏级锁的区别?

表级锁:Mysql中锁定粒度最大的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。但是其并发度最低

行级锁:Mysql中锁定粒度最小的一种锁,只针对当前操作的行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,并发度高,但加锁的开销也大,加锁慢,可能会出现死锁

4.Mysql的基本结构?

  • 连接器:身份认证和权限相关(登录MySQL的时候)。
  • 查询缓存:执行查询语句的时候,会先查询缓存(8.0后移除,因为该功能不常用)。
  • 分析器:未命中缓存时,SQL语句就会经过分析器,分析器说白了就是要先看你的SQL语句要干嘛,再检查SQL语句语法是否正确。
  • 优化器:按照MySQL认为最优的方案去执行。
  • 执行器:执行语句然后存储引擎返回结果。

5.一条SQL语句是如何执行的?

SQL语句可以分为两种,一种是查询,一种是更新(增删改)。

查询语句:

1.先检查该语句是否有权限,没有权限则直接返回错误信息,有的话在MySQL8.0之前会先查询缓存。

2.通过分析器进行词法分析,提取SQL语句的关键元素,比如查询时读还是写操作,提取需要查询的表名,需要查询的所有列,查询条件。然后判断该语句是否有语法错误。

3.优化器确定执行方案,比如有多个查询条件,是先执行A条件还是B条件,优化器根据自己的优化算法进行选择执行效率最好的方案(不一定最好)。

4.进行权限校验,如果没有返回错误信息,有的话调用数据库存储引擎接口返回引擎的执行结果。

更新语句:

其实流程跟查询语句差不多,只不过执行更新的时候肯定要记录日志了,这就会引入日志模块了,MySQL自带的日志模块binlog(归档日志),所有的存储引擎都可使用,InnoDB引擎还自带了一个redo log(重做日志)。

1.先查询到一条数据如果有缓存就走缓存。

2.修改结果集的值,然后调用引擎API接口,写入这一行数据,InnoDB把数据保存到内存中,同时记录redo log,此时redo log进入prepare状态,然后告诉执行器执行完成了随时可以提交。

3.执行器接收通知后记录binlog,然后调用引擎接口,提交redo log为提交状态。至此更新完成。

这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?

这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。

并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做?

先写 redo log 直接提交,然后写 binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 binlog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。

先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。

如果采用 redo log 两阶段提交的方式就不一样了,写完 binglog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:

•判断 redo log 是否完整,如果判断是完整的,就立即提交。

•如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。

这样就解决了数据一致性的问题。

说一说Mysql整体的调优思路?

表设计阶段

1.表结构要尽量遵循三大范式原则。

2.如果查询应用多,尤其是需要进行多表查询的时候,可以进行反范式话,通过增加冗余字段增加查询效率。

3.表字段的数据类型选择合理,可以采用数值类型就不要采用字符类型;字符类型要尽可能设计的短一点,当字符串长度固定时,就使用CHAR类型,长度不固定使使用VARCHAR,避免使用 TEXT、BLOG这样的大数据类型,使用TIMESTAMP(4字节)存储时间,用DECIMAL 代替 FLOAT 和 DOUBLE 存储精确浮点数。

4.拆分表:数据冷热分离,拆分表的思路是,把一个包含很多字段的表拆分为2个或多个相对较小的表。这样做的原因是,这些表中某些字段的操作频率很高(热数据),经常要进行查询或更新操作,而另外一些字段的使用频率却很低(冷数据),冷热数据分离,可以减小表的宽度。

5.增加中间表:对于经常需要联合查询的表,可以建立表以提高查询效率。通过建立中间表,把经常需要联合查询的数据插入中间表中,然后将原来的联合查询改为对中间表的查询,以此来提升查询效率。

查询优化

SQL 查询优化可以分为 逻辑查询优化 和 物理查询优化。

逻辑查询优化就是改变SQL语句的内容让SQL执行效率更高,采用的方式是对SQL语句进行等价变换,对查询进行重写。

对SQL查询重写包括子查询优化、等价谓词重写、视图重写、条件简化、连接消除和嵌套连接消除等。

物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(如索引),通过计算代价模型对各种可能的访问路径进行估算,从而找到执行方法中代价最小的作为执行计划。(使用索引时还要注意擅用explain来分析索引是否失效)

注:MySQL索引失效的几种情况。

  • 未符合最左匹配原则
  • 在索引列上进行计算、使用函数、强制类型转换。
  • is not null会导致索引失效
  • like %出现在关键词左边会导致索引失效
  • 范围条件右边的列索引会失效
  • 字符串不加单引号会导致索引失效
  • 不等于会导致索引失效
  • or来连接条件也会导致索引失效

库级优化

库级优化是在数据库的维度上进行的优化策略,如控制一个库中的数据表数量

1.读写分离: 为了提高系统的性能,优化用户体验,可以采用读写分离的方式降低主数据库的负载,比如用主数据库(master)完成写操作,用从数据库完成读操作。读写分离适用与读远大于写的场景。读写分离实现的基础是主从复制。主数据库利用主从复制将自身数据的改变同步到从数据库集群中,然后主数据库负责写操作,从数据库处理读操作。并可以根据压力情况,部署多个从数据库提高读操作的速度。

2.数据分片:对数据库分库分表。当数据量级达到千万级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。MySQL可以直接使用自带的分区表功能,当然也可以自己设计垂直拆分(分库)、水平拆分(分表),或垂直加水平拆分。

优化服务器硬件

服务器的硬件性能直接决定着MySQL数据库的性能。硬件的性能瓶颈直接决定MySQL数据库的运行速度和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。

(1)配置较大的内存。足够大的内存是提高MySQL数据库性能的方法之一。内存的速度比磁盘I/O快得多,可以通过增加系统的缓冲区容量使数据在内存中停留的时间更长,以减少磁盘I/O。

(2)配置高速磁盘系统,以减少读盘的等待时间,提高响应速度。磁盘的I/O能力,也就是它的寻道能力,目前的SCSI高速旋转的是7200转/分钟,这样的速度,一旦访问的用户量上去,磁盘的压力就会过大,如果是每天的网站pv (page view)在150w,这样的一般的配置就无法满足这样的需求了。现在SSD盛行,在SSD上随机访问和顺序访问性能几乎差不多,使用SSD可以减少随机I/O带来的性能损耗。

(3)合理分布磁盘I/O,把磁盘I/O分散在多个设备上,以减少资源竞争,提高并行操作能力。

(4)配置多处理器,MySQL是多线程的数据库,多处理器可同时执行多个线程。
 

最后

以上就是平淡云朵为你收集整理的Java面试题多线程相关虚拟机相关计算机网络框架相关Redis相关消息队列相关数据库相关的全部内容,希望文章能够帮你解决Java面试题多线程相关虚拟机相关计算机网络框架相关Redis相关消息队列相关数据库相关所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部