Java基础

  • JAVA 中的几种数据类型是什么,各自占用多少字节。

  • String 类能被继承吗,为什么。

不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。

*很多时候会容易把static和final关键字混淆,static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变

  • 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

两个对象equals相等,则它们的hashcode必须相等,反之则不一定。
两个对象==相等,则其hashcode一定相等,反之不一定成立。

原因:

其实hashCode无非是通过算法返回的一个int值

知道了实现方式以后,这个题目也就不难理解了,我们可以推测是不是这个算法会有两个对象算出来的结果是相等的?(虽然几率很小但我相信还是存在的)。如果仅仅是单纯的回答这个问题,我可以用一个比较极端的方式,自己改写hashCode的生成方式就好了。

  • String 属于基础的数据类型吗?

不属于。
Java8种基础的数据类型:byte、short、char、int、long、float、double、boolean。

  • Java 中操作字符串都有哪些类?它们之间有什么区别?
  • String
  • StringBuffer
  • StringBuilder

这三个类都是以char[]的形式保存的字符串,但是String类型的字符串是不可变的,对String类型的字符床做修改操作都是相当于重新创建对象.而对StringBuffer和StringBuilder进行增删操作都是对同一个对象做操作.

StringBuffer中的方法大部分都使用synchronized关键字修饰,所以StringBuffer是线程安全的,StringBuilder中的方法则没有,线程不安全,但是StringBuilder因为没有使用使用synchronized关键字修饰,所以性能更高,在单线程环境下我会选择使用StringBuilder,多线程环境下使用StringBuffer.如果生命的这个字符串几乎不做修改操作,那么我就直接使用String,因为不调用new关键字声明String类型的变量的话它不会在堆内存中创建对象,直接指向String的常量池,并且可以复用.效率更高.

  • Java 中 IO 流分为几种?
  • 按照流的流向分,可以分为输入流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的角色划分为节点流和处理流。

按操作对象分类结构图:

按操作方式分类结构图:

  • BIO、NIO、AIO 有什么区别?
  • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
  • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
  • 用过哪些 Map 类,都有什么区别,HashMap 时线程安全的吗,并发下使用的 Map 是什么,他们的内部原理分别是什么,比如存储方法,hashcode,扩容,默认容量等。

HashMap、HashTable、LinkedHashMap和TreeMap。

HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
遍历时,取得数据的顺序是完全随机的。
HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null
HashMap不支持线程的同步,是非线程安全的,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用 Collections和synchronizedMap方法使HashMap具有同步能力,或者使用ConcurrentHashMap。

其中最频繁的是HashMap和ConcurrentHashMap,他们的主要区别是HashMap是非线程安全的。ConcurrentHashMap是线程安全的。

并发下可以使用ConcurrentHashMap和HashTable,他们的主要区别是:

1.ConcurrentHashMap的hash计算公式:(key.hascode()^ (key.hascode()>>> 16)) & 0x7FFFFFFF

HashTable的hash计算公式:key.hascode()& 0x7FFFFFFF

2.HashTable存储方式都是链表+数组,数组里面放的是当前hash的第一个数据,链表里面放的是hash冲突的数据

ConcurrentHashMap是数组+链表+红黑树

3.默认容量都是16,负载因子是0.75。就是当hashmap填充了75%的busket是就会扩容,最小的可能性是(16*0.75),一般为原内存的2倍

4 .线程安全的保证:HashTable是在每个操作方法上面加了synchronized来达到线程安全,ConcurrentHashMap线程是使用CAS(compore and swap)来保证线程安全的

  • 如何将字符串反转?

1. StringBuilder(str).reverse()

在Java中,我们可以使用StringBuilder(str).reverse()使字符串字母倒序。

2. char[]

这一段我们使用 char[]数组进行实现,那要如何做呢?其实也很简单,通过如下几步即可:

将字符串转为 char[]数组逐个循环 char[]数组使用 temp 变量进行值交换

Byte[] – StringBuilder(str).reverse(str)

我们看看下面这段代码,是不是很类似于StringBuilder(str).reverse()的内部实现(UTF16内容除外)。

4. Apache commons-lang3

对于Apache commons-lang3库,我们可以使用StringUtils.reverse反转字符串和StringUtils.reverseDelimited反转单词。这里我主要介绍使用方法,有兴趣的朋友可以取看看内部实现,欢迎大家能在评论区留言进行交流,共同进步!

  • 抽象类必须要有抽象方法吗?

不需要,

抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类。(有抽象方法就是抽象类,是抽象类可以没有抽象方法)

解释:

抽象方法:

java中的抽象方法就是以abstract修饰的方法,这种方法只声明返回的数据类型、方法名称和所需的参数,没有方法体,也就是说抽象方法只需要声明而不需要实现。

抽象方法与抽象类:

当一个方法为抽象方法时,意味着这个方法必须被子类的方法所重写,否则其子类的该方法仍然是abstract的,而这个子类也必须是抽象的,即声明为abstract。abstract抽象类不能用new实例化对象,abstract方法只允许声明不能实现。如果一个类中含有abstract方法,那么这个类必须用abstract来修饰,当然abstract类也可以没有abstract方法。 一个抽象类里面没有一个抽象方法可用来禁止产生这种类的对象。

Java中的抽象类:

abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。

在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。

  • 普通类和抽象类有哪些区别?

关键点:abstract修饰符(抽象方法)、具体实现过程、实例化、子类实现父类的抽象方法

  1. 普通类中不可含有抽象方法,可以被实例化;
  2. 抽象类,则抽象类中所有的方法自动被认为是抽象方法,没有实现过程,不可被实例化;抽象类的子类,除非也是抽象类,否则必须实现该抽象类声明的方法
  • 抽象类能使用 final 修饰吗?

不能,抽象类的就是要子类继承然后实现内部方法的。但是final修饰的类是不能再被继承和修改的。所以不能用final修饰。

  • ArrayList 和 LinkedList 有什么区别?

有的人可能会说ArrayList底层是一个数组,所以查询快,LinkedList底层是一个链表,所以增删快.

那么为什么数组查询就快了呢?因为假设数组里面保存的是一组对象,每个对象都有内存大小,例如对象中有一个字段是int类型占用4个字节(只考虑实际数据占用的内存),数组在堆上的内存在数组被创建出来就被确定了是40个字节.如果我们要查找第9个对象,可以通过(9-1)*4=32,从33到36字节就是我们要找的对象.是不是很快呢?而链表却不能做到这样的效率.如上图,我们要找到A4,必须先找到A3,再先找到A2,再先找到A1.这样的查找效率会大大降低.

好了,说了查找,再说说插入,数组的插入也相当的浪费效率,如果要在数组内的某一个位置进行插入,需要先将插入位置的前面复制一份,然后在新的数组后面添加新的元素,最后将旧的数组后半部分添加的新的数组后面,而在链表中插入就变得相当简单了,比如我要在A3和A4中插入B,只需定位到A3的指针和A4的数据即可,将A3的指针指向B的值,将B的指针指向A4的值,B就插入进了链表.

  • ConcurrentHashMap的数据结构(必考)

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表.

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本.

  • volatile作用(必考)

1 保证内存可见性

可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

2 禁止指令重排

指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。

latile变量禁止指令重排序。针对volatile修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏

volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

JVM内存屏障插入策略:

每个volatile写操作的前面插入一个StoreStore屏障;

在每个volatile写操作的后面插入一个StoreLoad屏障;

在每个volatile读操作的后面插入一个LoadLoad屏障;

在每个volatile读操作的后面插入一个LoadStore屏障。

适用场景

(1)volatile是轻量级同步机制。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制。

(2)volatile**无法同时保证内存可见性和原子性。加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性**。

(3)volatile不能修饰写入操作依赖当前值的变量。声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。

(4)当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile;

(5)volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

  • Atomic类如何保证原子性(CAS操作)(必考)

CAS

全称是CompareAndSwap,它是一条CPU并发原语。用来判断内存某个位置的值是否为预期值,如果是则改为更新的值,这个过程是原子的。AtomicInteger之所以能保证原子性是依赖于UnSafe类,这个类是Java最底层的类之一,里面都是很屌的native方法,都是其他语言写的,咱看不见,Unsafe类可以执行以下几种操作:

  • 分配内存,释放内存
  • 可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法
  • 挂起和恢复线程,被封装在LockSupport类中供使用
  • CAS

CAS的缺点

  • 如果循环时间长,CPU开销很大
  • 只能保证一个共享变量的原子操作,对于多个共享变量操作时,循环CAS无法保证操作的原子性,这时候只能使用锁来保证
  • 会有ABA问题

ABA

CAS算法是取出内存中某时刻的数据并在当下时刻比较并替换,这中间会有一个时间差导致数据变化。比如说有两个线程,一快一慢,同时从主内存中取变量A,快线程将变量改为B并写入主内存,然后又将B从主内存中取出再改为A写入主内存,这时候慢线程刚完成了工作,使用CAS算法,发现预期值还是A,然后慢线程将自己的结果写入主内存。虽然慢线程操作成功,但是这个过程可能是有问题的

原子引用

AtomicReference用来把特定对象编程原子类,AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等,也就是它可以保证你在修改对象引用时的线程安全性。

原子引用时间戳

对于ABA问题可以在比较值的同时加上版本号,或者说是时间戳。AtomicStampedReference
这个类在创建的时候要求指定一个初始的版本号,并且每次做CAS操作的时候要求对比版本号,且版本号需要自增

  • 为什么要使用线程池(必考)

为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池.

java中线程池的顶级接口是Executor(e可rai kei ter),ExecutorService是Executor的子类,也是真正的线程池接口,它提供了提交任务关闭线程池等方法。调用submit方法提交任务还可以返回一个Future(fei 曲儿)对象,利用该对象可以了解任务执行情况,获得任务的执行结果取消任务

由于线程池的配置比较复杂,JavaSE中定义了Executors类就是用来方便创建各种常用线程池的工具类。通过调用该工具类中的方法我们可以创建单线程池(newSingleThreadExecutor),固定数量的线程池(newFixedThreadPool),可缓存线程池(newCachedThreadPool),大小无限制的线程池(newScheduledThreadPool),比较常用的是固定数量的线程池和可缓存的线程池,固定数量的线程池是每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行.可缓存线程池是当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行.

Executors类中还定义了几个线程池重要的参数,比如说int corePoolSize核心池的大小,也就是线程池中会维持不被释放的线程数量.int maximumPoolSize线程池的最大线程数,代表这线程池汇总能创建多少线程。corePoolSize :核心线程数,如果运行的线程数少corePoolSize,当有新的任务过来时会创建新的线程来执行这个任务,即使线程池中有其他的空闲的线程。maximumPoolSize:线程池中允许的最大线程数.

Redis

  • Redis的应用场景

高性能适合当做缓存

缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。
作为缓存使用时,一般有两种方式保存数据:

  • 1、读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。
  • 2、插入数据时,同时写入Redis。

方案一:实施起来简单,但是有两个需要注意的地方:
1、避免缓存击穿。(数据库没有就需要命中的数据,导致Redis一直没有数据,而一直命中数据库。)
2、数据的实时性相对会差一点。

方案二:数据实时性强,但是开发时不便于统一处理。

当然,两种方式根据实际情况来适用。如:方案一适用于对于数据实时性要求不是特别高的场景。方案二适用于字典表、数据量不大的数据存储。

丰富的数据格式性能更高,应用场景丰富

Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型 说明
string 字符串,最简单的k-v存储
hash hash格式,value为field和value,适合ID-Detail这样的场景。
list 简单的list,顺序列表,支持首位或者末尾插入数据
set 无序list,查找速度快,适合交集、并集、差集处理
sorted set 有序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

  • string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。
  • hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情,个人信息详情,新闻详情等。
  • list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。
  • set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同的好友等。
  • Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。

如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。

单线程可以作为分布式锁

谈到Redis和Memcached 的区别,大家更多的是谈到数据结构和持久化这两个特性,其实还有一个比较大的区别就是:

  • Redis 是单线程,多路复用方式提高处理效率。
  • Memcached 是多线程的,通过CPU线程切换来提高处理效率。

所以Redis单线程的这个特性,其实也是很重要的应用场景,最常用的就是分布式锁。
应对高并发的系统,都是用多服务器部署,每个技术框架针对数据锁都有很好的处理方式,如 .net 的lock,java 的synchronized,都能通过锁住某个对象来应对线程导致的数据污染问题。但是毕竟,只能控制本服务器的线程,分布式部署以后数据污染问题,就比较难处理了。Redis的单线程这个特性,就非常符合这个需求,伪代码如下:

//产生锁
while lock!=1//过期时间是为了避免死锁now = int(time.time())lock_timeout = now + LOCK_TIMEOUT + 1lock = redis_client.setnx(lock_key, lock_timeout)//真正要处理的业务
doing() //释放锁
now = int(time.time())
if now < lock_timeout:redis_client.delete(lock_key)

以上是一个只说明流程的伪代码,其实整体的逻辑是很简单的,只要考虑到死锁时的情况,就比较好处理了。Redis作为分布式锁,因为其性能的优势,不会成为瓶颈,一般会产生瓶颈的是真正的业务处理内容,还是尽量缩小锁的范围来确保系统性能。

自动过期能有效提升开发效率

Redis针对数据都可以设置过期时间,这个特点也是大家应用比较多的,过期的数据清理无需使用方去关注,所以开发效率也比较高,当然,性能也比较高。最常见的就是:短信验证码、具有时间性的商品展示等。无需像数据库还要去查时间进行对比。因为使用比较简单,就不赘述了。

分布式和持久化有效应对海量数据和高并发

Redis初期的版本官方只是支持单机或者简单的主从,大多应用则都是自己去开发集群的中间件,但是随着应用越来越广泛,用户关于分布式的呼声越来越高,所以Redis 3.0版本时候官方加入了分布式的支持,主要是两个方面:

  • Redis服务器主从热备,确保系统稳定性
  • Redis分片应对海量数据和高并发

而且Redis虽然是一个内存缓存,数据存在内存,但是Redis支持多种方式将数据持久化,写入硬盘,所有,Redis数据的稳定性也是非常有保障的,结合Redis的集群方案,有的系统已经将Redis当做一种NoSql数据存储来适用。

示例:秒杀和Redis的结合

秒杀是现在互联网系统中常见的营销模式,作为开发者,其实最不愿意这样的活动,因为非技术人员无法理解到其中的技术难度,导致在资源协调上总是有些偏差。秒杀其实经常会出现的问题包括:

  • 并发太高导致程序阻塞。
  • 库存无法有效控制,出现超卖的情况。

其实解决这些问题基本就两个方案:

  • 数据尽量缓存,阻断用户和数据库的直接交互。
  • 通过锁来控制避免超卖现象。

现在说明一下,如果现在做一个秒杀,那么,Redis应该如何结合进行使用?

  1. 提前预热数据,放入Redis
  2. 商品列表放入Redis List
  3. 商品的详情数据 Redis hash保存,设置过期时间
  4. 商品的库存数据Redis sorted set保存
  5. 用户的地址信息Redis set保存
  6. 订单产生扣库存通过Redis制造分布式锁,库存同步扣除
  7. 订单产生后发货的数据,产生Redis list,通过消息队列处理
  8. 秒杀结束后,再把Redis数据和数据库进行同步
  • Redis支持的数据类型(必考)
  • zset跳表的数据结构(必考)

 以上这种链表加多级索引的结构,就是跳表。

跳表查询时间复杂度:

有n个结点的链表,假设每两个链表构建一个索引,那么:
第一级索引个数为:n/2;
第二级索引个数为:n/4;
···
第h级索引个数为:n/2^h;
现在假设最后一级索引的个数为2 ,则h +1 = logn,算上最底下的一层链表,那么这个跳表的高度H= logn。

当我们要查找跳表里的一个数时,参考图如下:

Search X.png

在图里,我们想查找x,在第k级,遍历到y结点,发现x大于y,但x小于y后面的结点z,所以先顺着y往下到第k-1级,发现y,z之间有三个节点,所以我们在k-1级索引中,遍历3个节点找到x,以此类推,在每一层需要通过3个节点找目标数,那么总的时间复杂度就为O(3*logn),因为3是常数,所以最后的时间复杂度为O(logn)。
这一结构相当于让跳表实现了二分查找,只是建立这么多的索引是否会浪费空间呢?我们来看一下跳表的空间复杂度。

还是回到刚刚的例子,我们可以发现,链表上的索引数目按第一层,第二层,···,倒数第二层,最后一层的顺序排列下来分别为:n/2,n/4,···,4,2,观察到了吗?就是一个等比数列,计算该跳表的空间复杂度,相当于给等比数列求和,高中数学都快忘完了,网上求得一个等比数列求和公式,放在这里:

Sn.png

顺着公式依次带入:a1=n/2,an= 2,q=1/2,求得Sn= n-2,所以空间复杂度为O(n),与此同时,我们顺便考虑一下每三个节点抽取一个索引的情况,还是依据刚刚的思路,发现Sn= n-1/2,空间复杂度将近缩减了一半。
总之,跳表就是空间换时间的那个思路,但如果链表中存储的对象很大时,其实索引占用的这些空间对整个来说是可以忽略不计的。

  • Redis的数据过期策略(必考)
  • 定时删除

    • 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
    • 优点:保证内存被尽快释放
    • 缺点:
      • 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
      • 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
      • 没人用
  • 惰性删除
    • 含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
    • 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
    • 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
  • 定期删除
    • 含义:每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
    • 优点:
      • 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
      • 定期删除过期key--处理"惰性删除"的缺点
    • 缺点
      • 在内存友好方面,不如"定时删除"
      • 在CPU时间友好方面,不如"惰性删除"
    • 难点
      • 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)
  • Redis的LRU过期策略的具体实现

缓存思想在硬件设计、软件设计中都有着十分重要的作用。而缓存空间是有限的,当缓存空间消耗殆尽时,我们就需要对缓存进行更新。常见的缓存更新算法主要有如下几种:先进先出策略 FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。

  1. 先进先出策略 FIFO:采用队列即可实现。

  2. 最少使用策略LFU:使用有序链表实现

  3. 最近最少使用策略LRU:可以使用单链表或者数组实现

    public class LRUCache<K, V> extends LinkedHashMap<K, V> {private final int CACHE_SIZE;// 这里就是传递进来最多能缓存多少数据public LRUCache(int cacheSize) {super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); // 这块就是设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾CACHE_SIZE = cacheSize;}@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return size() > CACHE_SIZE; // 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据}}
  • 如何解决Redis缓存雪崩,缓存穿透问题

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力,造成数据库后端故障,从而引起应用服务器雪崩。

缓存失效的几种情况:

1、缓存服务器挂了

2、高峰期缓存局部失效

3、热点缓存失效

解决方案:

1、避免缓存集中失效,不同的key设置不同的超时时间

2、增加互斥锁,控制数据库请求,重建缓存。

3、提高缓存的HA,如:redis集群。

雪崩的整体解决方案

一般情况对于服务依赖的保护主要有3种解决方案:

(1)熔断模式

这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。

重点监控的机器性能指标

  • cpu(Load) cpu使用率/负载
  • memory 内存
  • mysql监控长事务(这里与sql查询超时是紧密结合的,需要重点监控)
  • sql超时
  • 线程数等

总之,除了cpu、内存、线程数外,重点监控数据库端的长事务、sql超时等,绝大多数应用服务器发生的雪崩场景,都是来源于数据库端的性能瓶颈,从而先引起数据库端大量瓶颈,最终拖累应用服务器也发生雪崩,最后就是大面积的雪崩。

(2)隔离模式

这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。

例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。

(3)限流模式

上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。

熔断设计

在熔断的设计主要参考了hystrix的做法。其中最重要的是三个模块:熔断请求判断算法、熔断恢复机制、熔断报警

(1)熔断请求判断机制算法:使用无锁循环队列计数,每个熔断器默认维护10个bucket,每1秒一个bucket,每个blucket记录请求的成功、失败、超时、拒绝的状态,默认错误超过50%且10秒内超过20个请求进行中断拦截。

(2)熔断恢复:对于被熔断的请求,每隔5s允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复。

(3)熔断报警:对于熔断的请求打日志,异常请求超过某些设定则报警。

隔离设计

隔离的方式一般使用两种

(1)线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)

(2)信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)

超时机制设计

(1)超时分两种,一种是请求的等待超时,一种是请求运行超时。

(2)等待超时:在任务入队列时设置任务入队列时间,并判断队头的任务入队列时间是否大于超时时间,超过则丢弃任务。

(3)运行超时:直接可使用线程池提供的get方法。

缓存穿透是指查询一个一不存在的数据。例如:从缓存redis没有命中,需要从mysql数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决思路:

如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。

可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。

  • Redis的持久化机制(必考)

Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数 据恢复。理解掌握持久化机制对于Redis运维非常重要

1.RDB持久化

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发

1)触发机制

手动触发分别对应save和bgsave命令

·save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用

·bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短

2)自动触发RDB的持久

1)使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改 时,自动触发bgsave。

2)如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点,更多细节见6.3节介绍的复制原理。

3)执行debug reload命令重新加载Redis时,也会自动触发save操作。

4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。

bgsave是主流的触发RDB持久化方式

1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在bgsave命令直接返回。

2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒

3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。

4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后 对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。

5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb_*相关选项。

RDB文件的处理

保存:RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配 置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。

RDB的优缺点

RDB的优点:

·RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据 快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。

·Redis加载RDB恢复数据远远快于AOF的方式。

RDB的缺点:

·RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运 行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。

·RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。

2.AOF持久化

AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用 是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式

1)使用AOF

开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)

1)所有的写入命令会追加到aof_buf(缓冲区)中。

2)AOF缓冲区根据对应的策略向硬盘做同步操作。

AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如 果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡

3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

重写后的AOF文件为什么可以变小?有如下原因:

1)进程内已经超时的数据不再写入文件。

2)旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保

留最终数据的写入命令。

3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢 出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。

AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF 文件可以更快地被Redis加载

AOF重写过程可以手动触发和自动触发:

·手动触发:直接调用bgrewriteaof命令。

·自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机

·auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认 为64MB。

·auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。

自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage

其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。

4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。

流程说明:

1)AOF持久化开启且存在AOF文件时,优先加载AOF文件,打印如下日志:

* DB loaded from append only file: 5.841 seconds

2)AOF关闭或者AOF文件不存在时,加载RDB文件,打印如下日志:

* DB loaded from disk: 5.586 seconds

3)加载AOF/RDB文件成功后,Redis启动成功。

4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

重点:

1)Redis提供了两种持久化方式:RDB和AOF。

2)RDB使用一次性生成内存快照的方式,产生的文件紧凑压缩比更 高,因此读取RDB恢复速度更快。由于每次生成RDB开销较大,无法做到实

时持久化,一般用于数据冷备和复制传输。

3)save命令会阻塞主线程不建议使用,bgsave命令通过fork操作创建子 进程生成RDB避免阻塞。

4)AOF通过追加写命令到文件实现持久化,通过appendfsync参数可以 控制实时/秒级持久化。因为需要不断追加写命令,所以AOF文件体积逐渐变大,需要定期执行重写操作来降低文件体积。

5)AOF重写可以通过auto-aof-rewrite-min-size和auto-aof-rewritepercentage参数控制自动触发,也可以使用bgrewriteaof命令手动触发。

6)子进程执行期间使用copy-on-write机制与父进程共享内存,避免内 存消耗翻倍。AOF重写期间还需要维护重写缓冲区,保存新的写入命令避免数据丢失。

7)持久化阻塞主线程场景有:fork阻塞和AOF追加阻塞。fork阻塞时间 跟内存量和系统有关,AOF追加阻塞说明硬盘资源紧张。

8)单机下部署多个实例时,为了防止出现多个子进程执行重写操作, 建议做隔离控制,避免CPU和IO资源竞争。

  • Redis为什么是单线程的?

redis 核心就是 如果我的数据全都在内存里,我单线程的去操作 就是效率最高的,为什么呢,因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。redis 用 单个CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案

  • Redis为什么这么快?(必考)
  1. Redis是完全基于内存的数据库
  2. 处理网络请求使用的是单线程,避免了不必要的上下文切换和锁的竞争维护。
  3. 使用了I/O多路复用模型。

完全基于内存

为什么要用完全呢。因为像mysql这样的成传统关系型数据库是索引文件存储来内存,数据文件存储在硬盘的,那么硬盘的性能和瓶颈将会影响到数据库。

  1. 硬盘型数据库工作模式:
  2. 内存型数据库工作模式:


本身内存和硬盘的读写方式不相同导致了它们在读写性能上的巨大差异。

单线程

需要注意的是,这里的单线程指的是,Redis处理网络请求的时候只有一个线程,而不是整个Redis服务是单线程的。

单线程处理的好处

单线程处理也就是常说的串行处理,它和多线程处理各有优劣,不能一概而论。在Redis中由于是基于内存的数据库,它处理单个读写请求的速度非常快,若使用的是多线程在任务调度和线程维护上的消耗远大于处理请求的时间,这样会造成资源的浪费。

I/O多路复用模型


结合Redis:

  1. 在Redis中的I/O多路复用程序会监听多个客户端连接的Socket
  2. 每当有客户端通过Socket流向Redis发送请求进行操作时,I/O多路复用程序会将其放入一个队列中。
  3. 同时I/O多路复用程序会同步有序、每次传送一个任务给处理器处理。
  4. I/O多路复用程序会在上一个请求处理完毕后再继续分派下一个任务。(同步)

1、缩减键值对象

  缩减键(key)和值(value)的长度,

  • key长度:如在设计键时,在完整描述业务情况下,键值越短越好。

  • value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等,下图是JAVA常见序列化工具空间压缩对比。

2、共享对象池

  对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

3、字符串优化

4、编码优化

5、控制key的数量

  • Redis常见的性能问题有哪些?该如何解决?

1.Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。

2.Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。

3.Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

下面是我的一个实际项目的情况,大概情况是这样的:一个Master,4个Slave,没有Sharding机制,仅是读写分离,Master负责写入操作和AOF日志备份,AOF文件大概5G,Slave负责读操作,当Master调用BGREWRITEAOF时,Master和Slave负载会突然陡增,Master的写入请求基本上都不响应了,持续了大概5分钟,Slave的读请求过也半无法及时响应,上面的情况本来不会也不应该发生的,是因为以前Master的这个机器是Slave,在上面有一个shell定时任务在每天的上午10点调用BGREWRITEAOF重写AOF文件,后来由于Master机器down了,就把备份的这个Slave切成Master了,但是这个定时任务忘记删除了,就导致了上面悲剧情况的发生,原因还是找了几天才找到的。

将no-appendfsync-on-rewrite的配置设为yes可以缓解这个问题,设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入。最好是不开启Master的AOF备份功能。

4.Redis主从复制的性能问题,第一次Slave向Master同步的实现是:Slave向Master发出同步请求,Master先dump出rdb文件,然后将rdb文件全量传输给slave,然后Master把缓存的命令转发给Slave,初次同步完成。第二次以及以后的同步实现是:Master将变量的快照直接实时依次发送给各个Slave。不管什么原因导致Slave和Master断开重连都会重复以上过程。Redis的主从复制是建立在内存快照的持久化基础上,只要有Slave就一定会有内存快照发生。虽然Redis宣称主从复制无阻塞,但由于磁盘io的限制,如果Master快照文件比较大,那么dump会耗费比较长的时间,这个过程中Master可能无法响应请求,也就是说服务会中断,对于关键服务,这个后果也是很可怕的。

  • Redis的使用要注意什么?

一. key的设计

1. key命名规范:为了避免不必要的麻烦,我们要给系统定义一套key的设计规范。通俗点举个例子,我们在电脑上写好了一篇文章,需要保存起来,这时候我们会找个合适目录并且取个合适的文件名,以便后续要找它的时候,能想起它的名字并找到它,key的命名就好比给你要保存的文件命名和选目录,好的命名,能让你很容易想起它、找到它。大多缓存场景,是将需高频读取低频变更的数据从数据库中加载到redis,比较常用的key命名规范是:表名:主键名:主键值:存储列名,存储列名可根据下面第2点提到的粒度问题来自行定义。比如,缓存用户信息表user,set user:id:1:name 张三,缓存了用户id为1的用户的名字叫张三。

2. 粒度的把握:需要根据不同应用场景来设定。粒度越大,操作越简单,通用性好,但空间占用大,重建缓存需要的资源也越多;粒度越小,控制越复杂,通用性差,但空间占用小,重建缓存时需要的资源就越少。缓存粒度设计不当,可能会造成很多无用空间的浪费、网络带宽的浪费,也可能会造成代码通用性较差等情况,如何权衡缓存的粒度控制,需要根据实际业务提前设计好。

二. 缓存更新策略

缓存中的数据会和数据源有一段时间窗口的不一致,需要利用某些策略更新,下面介绍几种主要的缓存更新策略。

1. LRU/LFU/FIFO:剔除算法通常用于,当缓存使用量超过预设的最大值,如何对现有的数据进行剔除。

2. 超时剔除:给缓存数据设置过期时间,过期后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。

3. 主动更新:对数据一致性要求高,当源数据更新后,立即更新缓存,可通过MQ来实现触发。

三. 缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。解决缓存穿透,一般的方法有:

1. 缓存空对象。即当某个key从缓存和db都查不到时,为这个key缓存一个空对象,这样下次来就不会击穿到db。这样做,会有2个隐患,一个是,如果被攻击,则可能造成缓存大量的空对象,导致占用大量内存,可以设置比较短的过期时间来应对。另一个隐患是,如果db新增这个key的值,就会有一段时间不一致,当然这个也是数据一致性问题,通过主动更新的策略可避免。

2. 布隆过滤器。利用布隆过滤器保存在redis中存在的key,在击穿缓存时,先查一下布隆过滤器,如果不存在,则不查db,一定程度保护了db层。

四. 热点key重建问题

这个问题是指,某个key高并发读,如果刚好碰上到期更新,会导致多个线程重建key,导致db负载过大,应用雪崩。要解决这个隐患,可以给重建key设置互斥锁,确保同一时间只有一个线程重建缓存。另外,还有一个办法就是,不设置过期时间,然后在逻辑上去控制,即逻辑上记录一个过期时间,如果到了这个过期时间,缓存还能用,只是要通知缓存重建线程去重建。

五. 查缓存要注意

1. 使用连接池来管理连接。
2. 一个业务多次查询,考虑用Pipeline,将多次查询合并为一次,虽然命令会被执行多次,但节省IO,能有效提高响应速度。
3. 多次String查询,使用mget,将多次请求合并为一次,命令也会被合并为一次,能有效提高响应速度,对于Hash内多个Field查询,使用hmget,起到和mget同样的效果。
4. Redis是单线程执行的,如果一条命令执行时间较长,其他线程在此期间会被阻塞,所以在操作Redis时要注意操作指令的涉及的数据量,尽量降低单次操作的执行耗时,比如要慎用模糊匹配。

ZooKeeper

  • CAP定理

CAP定理指出,在一个分布式系统中,对于一致性、可用性、分区容错这三个特性,不可能同时满足,而是必须有所舍弃。我们设计分布式系统时,必须在三者之间(尤其是一致性和可用性之间)有所取舍和平衡。

  • ZAB协议

Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。
Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性

  • leader选举算法和流程

LeaderElection是Fast Paxos最简单的一种实现,每个Server启动以后都询问其它的Server它要投票给谁,收到所有Server回复以后,就计算出zxid最大的哪个Server,并将这个Server相关信息设置成下一次要投票的Server。该算法于Zookeeper 3.4以后的版本废弃。

选举算法流程如下:

  1. 选举线程首先向所有Server发起一次询问(包括自己);
  2. 选举线程收到回复后,验证是否是自己发起的询问(验证xid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
  3. 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
  4. 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得多数Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。

  • zookeeper 是什么?

ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。
分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能

  • zookeeper 有几种部署模式?

  • zookeeper 怎么保证主从节点的状态同步?

Mysql

  • 事务的基本要素

ACID,指数据库事务正确执行的四个基本要素的缩写.包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求.
原子性
  (Atomicity)
  事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
一致性
  (Consistency)
  事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
隔离性
  (Isolation)
  由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
持久性
  (Durability)
  事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。

  • 事务隔离级别(必考)
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

  • 如何解决事务的并发问题(脏读,幻读)(必考)

可串行化(SERIALIZABLE):

这是最高的隔离级别,可以解决上面提到的所有问题,因为他强制将所有的操作串行执行,这会导致并发性能急速下降,因此也不常用.

  • MVCC多版本并发控制(必考)

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

  • binlog,redolog,undolog都是什么,起什么作用

综述

binlog二进制日志是server层的,主要是左主从复制,时间点恢复使用
redo log重做日志是InnoDB存储引擎层的,用来保证事务安全
undo log回滚日志保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读

主备数据一致性

为了保证master和slave数据一致性,则binlog和redo log保持一致
否则当binlog写完未fsync,主库crash了,备库却执行了,数据会不一致
binlog是mysql内部实现二阶段提交协调者
为每个事务分配一个XID
一阶段
事务状态为prepare,redo log和undo log已经记录了对应的日志
二阶段

  1. binlog 完成write和fsync后,成功,事务一定提交了,否则回滚
  2. 发送commit,清除undo信息,刷redo,设置事务状态为completed

故障恢复是如何做的

当出现crash等问题,通过扫描binlog中所有的xid,告知innodb,innodb回滚其它事务

需要保证binlog写入和redo log事务提交顺序一致性

如果不一致,会导致数据不一致
BLGC(Binary Log Group Commit) 解决串行prepare_commit_mutex的问题
引入队列解决,

BLGC.png

区别

redo log在事务没有提交前,每一个修改操作都会记录变更后的数据,保存的是物理日志->数据
防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性
redo log只是先写入Innodb_log_buffer,定时fsync到磁盘

binlog只会在日志提交后一次性记录执行过的事务中的sql语句以及其反向sql(作为回滚用),保存的是逻辑日志->执行的sql语句
undo log事务开始之前,将当前版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性,保存的是逻辑日志->数据前一个版本

  1. 基于redo log直接恢复数据的效率 高于 基于binglog sql语句恢复
  2. binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redo log是循环使用。
  3. binlog可以作为恢复数据使用,主从复制搭建,redo log作为异常宕机或者介质故障后的数据恢复使用。
  • InnoDB的行锁/表锁

1.行锁和表锁

在mysql 的 InnoDB引擎支持行锁,与Oracle不同,mysql的行锁是通过索引加载的,即是行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,

行锁则无法实现,取而代之的是表锁。

表锁:不会出现死锁,发生锁冲突几率高,并发低。

行锁:会出现死锁,发生锁冲突几率低,并发高。

锁冲突:例如说事务A将某几行上锁后,事务B又对其上锁,锁不能共存否则会出现锁冲突。(但是共享锁可以共存,共享锁和排它锁不能共存,排它锁和排他锁也不可以)

死锁:例如说两个事务,事务A锁住了1~5行,同时事务B锁住了6~10行,此时事务A请求锁住6~10行,就会阻塞直到事务B施放6~10行的锁,而随后事务B又请求锁住1~5行,事务B也阻塞直到事务A释放1~5行的锁。死锁发生时,会产生Deadlock错误。

锁是对表操作的,所以自然锁住全表的表锁就不会出现死锁。

2.行锁的类型

行锁分 共享锁 和 排它锁。

共享锁又称:读锁。当一个事务对某几行上读锁时,允许其他事务对这几行进行读操作,但不允许其进行写操作,也不允许其他事务给这几行上排它锁,但允许上读锁。

排它锁又称:写锁。当一个事务对某几个上写锁时,不允许其他事务写,但允许读。更不允许其他事务给这几行上任何锁。包括写锁。

上共享锁的写法:lock in share mode

例如: select  math from zje where math>60 lock in share mode;

上排它锁的写法:for update

例如:select math from zje where math >60 for update;

3.行锁的实现

注意几点:

1.行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。

2.两个事务不能锁同一个索引,例如:

 
  1. 事务A先执行:

  2. select math from zje where math>60 for update;

  3. 事务B再执行:

  4. select math from zje where math<60 for update;

  5. 这样的话,事务B是会阻塞的。如果事务B把 math索引换成其他索引就不会阻塞,但注意,换成其他索引锁住的行不能和math索引锁住的行有重复。

3 .insert ,delete , update在事务中都会自动默认加上排它锁。

实现:

会话1:
begin;
select  math  from zje where math>60 for update;

会话2:

begin;
update zje set math=99 where math=68;
阻塞...........

会话相当与用户

如上,会话1先把zje表中math>60的行上排它锁。然后会话2试图把math=68的行进行修改,math=68处于math>60中,所以是已经被锁的,会话2进行操作时,

就会阻塞,等待会话1把锁释放。当commit时或者程序结束时,会释放锁。

  • myisam和innodb的区别,什么时候选择myisam

InnoDB:
支持事务处理等
不加锁读取
支持外键
支持行锁
不支持FULLTEXT类型的索引
不保存表的具体行数,扫描表来计算有多少行
DELETE 表时,是一行一行的删除
InnoDB 把数据和索引存放在表空间里面
跨平台可直接拷贝使用
InnoDB中必须包含AUTO_INCREMENT类型字段的索引
表格很难被压缩

MyISAM:
不支持事务,回滚将造成不完全回滚,不具有原子性
不支持外键
不支持外键
支持全文搜索
保存表的具体行数,不带where时,直接返回保存的行数
DELETE 表时,先drop表,然后重建表
MyISAM 表被存放在三个文件 。frm 文件存放表格定义。 数据文件是MYD (MYData) 。 索引文件是MYI (MYIndex)引伸
跨平台很难直接拷贝
MyISAM中可以使AUTO_INCREMENT类型字段建立联合索引
表格可以被压缩

选择:
因为MyISAM相对简单所以在效率上要优于InnoDB.如果系统读多,写少。对原子性要求低。那么MyISAM最好的选择。且MyISAM恢复速度快。可直接用备份覆盖恢复。
如果系统读少,写多的时候,尤其是并发写入高的时候。InnoDB就是首选了。
两种类型都有自己优缺点,选择那个完全要看自己的实际类弄。

  • 为什么选择B+树作为索引结构(必考)

磁盘读取数据是以数据块(block)(或者:页,page)为基本单位的,位于同一数据块中的所有数据都能被一次性全部读取出来。换句话说,从磁盘中读1B,与读1KB几乎一样快!因此,想要提升速度,应该利用外存批量访问的特点,在一些文章中,也称其为磁盘预读。系统之所以这么设计,是基于一个著名的局部性原理

当一个数据被用到时,其附近的数据也通常会马上被使用,程序运行期间所需要的数据通常比较集中

B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;

B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。

  • 索引B+树的叶子节点都可以存哪些东西(必考)

首先MYSQL默认InnoDB引擎,该引擎默认B+树;先说结论:B+树叶子结点存储的是主键KEY或者具体数据

1. 非叶子节点只存key不存数据

B+树中,非叶子节点是不存储数据的,只存储key,这样每个页能够存储更多的key,使得树更胖更矮,所以读取磁盘次数更少。假如B+树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据,一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

2. 所有数据都存在叶子节点

所有的数据都存在叶子节点,所以数据是按顺序存储的,使得范围查找、排序查找更加方便。

3. B+树的页之间用双向链表连接,数据间用单项链表链接

页之间有双向链表链接,使得扫描数据更加快捷。

聚集索引

上面说的B+树在叶子节点存储数据,这样的索引实际上就是聚集索引,像MySQL里会默认根据主键创建的索引就是聚集索引,根据主键构建一棵B+树,主键所对应的值直接存在叶子节点中。

根据主键查找的意义图如下所示:

select * from user where id>=18 and id <40
  • 1

非聚集索引

根据主键意外的字段创建的索引一般都是非聚集索引,非聚集索引也是用B+树构建的,他和聚集索引的唯一不同就是叶子节点中保存的值不是实际的值,而是主键值,找到主键值后再去聚集索引中查找。

举例:
有这样一张表,id是主键,我们在luckyNum字段上创建的非聚集索引。

select * from user where luckNum=33
  • 1

根据非聚集索引查找过程示意图如下:

  • 查询在什么时候不走(预期中的)索引(必考)

1.索引列参与计算,不走索引!

SELECT `username` FROM `t_user` WHERE age=20;-- 会使用索引
SELECT `username` FROM `t_user` WHERE age+10=30;-- 不会使用索引!!因为所有索引列参与了计算
SELECT `username` FROM `t_user` WHERE age=30-10;-- 会使用索引
  • 1
  • 2
  • 3

2.索引列使用了函数,不走索引!

-- 不会使用索引,因为使用了函数运算,原理与上面相同
SELECT username FROM t_user WHERE concat(username,'1') ='admin1';
-- 会使用索引
SELECT username FROM t_user WHERE username =concat('admin','1');
  • 1
  • 2
  • 3
  • 4

3.索引列使用了Like %XXX,不走索引!

like 模糊查询 前模糊或者 全模糊不走索引

select * from user where username like '%mysql测试'
  • 1

4.隐式转换——字符串列与数字直接比较,不走索引!

SELECT * FROM t_user WHERE `age`='23' -- 走索引
  • 1

5.尽量避免 OR 操作,只要有一个字段没有索引,改语句就不走索引,不走索引!

select * from t_user  where username  = 'mysql测试' or password ='123456'
  • 1

6.where id !=2 或者 where id <> 2,不走索引!

select * from t_user where username <> 'mysql测试'
  • 1

7. is null,is not null也无法使用索引,不走索引!

select * from t_user where username  is not null --is not null 不走索引
  • 1

8.复合索引a-b-c,a用到,b用不到,c用不到,ab有效,ba有效,a or b无效,ac有效,bc无效,abc有效 ,不走索引!

  • sql如何优化

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。    
    
2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:    
select id from t where num is null    
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:    
select id from t where num=0    
    
3.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。    
    
4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:    
select id from t where num=10 or num=20    
可以这样查询:    
select id from t where num=10    
union all    
select id from t where num=20    
    
5.in 和 not in 也要慎用,否则会导致全表扫描,如:    
select id from t where num in(1,2,3)    
对于连续的数值,能用 between 就不要用 in 了:    
select id from t where num between 1 and 3    
    
6.下面的查询也将导致全表扫描:    
select id from t where name like '%abc%'    
    
7.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:    
select id from t where num/2=100    
应改为:    
select id from t where num=100*2    
    
8.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:    
select id from t where substring(name,1,3)='abc'--name以abc开头的id    
应改为:    
select id from t where name like 'abc%'    
    
9.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。    
    
10.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。    
    
11.不要写一些没有意义的查询,如需要生成一个空表结构:    
select col1,col2 into #t from t where 1=0    
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:    
create table #t(...)    
    
12.很多时候用 exists 代替 in 是一个好的选择:    
select num from a where num in(select num from b)    
用下面的语句替换:    
select num from a where exists(select 1 from b where num=a.num)    
    
13.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。    
    
14.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,    
因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。    
一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。    
    
15.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。    
这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。    
    
16.尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间,    
其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。    
    
17.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。    
    
18.避免频繁创建和删除临时表,以减少系统表资源的消耗。

19.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。    
    
20.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,    
以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

21.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。    
    
22.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。    
    
23.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

24.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。
在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

25.尽量避免大事务操作,提高系统并发能力。

26.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

  • explain是如何解析sql的

一、explain简介

explain是MySQL一款查看SQL语句的执行计划的命令,使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。帮助分析你的查询语句或是表结构的性能瓶颈。

官方解释:
该EXPLAIN语句提供有关MySQL如何执行语句的信息。
EXPLAIN为SELECT语句中使用的每个表返回一行信息 。它按照MySQL在处理语句时读取它们的顺序列出了输出中的表。这意味着MySQL从第一个表中读取一行,然后在第二个表中然后在第三个表中找到匹配的行,依此类推。处理完所有表后,MySQL将通过表列表输出选定的列和回溯,直到找到一个表,其中存在更多匹配的行。从该表中读取下一行,然后继续下一个表。

详细资料请参加官方文档

二、用法

EXPLAIN [sql语句]

示例:

mysql> explain select count(*) from tb_orders;
+----+-------------+-----------+------------+-------+---------------+--------------------------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key                      | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+--------------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_orders | NULL       | index | NULL          | tb_orders_trade_id_index | 258     | NULL |  873 |   100.00 | Using index |
+----+-------------+-----------+------------+-------+---------------+--------------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

各个字段的含义如下:

字段名称 JSON名称 含义
id select_id 该SELECT标识符
select_type 没有 该SELECT类型
table table_name 输出行表
partitions partitions 匹配的分区
type access_type 联接类型
possible_keys possible_keys 可能的索引选择
key key 实际选择的索引
key_len key_length 所选键的长度
ref ref 与索引比较的列
rows rows 估计要检查的行
filtered filtered 按表条件过滤的行百分比
Extra 没有 附加信息

三、各个字段的含义

  1. id
    id是执行顺序的标识。

    相同id的执行顺序自上而下。
    不同id时候,id越大,越先执行。

  2. select_type
    查询的类型,可以是下表中显示的任何类型。

    select_type 含义
    SIMPLE 简单SELECT(不使用 UNION或子查询)
    PRIMARY 最外层 SELECT
    UNION SELECT陈述中的第二个或之后的陈述 UNION
    DEPENDENT UNION UNION中的第二个或更高版本的SELECT语句,取决于外部查询
    UNION RESULT UNION的结果。
    SUBQUERY 首先SELECT在子查询
    DEPENDENT SUBQUERY 首先SELECT在子查询中,取决于外部查询
    DERIVED 没有 派生表
    DEPENDENT DERIVED 派生表依赖于另一个表
    MATERIALIZED 物化子查询
    UNCACHEABLE SUBQUERY 子查询,其结果无法缓存,必须针对外部查询的每一行重新进行评估
    UNCACHEABLE UNION UNION 属于不可缓存子查询的中的第二个或更高版本的选择(请参阅参考资料 UNCACHEABLE SUBQUERY)
  3. table
    输出行所引用的表的名称。或者连表名称。

  4. partitions
    查询将从中匹配记录的分区。

  5. type
    联接类型。
    这是重要的列,显示连接使用了何种类型,从最好到最差的连接类型一次是:
    const、eq_reg、ref、range、index和 ALL。
    详情参见 EXPLAIN 连接类型。

    类型 说明
    system 该表只有一行(=系统表)。这是const联接类型的特例 。
    const 该表最多具有一个匹配行,该行在查询开始时读取。因为只有一行,所以优化器的其余部分可以将这一行中列的值视为常量。 const表非常快,因为它们只能读取一次。
    eq_ref 对于先前表中的每行组合,从此表中读取一行。除了 system和 const类型,这是最好的联接类型。
    ref 对于先前表中的每个行组合,将从该表中读取具有匹配索引值的所有行。
    fulltext 使用FULLTEXT 索引执行联接。
    ref_or_null 这种连接类型类似于 ref,但是除了MySQL会额外搜索包含NULL值的行。
    index_merge 此联接类型指示使用索引合并优化。
    unique_subquery 此类型替换某些eq_ref的IN子查询
    index_subquery 此连接类型类似于 unique_subquery。
    range 使用索引选择行,仅检索给定范围内的行。
    ALL 对来自先前表的行的每个组合进行全表扫描。
  6. possible_keys
    显示可能应用在这张表中的索引。如果是空,没有可能的索引。

  7. key
    实际应用到这张表的索引,如果是null,则没有索引使用。

  8. key_len
    使用的索引的长度,在不损失精度的情况下,长度越短越好。

  9. ref
    显示索引的拿一列被使用了。如果可能的话,是一个常数。

  10. row
    MySQL认为必须检查的用来返回请求数据的行数。

  • order by原理
  1. 利用索引的有序性获取有序数据
  2. 利用内存/磁盘文件排序获取结果
    1) 双路排序:是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行指针信息,然后在sort buffer 中进行排序。
    2)单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序。

优化方式

  1. 给order by 字段增加索引,orderby的字段必须在最前面设置
    接下来给来说一下orderby什么时候使用索引和什么时候不使用索引的情况
    1.使用索引情况

    1. SELECT id from form_entity order by name(给name建立索引)
      当select 的字段包含在索引中时,能利用到索引排序功能,进行覆盖索引扫描,使用select * 则不能利用覆盖索引扫描且由于where语句没有具体条件MySQL选择了全表扫描且进行了排序操作。
    2. SELECT * from form_entity where name =’123’ and category_id=’1’ order by comment desc(name,comment建立复合索引)
      组合索引中的一部分做等值查询 ,另一部分作为排序字段
    3. SELECT comment from form_entity group by name,comment order by name (name,comment建立复合索引)

    2.不使用索引情况

    1. SELECT id,comment from form_entity order by name(给name建立索引,comment没有索引)
    2. SELECT * from form_entity order by name(给name设置索引)
    3. SELECT * from form_entity order by name desc,comment desc(name,comment建立复合索引)
    4. SELECT * from form_entity where comment =’123’ order by name desc(name,comment建立复合索引)
    5. SELECT name from form_entity where category_id =’123’ order by name desc,comment desc(name,comment建立复合索引,category_id独立索引)
      当查询条件使用了与order by不同的索引,但排序字段是另一个联合索引的非连续部分
    6. SELECT comment from form_entity group by name order by name ,comment(name,comment建立复合索引)
    7. 返回数据量过大也会不使用索引
    8. 排序非驱动表不会走索引
    9. order by 字段使用了表达式
  2. 去掉不必要的返回字段

  3. 增大 sort_buffer_size 参数设置

JVM

  • 运行时数据区域(内存模型)(必考)

Java运行时数据区域

众所周知,Java 虚拟机有自动内存管理机制,如果出现内存泄漏和溢出方面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。

下图是 JDK8 之后的 JVM 内存布局。

Java虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在活动线程中,只有位千栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

1. 局部变量表

局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算完成的局部变呈写回局部变量表的存储空间内。

虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

2. 操作栈

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往
栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操
作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。

i++ 和 ++i 的区别:

  1. i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
  2. ++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。

之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

3. 动态链接

每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。

4.方法返回地址

方法执行时有两种退出情况:

  1. 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
  2. 异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  1. 返回值压入上层调用栈帧。
  2. 异常信息抛给能够处理的栈帧。
  3. PC计数器指向方法调用后的下一条指令。
  • 垃圾回收机制(必考)

一、垃圾回收机制的意义

  Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

  ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。

二、垃圾回收机制中的算法

  Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。  

1.引用计数法(Reference Counting Collector)

1.1算法分析 

  引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

1.2优缺点

优点:

  引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:

  无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

1.3引用计数算法无法解决循环引用问题,例如:

public class Main {public static void main(String[] args) {MyObject object1 = new MyObject();MyObject object2 = new MyObject();object1.object = object2;object2.object = object1;object1 = null;object2 = null;}
}

  最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)

2.1根搜索算法

  根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

java中可作为GC Root的对象有

  1.虚拟机栈中引用的对象(本地变量表)

  2.方法区中静态属性引用的对象

  3. 方法区中常量引用的对象

  4.本地方法栈中引用的对象(Native对象)

2.2tracing算法的示意图

  

2.3标记-清除算法分析

  标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

3.compacting算法 或 标记-整理算法

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

4.copying算法(Compacting Collector)

  

  该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

5.generation算法(Generational Collector)

  分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代(Young Generation)

  1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

  2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

  3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

  4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代(Old Generation)

  1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

  2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)

  用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

三.GC(垃圾收集器)

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

Serial收集器(复制算法)

  新生代单线程收集器,标记和清理都是单线程,优点是简单高效。

Serial Old收集器(标记-整理算法)

  老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(停止-复制算法) 

  新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(停止-复制算法)

  并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

Parallel Old收集器(停止-复制算法)

  Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

  高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

四、GC的执行机制

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

  1.年老代(Tenured)被写满

  2.持久代(Perm)被写满

  3.System.gc()被显示调用

  4.上一次GC之后Heap的各域分配策略动态变化

五、Java有了GC同样会出现内存泄露问题

1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

Static Vector v = new Vector();
for (int i = 1; i<100; i++)
{ Object o = new Object(); v.add(o); o = null;
}

  在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

  • Minor GC和Full GC触发条件

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法去空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

  • GC中Stop the world(STW)

Stop一the一World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。.

举例:

➢可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。.

停顿的原因

分析工作必须在一个能确保一致性的快照 中进行

一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上V

如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

示例代码:

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。

STW事件和采用哪款GC无关,所有的GC都有这个事件。

哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

开发中采用System.gc();会导致Stop一the一world的发生。

public class StopTheWorldDemo {public static class WorkThread extends Thread {List<byte[]> list = new ArrayList<byte[]>();public void run() {try {while (true) {for(int i = 0;i < 1000;i++){byte[] buffer = new byte[1024];list.add(buffer);}if(list.size() > 10000){list.clear();System.gc();//会触发full gc,进而会出现STW事件}}} catch (Exception ex) {ex.printStackTrace();}}}public static class PrintThread extends Thread {public final long startTime = System.currentTimeMillis();public void run() {try {while (true) {// 每秒打印时间信息long t = System.currentTimeMillis() - startTime;System.out.println(t / 1000 + "." + t % 1000);Thread.sleep(1000);}} catch (Exception ex) {ex.printStackTrace();}}}public static void main(String[] args) {WorkThread w = new WorkThread();PrintThread p = new PrintThread();w.start();p.start();}
}

W线程当中的GC触发了STW,进而干扰了P线程有规律性打印。打印变得杂乱无章

打印输出:

  • 各垃圾回收器的特点及区别

每种垃圾回收器之间不是独立操作的,下图表示垃圾回收器之间有连线表示,可以协作使用。

垃圾回收器


二、新生代垃圾收集器

==1、Serial收集器==

  • 概述:Serial是一类用于新生代的单线程收集器,采用++复制算法++进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停。其执行过程如下图所示

    serial垃圾收集器执行过程

从上图可知当应用程序进行到一个安全的节点的时候,所有的线程全都暂停,等到GC完成后,应用程序线程继续执行。这就像是你一边扫地,旁边要是有人一边嗑瓜子,那你这要一直扫下去的节奏,只能先让他别吃了,然后你才能干活。

  • 优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
  • 缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
  • 适用场景:Client 模式(桌面应用);单核服务器。
  • 参数: 可以使用命令如下开启Serial作为新生代收集器
    -XX:+UserSerialGC #选择Serial作为新生代垃圾收集器
    

==2、ParNew收集器==

  • 概述:parNew收集器其实就是Serial的一个多线程版本,其在单核cpu上的表现并不会比Serail收集器更好,在多核机器上,其默认开启的收集线程数与cpu数量相等。可以通过如下命令进行修改

    -XX:ParallelGCThreads #设置JVM垃圾收集的线程数
    

如下是ParNew收集器和Serial Old 收集器结合进行垃圾收集的示意图.

ParNew与serialOld

当用户线程都执行到++安全点++时,所有线程暂停执行,采用复制算法进行垃圾收集工作,完成之后,用户线程继续开始执行。

  • 优点:随着cpu的有效利用,对于GC时系统资源的有效利用有好处。
  • 缺点:和Serial是一样的。
  • 适用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器。因为CMS收集器只能与serial或者parNew联合使用,在当下多核系统环境下,首选的是parNew与CMS配合。ParNew收集器也是使用CMS收集器后默认的新生代收集器。也可以使用如下命令进行强制指定。
-XX:UseParNewGC #新生代采用ParNew收集器

==3、Parallel Scavenge收集器==

  • 概述:Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。++与ParNew的不同之处在于 Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。++ 所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
    例如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99% 。比如下面两个场景,垃圾收集器每 100 秒收集一次,每次停顿 10 秒,和垃圾收集器每 50 秒收集一次,每次停顿时间 7 秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU 总体利用率变低了。其与Parallel Old收集器运行示意图如下

    Parallel.

  • 优点: 追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。

  • 缺点: ""

  • 适用场景:注重吞吐量高效利用CPU,需要高效运算,且不需要太多交互。

  • 参数:

    • ==-XX:MaxGCPauseMilis==。 控制最大垃圾收集停顿时间,参数值是一个大于0的毫秒数,收集器尽可能保证回收花费时间不超过设定值。但将这个值调小,并不一定会使系统垃圾回收速度更快,GC停顿时间是以牺牲吞吐量和新生代空间换来的。
    • ==-XX:GCTimeRadio==。设置吞吐量大小,参数值是一个(0,100)两侧均为开区间的整数。也是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。若把参数设置为19,则允许的最大GC时间就占总时间的5%(1/(1+19))。默认值是99,即允许最大1%的垃圾收集时间。
    • ==-XX:+UserAdaptiveSizePolicy==。这是一个开关函数,当打开这个函数,就不需要手动指定新生代的大小,Eden与Survivor区的比例(-XX:SurvivorRatio,默认是8:1:1),晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等参数。JVM会动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略.

三、老年代垃圾收集器

1、Serial Old 收集器

  • 概念:Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。下图是Serial收集器与Serial Old收集器的运行示意图。

    Serial/Serial Old收集器运行示意图

  • 适用场景:Client模式;单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用

2、Parallel Old收集器

  • 概念:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,可以充分利用多核CPU的计算能力。下图是两种收集器合作的运行示意图

    Parallel Scavenge/Parallel Old 收集器运行示意图

  • 适用场景:注重吞吐量与CPU资源敏感的场合,与Parallel Scavenge 收集器搭配使用,jdk7和jdk8默认使用该收集器作为老年代收集器。使用参数进行指定

-XX:+UserParallelOldGC

3、CMS收集器

  • 概念:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤

    • 初始标记,标记GC Roots 能够直接关联到达对象
    • 并发标记,进行GC Roots Tracing 的过程
    • 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
    • 并发清除,用标记清除算法清除对象。
      其中初始标记和重新标记这两个步骤仍然需要"stop the world"。耗时最长的并发标记与并发清除过程收集器线程都可以与用户线程一起工作,总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。下图是CMS运行示意图。

      CMS收集器运行示意图

  • 优点:并发收集,低停顿

  • 缺点:

    • CMS收集器对CPU资源非常敏感,CMS默认启动对回收线程数(CPU数量+3)/4,当CPU数量在4个以上时,并发回收时垃圾收集线程不少于25%,并随着CPU数量的增加而下降,但当CPU数量不足4个时,对用户影响较大。
    • CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致一次FullGC的产生。这时会地洞后备预案,临时用SerialOld来重新进行老年代的垃圾收集。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然还会有新的垃圾产生,这部分垃圾出现在标记过程之后,CMS无法在当次处理掉,只能等到下一次GC,这部分垃圾就是浮动垃圾。同时也由于在垃圾收集阶段用户线程还需要运行,那也就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他老年代几乎完全填满再进行收集。可以通过参数-XX:CMSInitiatingOccupancyFraction修改CMS触发的百分比。
    • 因为CMS采用的是标记清除算法,因此垃圾回收后会产生空间碎片。通过参数可以进行优化。
    -XX:UserCMSCompactAtFullCollection #开启碎片整理(默认是开的)-XX:CMSFullGCsBeforeCompaction #执行多少次不压缩的Full GC之后,跟着来一次压缩的Full GC
    
  • 适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用参数-XX:+UserConMarkSweepGC来选择CMS作为老年代回收器。


四、新生代和老年代垃圾收集器

G1收集器

  • 概念: G1收集器是一款面向服务端应用的垃圾收集器,目前是JDK9的默认垃圾收集器。与其他收集器相比,G1具有如下特点。

    • 并行与并发。G1能充分利用多CPU,多核环境下的硬件优势。
    • 分代收集。能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
    • 空间整合。G1从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的,因此G1运行期间不会产生空间碎片。
    • 可预测的停顿。G1能建立可预测的时间停顿模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器将这个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但两者之间不是物理隔离的。他们都是一部分Region的集合。下图是Java堆的划分示意图。

G1Java堆分区

每一个方块就是一个区域,每个区域可能是 Eden、Survivor、老年代,每种区域的数量也不一定。JVM 启动时会自动设置每个区域的大小(1M ~ 32M,必须是 2 的次幂),最多可以设置 2048 个区域(即支持的最大堆内存为 32M*2048 = 64G),假如设置 -Xmx8g -Xms8g,则每个区域大小为 8g/2048=4M。

G1收集器可以有计划地避免在整个Java堆全区域的垃圾收集。G1可以跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,收集加载最大的region,这种方式保证了有限时间内可以获取尽可能多高的收集效率。

为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个 Remembered Set 来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。

下图是G收集器运行示意图。从图中可知G1收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和 CMS 收集器前几步的收集过程很相似:

G1垃圾收集

  • 过程:

    • 初始标记。标记出GC Roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
    • 并发标记。从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
    • 最终标记。修正在并发标记阶段引用户程序执行而产生变动的标记记录。
    • 筛选回收。选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First ,第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。
  • 适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器

五、常见参数列表

参数 描述
UserSerialGC 采用Serial+ Serial Old的收集器组合进行垃圾回收
UserParNewGC 使用ParNew+Serial Old组合
UseConMarkSweepGC 使用ParNew+CMS+SerialOld组合进行垃圾回收。SerialOld是在Concurrent Mode Failure失败后的后备收集器使用
UseParallelGC 虚拟机运行在Server模式下的默认值。使用Parallel Scavenge +Serial Old的收集器组合进行垃圾回收 (jdk8用的就是这个,但是老年代用的是Parallel Old)
UseParallelOldGC 使用Parallel Scavenge + Parallel Old组合进行垃圾回收
SurvivorRatio 新生代中Eden区与Survior区域的容量比值,默认为8,代表Eden:Survior=8:1
PretenureSizeThreshold 直接晋升到老年代的对象的大小
MaxTenuringThreshold 晋升到老年代对象的年龄,超过这个数值进入老年代
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的 所有对象存活的极端情况
ParallelGCThreads 并行GC时进行内存回收的线程数
GCTimeRatio GC时间占总时间的比率,默认值99%,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillis GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction 设置CMS在老年代空间被使用多少后触发垃圾回收,默认是92%,仅在使用CMS收集器时生效
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存整理。仅在使用CMS收集器时生效
CMSFullGCsBeforeCompaction 设置CMS收集器进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS时生效
PrintGCDetails 查看程序运行时的GC细节
  • 双亲委派模型

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

  • JDBC和双亲委派模型关系

SPI机制简介

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。 有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。JDBC SPI mysql的实现如下所示。

SPI机制带来的问题

Java 提供了很多服务SPI,允许第三方为这些接口提供实现。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现则是由各供应商来完成。终端只需要将所需的实现作为 Java 应用所依赖的 jar 包包含进类路径(CLASSPATH)就可以了。问题在于SPI接口中的代码经常需要加载具体的实现类:SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;而SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。


二、线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。

线程上下文类加载器从根本解决了一般应用不能违背双亲委派模式的问题,使得java类加载体系显得更灵活。 上面所提到的问题正是线程上下文类加载器的拿手好菜。如果不做任何的设置,Java应用的线程上下文类加载器默认就是系统类加载器。因此,在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

// Now create the class loader to use to launch the application
try {  loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {  throw new InternalError(
"Could not create application class loader" );
}  // Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

三. 违背双亲委派案例之JDBC

1、JDBC驱动注册的常用几种方式

Java数据库连接(Java Database Connectivity,简称 JDBC)是Java语言用来规范客户端程序如何访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。 JDBC驱动包就是上述接口的实现,由数据库厂商开发,是java和具体数据库之间的连接桥梁。每一种数据库对应一款驱动jar,甚至每一个版本的数据库都有自己对应版本的驱动。我们知道,JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己,所以在与数据库交互前必须完成驱动注册,那么先来看看平时我们是如何注册JDBC驱动的。

方式一:Class.forName(“com.mysql.jdbc.Driver”)

     try {// 注册Class.forName(driver);conn = (Connection)DriverManager.getConnection(url, user, passwd);} catch (Exception e) {System.out.println(e);}

使用该方式注册的关键在于 Class.forName(driver);,这句话的作用是加载并初始化指定驱动。mysql jdbc正是在Driver初始化的时候完成注册:

package com.mysql.jdbc;import com.mysql.jdbc.NonRegisteringDriver;
import java.sql.DriverManager;
import java.sql.SQLException;public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}// 类初始化时完成驱动注册static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can\'t register driver!");}}
}

方式二:System.setProperty(“jdbc.drivers”,“com.mysql.jdbc.Driver”)

 try {//Class.forName(driver);System.setProperty("jdbc.drivers", driver);conn = (Connection)DriverManager.getConnection(url, user, passwd);} catch (Exception e) {System.out.println(e);}
  • 这种方式是通过系统的属性设置注册驱动,最终还是通过系统类加载器完成。
 // DriverManager 中的静态代码块static {loadInitialDrivers();println("JDBC DriverManager initialized");}// 初始化 DriverManagerprivate static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader// exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();/* Load these drivers, so that they can be instantiated.* It may be the case that the driver class may not be there* i.e. there may be a packaged driver with the service class* as implementation of java.sql.Driver but the actual class* may be missing. In that case a java.util.ServiceConfigurationError* will be thrown at runtime by the VM trying to locate* and load the service.** Adding a try catch block to catch those runtime errors* if driver not available in classpath but it's* packaged as service and that service is there in classpath.*/try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {// 注册驱动,底层实现还是和方式一一样的套路println("DriverManager.Initialize: loading " + aDriver);Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}

方式三:SPI服务加载机制注册驱动

 try {// Class.forName(driver);conn = (Connection)DriverManager.getConnection(url, user, passwd);} catch (Exception e) {System.out.println(e);}

各位可以发现,这种方式与第一种方式唯一的区别就是经常写的Class.forName被注释掉了,但程序依然可以正常运行,这是为什么呢?这是因为,从JDK1.6开始,Oracle就修改了加载JDBC驱动的方式,即JDBC4.0。在JDBC 4.0中,我们不必再显式使用Class.forName()方法明确加载JDBC驱动。当调用getConnection方法时,DriverManager会尝试自动设置合适的驱动程序。前提是,只要mysql的jar包在类路径中。

那到底是在哪一步自动注册了mysql driver的呢?我们接下来进一步分析。


2、SPI服务加载机制注册驱动原理分析

重点就在DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。DriverManager的静态代码块:

static {loadInitialDrivers();println("JDBC DriverManager initialized");
}
  • 初始化方法loadInitialDrivers()的代码我们其实已经见过了,第二种和第三种的驱动注册逻辑都在这里面:
private static void loadInitialDrivers() {String drivers;try {// 先读取系统属性 : 对应上面第二种驱动注册方式drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 通过SPI加载驱动类AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});// 加载系统属性中的驱动类 : 对应上面第二种驱动注册方式if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);// 使用AppClassloader加载Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}
}

从上面可以看出,JDBC中的DriverManager加载Driver的步骤顺序依次是:

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;
  2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。

我们现在只讨论SPI方式的实现,来看刚才的代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}
} catch(Throwable t) {
// Do nothing
}
  • 注意driversIterator.next()这条语句完成了驱动的注册工作,如下所示:
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {// 加载实现类,注意还没有初始化;以JDBC为例,此时还没有完成驱动注册c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}// service就是SPI,以JDBC为例,service就是Java Driver接口;此处判断c是否为Driver的实现if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn  + " not a subtype");}try {// c是spi的实现,c.newInstance()会触发类的初始化动作,以JDBC为例,这一操作会完成驱动注册S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}
  • 好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。因为Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader(启动类加载器只能加载java核心类库)。这时候只能使用线程上下文类加载器了:把自己加载不了的类加载到线程上下文类加载器中(通过Thread.currentThread()获取),而线程上下文类加载器默认是使用系统类加载器AppClassLoader。

回头再看ServiceLoader.load(Class)的代码,的确如此:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

到这儿差不多把SPI机制解释清楚了。直白一点说就是:我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载。但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作就是你的事了。


四. Tomcat与Spring的类加载器案例

接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题(部分类容来自于书中原文)。

Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用;
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见;
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见;
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示:

灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

Spring加载问题

Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

解答

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程类加载器来加载类,而线程类加载器默认设置为了WebAppClassLoader。也就是说,哪个WebApp应用调用了Spring,Spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {try {// 创建WebApplicationContextif (this.context == null) {this.context = createWebApplicationContext(servletContext);}// 将其保存到该webapp的servletContext中      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);// 获取线程上下文类加载器,默认为WebAppClassLoaderClassLoader ccl = Thread.currentThread().getContextClassLoader();// 如果spring的jar包放在每个webapp自己的目录中// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoaderif (ccl == ContextLoader.class.getClassLoader()) {currentContext = this.context;}else if (ccl != null) {// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出currentContextPerThread.put(ccl, this.context);}return this.context;}catch (RuntimeException ex) {logger.error("Context initialization failed", ex);throw ex;}catch (Error err) {logger.error("Context initialization failed", err);throw err;}
}

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用线程上下文类加载器来解决所有可能面临的情况。


五. 总结

通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

  • 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

  • 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

  • JVM 中一次完整的 GC 流程是什么样子的,对象如何晋升到老年代,说说你知道的几种主要的 JVM 参数
  1. 对象太大,Eden放不下
  2. 存放存活对象的Survivor区太小,不足以存下存活对象
  3. 经历超过默认15次gc或者设定的
  4. Survivor空间中相同年龄的所有对象综合大于等于Survivor空间的一半,那么这些对象就会直接进入到老年代中

Spring

  • Spring的IOC/AOP的实现(必考)

Spring AOP

ProxyFacotryBean是FacotryBean的一种实现,FacotryBean要产生bean都要重写getObject方法,而ProxyFacotryBean这里的这个getObject正是为代理做了准备并返回代理对象。首先用initializeAdvisorChain(第一次去取代理对象时初始化一遍)初始化Advisor链后对于singleton和prototype进行区分生成对应的proxy。

  • 动态代理的实现方式(必考)

AOP用到了两种动态代理来实现织入功能:

  • jdk动态代理
  • cglib动态代理
  • 比较:
  • jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。
  • jdk动态代理的应用前提是目标类必须基于统一的接口。因此,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

基于接口代理(jdk)

  • 类:java.lang.reflect.Proxy(通过该类动态生成代理类)
  • 代理类实现接口:InvocationHandler
  • jdk代理只能基于接口动态代理(因为生成的proxy class中,继承了Proxy类,实现了需要代理的接口,而Java是单继承,多实现的处理方式)
  • 动态代理类
  • 实现接口的方法
  • 引用目标方法
  • 构造器注入目标方法
  • 通过反射调用目标方法
    public class JdkProxySubject implements InvocationHandler{//引入要代理的真实对象private RealSubject realSubject;//用构造器注入目标方法,给我们要代理的真实对象赋初值public JdkProxySubject(RealSubject realSubject){this.realSubJect=realSubject;}//实现接口的方法@Overridepublic Object invoke(Object proxy,Method method,Object[] args)throws Throwable{System.out.println("before");Object result = null;try{//调用目标方法//利用反射构造目标对象//    当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用result=method.invoke(realSubject,args);}catch(Exception e){System.out.println("ex:"+e.getMessage());throw e; }finally{System.out.println("after");}return result;}}

客户端调用(使用Proxy)

  public class Client{public static void main(String[] args){//使用Proxy构造对象//参数//java泛型需要转换一下/* 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数* 第一个参数 getClassLoader() ,我们这里使用Client这个类的ClassLoader对象来加载我们的代理对象* 第二个参数表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了* 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上* /Subject subject = (Subject) java.lang.reflect.Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Subject.class},new JdkProxySubject(new RralSubject()));//调用方法subject.test;}}

原理解析

过程

调用Proxy.newProxyInstance生成代理类的实现类:

  • 调用getProxyClass0寻找或生成指定代理类
    (从缓存中取,如果没有,就生成一个放在缓存中 : 通过ProxyClassFactory生成)
  • 缓存调用ProxyClassFactory生成代理类,proxy class的生成最终调用ProxyClassFactory的apply方法.
  • ProxyGenerator.generateProxyClass使用生成的代理类的名称,接口,访问标志生成proxyClassFile字节码
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);try {return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);} catch (ClassFormatError e) {throw new IllegalArgumentException(e.toString());}
  • 生成字节码之后利用反射生成实例
    Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。

InvocationHandler接口:

每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
我们看到这个方法一共接受三个参数,那么这三个参数分别代表什么呢?

-proxy:  指代我们所代理的那个真实对象
-method:  指代的是我们所要调用真实对象的某个方法的Method对象
-args:  指代的是调用真实对象某个方法时接受的参数

Proxy类

Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:
这个方法的作用就是得到一个动态的代理对象,其接收三个参数,我们来看看这三个参数所代表的含义:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
  • loader:  一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载

  • interfaces:  一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法

  • h:  一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

基于继承代理(cglib)

  • 代理类
 public class CglibMethodInterceptor implements MethodInterceptor{//主要的方法拦截类,它是Callback接口的子接口,需要用户实现@Overridepublic Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy )throws Throwable{System.out.println("before cgplib");Object result = null;try{//利用反射创建代理对象 result = proxy.invokeSuper(obj,args);}catch(Exception e){System.out.println("ex:"+e.getMessage());throw e;}finally{System.out.println("after cglib");}return result;}}
  • 调用类
 public class Client{public static void main(String[] args){// 主要的增强类Enhancer enhancer=new Enhancer;//  目标类 , 设置父类,被增强的类enhancer.setSuperclass(RealSubject.class);// 回调对象enhancer.setCallback(new CglibMethodInterceptor());//生成代理类对象,用cglibProxy来增强RealSubjectSubject subject=enhancer.create();subject.test();}
}

原理解析

Cglib是一个优秀的动态代理框架,它的底层使用ASM在内存中动态的生成被代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理:
CGLIB的核心类:

  • net.sf.cglib.proxy.Enhancer – 主要的增强类
  • net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
  • net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用,如使用:
  • Object o = methodProxy.invokeSuper(proxy, args);//虽然第一个参数是被代理对象,也不会出现死循环的问题。

-net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调(callback)类型,它经常被基于代理的AOP用来实现拦截(intercept)方法的调用。这个接口只定义了一个方法

public Object intercept(Object object, java.lang.reflect.Method method,
Object[] args, MethodProxy proxy) throws Throwable;
  • 第一个参数是代理对像,第二和第三个参数分别是拦截的方法和方法的参数。原来的方法可能通过使用java.lang.reflect.Method对象的一般反射调用,或者使用 net.sf.cglib.proxy.MethodProxy对象调用。net.sf.cglib.proxy.MethodProxy通常被首选使用,因为它更快。

Java字节码生成开源框架–ASM:

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。

  • Spring如何解决循环依赖(三级缓存)(必考)

1.singletonObjects:存放初始化好的bean
2.earlySingletonObjects:存放了刚实例化好的,但是还未配置属性和初始化的bean,我们在获取该bean的时候会调用beanPostProcessor的getEarlyReference进行一些提前获取bean的必要操作
3.singletonFactories:存放我刚实例化的bean,通过ObjectFactory,可以让如果有提前需要bean的需要可以调用该
objectfactory 其会将刚实例化好的bean经过beanPostProcessor的getEarlyReference处理进行返回

  • Spring的后置处理器
  • Spring的@Transactional如何实现的(必考)
  • Spring的事务传播级别
  • BeanFactory和ApplicationContext的联系和区别

其他

  • 高并发系统的限流如何实现
  • 高并发秒杀系统的设计
  • 负载均衡如何设计

操作系统篇

  • 进程和线程的区别
  • 进程同步的几种方式
  • 线程间同步的方式
  • 什么是缓冲区溢出。有什么危害,其原因是什么
  • 进程中有哪几种状态
  • 分页和分段有什么区别

多线程篇

  • 多线程的几种实现方式,什么是线程安全
  • volatile 的原理,作用,能代替锁吗?
  • sleep 和 wait 的区别
  • sleep(0)的意义
  • Lock 和 Synchronized 的区别
  • synchronized 的原理是什么,一般用在什么地方(比如加载静态方法和非静态方法的区别)

JAVA知识梳理(日常整理)相关推荐

  1. 【JAVA知识梳理】集合总结!

    活动地址:CSDN21天学习挑战赛 本文已收录于专栏 ⭐️ <java 知识梳理>⭐️ 集合 集合体系图 单列集合: Collection 的子接口下的 List 和 Set 的实现子类均 ...

  2. 项目中,用到过的Java知识梳理(自己的百科全书)

    在项目中已经使用过的知识梳理 一.8种数据类型 基本逻辑类型 boolean 字符 char 整型 byte short int long 1 2 4 8 浮点型 float double 4 8 c ...

  3. Java知识系统回顾整理01基础03变量03字面值

    一.字面值定义 创建一个Hero对象会用到new关键字,但是给一个基本类型变量赋值却不是用new. 因为基本类型是Java语言里的一种内置的特殊数据类型,并不是某个类的对象.  给基本类型的变量赋值的 ...

  4. Java知识系统回顾整理01基础04操作符02关系操作符

    一.关系操作符 关系操作符:比较两个变量之间的关系  > 大于 >= 大于或等于 < 小于 <= 小于或等于 == 是否相等 != 是否不等 public class Hell ...

  5. Java知识系统回顾整理01基础04操作符07Scanner

    一.Scanner 需要用到从控制台输入数据时,使用Scanner类. 二.使用Scanner读取整数 注意: 使用Scanner类,需要在最前面加上 import java.util.Scanner ...

  6. Java知识系统回顾整理01基础01第一个程序01JDK 安装

    一.首先第一步看JDK配置成功后的效果 点WIN键->运行(或者使用win+r) 输入cmd命令 输入java -version 注: -version是小写,不能使用大写,java后面有一个空 ...

  7. Java知识系统回顾整理01基础05控制流程07结束外部循环

    一.break是结束当前循环 二.结束当前循环实例 break; 只能结束当前循环 public class HelloWorld { public static void main(String[] ...

  8. Java知识系统回顾整理01基础04操作符05赋值操作符

    一.赋值操作 赋值操作的操作顺序是从右到左 int i = 5+5; 首先进行5+5的运算,得到结果10,然后把10这个值,赋给i public class HelloWorld { public s ...

  9. java 01 02_Java知识系统回顾整理01基础02面向对象01类和对象

    一.面向对象实例--设计英雄这个类 LOL有很多英雄,比如盲僧,团战可以输,提莫必须死,盖伦,琴女 所有这些英雄,都有一些共同的状态 比如,他们都有名字,hp,护甲,移动速度等等 这样我们就可以设计一 ...

最新文章

  1. 活着不容易!几度被扼杀又雄起的NLP简史
  2. Go -- 配置监控系统
  3. 从汉诺塔讲递归的思考方式
  4. Twitter Heron 实时流处理系统简介
  5. 即时与及时有什么区别_什么是即时配送它和快递有什么不同,镖滴新势力
  6. 为什么要叫长虹玻璃呢_中和热测定实验中为什么温度计要放到环形玻璃棒中间?放外面可行吗?...
  7. 深度学习之卷积神经网络(3)卷积层实现
  8. 常用Python标准库对象速查表(1)
  9. HanLP自定义词典注意事项
  10. 郁闷,IT厂商认证考试没有通过!
  11. (转)JVM中的OopMap(zz)
  12. 楼下邻居是事逼怎么办
  13. python捕获键盘按键_Python中捕获键盘的方式详解
  14. 提取gps经纬度信息
  15. 02 SpringBoot入门程序剖析之各种稀奇古怪的starter
  16. 虚拟桌面分屏_办公人员必备技能,WINDOWS桌面分屏,多个桌面视图互不干扰
  17. ROS编译ORB-SLAM2或其各种变种的算法遇到的编译问题
  18. 压缩文件中文件名乱码问题
  19. 高德地图ajax距离,高德地图 API 计算两个城市之间的距离
  20. 多功能Web文件管理器Filestash

热门文章

  1. 名编辑电子杂志大师教程 | 导入、导出主题模板设置
  2. 计算机算法对程序设计的作用,浅谈对计算机程序设计的认识
  3. Java中常用的加密与解密
  4. playfair 加密与解密
  5. 小鹏G3 2020款到底有多智能
  6. 微信小程序css单位,微信小程序 rpx 尺寸单位详细介绍
  7. 在元宇宙星际旅行,程序化生成星球技术解读
  8. 金三银四如何抱佛脚?2022 最新大厂 Java 面试真题合集(附权威答案)
  9. KST-51单片机:c语言编程实现数码管动态显示秒表的倒计时
  10. 关于Excel文件导入