一、Java基础
1. Java有哪些基本数据类型?
byte、short、int、long、float、double、char、boolean。
2. Java类型转换。
从小类型到大类型,直接转。
从大类型到小类型,需要在强制类型转换的变量前面加上括号,然后在括号里面标注要转换的类型。强制类型转换可能导致溢出或损失精度。
3. 自动拆装箱。
自动装箱:将基本数据类型自动转换成对应的包装类。
自动拆箱:将包装类自动转换成对应的基本数据类型。
4. 基本数据类型缓冲池
事先缓存-128至127之间的整型数字,当需要进行自动装箱时,直接使用缓存中的对象,而不是重新创建一个新对象。
这个范围可以通过 -XX:AutoBoxCacheMax=size 参数进行调整。
5. 抽象类和接口的区别?
抽象类是可以包含抽象方法的类,被abstract修饰。单继承。
接口就是接口。多实现。
6. 内部类有哪些优点?
有效实现了多继承。
匿名内部类可以很方便地定义回调。
Java内部类详解:Java 内部类详解 | 菜鸟教程
7. Java有哪些引用类型?
强、软、弱、虚。
强引用:Java默认的引用类型,垃圾回收器永远不会回收被引用的对象。
软引用:用来描述一些非必需但仍有用的对象,软引用对象在内存足够时不会被回收,在内存不足时会被系统回收。常被用来实现缓存技术。
弱引用:只要JVM进行垃圾回收,就会回收弱引用对象。经典使用场景是ThreadLocal中的ThreadLocalMap中的Entry,它的key弱引用了一个ThreadLocal对象。
虚引用:随时可能会被回收,无法通过虚引用来获取对象。虚引用必须要和引用队列ReferenceQueue一起使用。
8. 类的初始化顺序。
先静态,再非静态。
先父类,再子类。
先属性/方法,再构造器。
9. Java中的修饰符。
public、protected、default、private。
static、final、abstract、synchronized、volatile。
10. final关键字。
修饰类,类不能被继承。
修饰方法,方法不能被重写。
修饰变量,变量在初始化后不能再被赋值。
11. switch支持的数据类型。
byte、short、int、char。
enum、String。
12. 面向对象三大特性。
封装、继承、多态。
13. Java是如何实现多态的?
继承父类或实现接口。
14. 重载和重写的区别。
重载:同一个类中的同名方法,根据参数列表类型不同来区分。
重写:父类和子类中的同名方法。重写方法的访问修饰符不能比父类中被重写的方法的访问权限更低。
15. Object类有哪些常见方法?
equals():判断相等。
hashcode():获取哈希值。
toString():转字符串。
wait():线程等待。
notify():唤醒正在等待的线程。
notifyAll():唤醒所有正在等待的线程。
clone():克隆对象。
finalize():对象首次被垃圾收集器回收时,该方法被执行。
getclass():获取对象的类。
16. equals方法和==的区别。
==比较的是两个变量的值是否相等,如果比较的是两个引用,则判断两个引用是否指向同一个地址。
equals比较的是两个对象的内容是否相同,具体如何比较要看equals方法是如何实现的。Object类中的equals方法就只是用了简单的==。
17. equals方法和hashcode方法的联系。
其实没什么联系。一般把这两个方法放到一起说,是想使用奇怪的对象作为hashMap的key。
向hashMap中put键值对时,如果key已存在,就更新value,这里判断key已存在的条件是 p.hash == hash && (p.key == key || key.equals(p.key)),其中p是与新结点发生哈希碰撞的结点,hash是用hashcode计算得到的。可以看出,如果想使用奇怪的对象作为hashMap的key,必须同时重写equals方法和hashcode方法。
18. 浅拷贝和深拷贝的区别。
浅拷贝:只复制对象,不复制对象引用的其他对象。
深拷贝:复制对象的同时,把对象引用的其他对象都复制一遍。
19. String s = new String("x")创建了几个对象?
如果字符串常量池中没有"x",则创建常量对象"x",和一个String类型的引用对象s。
如果字符串常量池中已有"x",则只创建引用对象s。
20. String、StringBuffer、StringBuilder的区别。
String是不可变对象,每次对String进行操作都产生新的String对象,所以尽量不要对String进行大量的拼接操作(不过如果在编译期就可以确定字符串拼接的结果,则在编译时直接优化成拼接结果)。
StringBuffer可以用来创建字符串,不产生中间对象,并且StringBuffer的每个方法都被synchronized修饰,是线程安全的。
StringBuilder不是线程安全的,但相较于StringBuffer有一定的速度优势,因此使用得比较多。
二、Java集合
1. 什么是集合?
集合是用于存放对象的容器,常用的集合类定义在java.util包中。
集合类只能存放对象的引用,不能存放基本数据类型或对象本身。
2. Java中有哪些常见的集合类?
Java中的集合主要包括Collection(集)和Map(映射)两大类,Collection接口又包括Set(集合)和List(列表)两个子接口。
List接口的主要实现类有:ArrayList、LinkedList、Stack、Vector等。
Set接口的主要实现类有:HashSet、TreeSet、LinkedHashSet等。
Map接口的主要实现类有:HashMap、TreeMap、HashTable、ConcurrentHashMap等。
3. 数组和集合的区别。
数组的长度固定,集合的长度可变。
数组可以存放基本数据类型,集合不可以。
4. 集合是否可以存储null?
List接口的实现类都可以存储多个null。
Set接口的实现类中,HashSet、LinkedHashSet可以存储一个null,TreeSet不能存储null。
Map接口的实现类中,HashMap、LinkedHashMap的key和value都可以为null,TreeMap的key不可用为null,value可以为null,HashTable、ConcurrentHashMap的key和value都不能为null。
5. 什么是Fail-Fast机制?
在实现了Collection接口的集合中,对于线程不安全的类,并发情况下可能会出现快速失败(fail-fast)情况,即:当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出ConcurrentModificationException。
我们以ArrayList为例,ArrayList继承自AbstractList类,AbstractList内部有一个modCount属性,代表修改的次数,每一次add、remove操作都会使modCount自增。
迭代器对象iterator有一个expectedModCount属性,它被赋值为开始遍历时的modCount值,并且这个值在遍历期间不再发生变化。
我们在使用迭代器调用next方法时,第一步就是检查modCount和expectedModCount是否相等,如果不相等就抛出ConcurrentModificationException。
那使用迭代器remove方法为什么不会触发fail-fast机制?原因是在remove方法中把expectedModCount修改成了modCount。
6. 什么是Fail-Safe机制?
Fail-Safe机制主要针对线程安全的集合类,如ConcurrentHashMap。
并发容器的iterator方法返回的迭代器内部保存了集合对象的一个快照副本,并且没有modCount校验,这样就可以保证并发读取时不会抛出异常,但是无法保证遍历读取的值和当前集合对象中的值是一致的。此外,创建集合快照需要时间和空间上的额外开销。
7. ArrayList和LinkedList的区别。
ArrayList的底层实现是动态数组,随机访问的效率较高。
LinkedList的底层实现是双向链表,增加或删除结点的效率较高。并且实现了Deque接口,可以用作双端队列。
8. Vector和ArrayList的区别。
Vector的方法加了synchronized关键字,是线程安全的。ArrayList是线程不安全的。
Vector每次扩容,容量乘以2,ArrayList乘以1.5。
9. 如何一边遍历一边删除集合中的元素?
使用迭代器Iterator.remove方法。
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.remove();
}
如果直接使用foreach循环遍历删除时,会自动生成一个iterator进行遍历集合,但Iterator不允许遍历中的元素背修改,所以会抛出ConcurrentModificationException。
10. ArrayList中的elementData为什么用transient修饰?
用transient修饰的成员变量不参与序列化过程。
elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,这样,有的空间就没有实际存储元素。
ArrayList在序列化时会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject从ObjectInputStream中获取size和element,再恢复到elementData。采用这样的方式来实现序列化,就可以保证只序列化实际存储的元素,而不是整个数组,从而节省时间和空间。
11. ArrayList的扩容机制。
创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add方法时,初始化定义数组容量为10,当容量已满,再调用add方法时就会发生扩容。
ArrayList扩容时,newCapacity = oldCapacity + (oldCapacity >> 1),即新容量是老容量的1.5倍。
当ArrayList的长度大于Integer.MAX_VALUE时,抛出OutOfMemoryError。
12. HashMap的底层数据结构是怎样的?
JDK1.8 之后,hashMap的数据结构是 数组+链表/红黑树。
链表长度大于8且数组长度大于等于64时,链表转换成红黑树。
链表长度小于6时,红黑树转换成链表。
链表转红黑树的阈值设为8的原因是:理想情况下使用随机hashCode算法,所有桶中的结点遵循泊松分布,在同一个桶位发生8次哈希碰撞的概率微乎其微,将阈值设为8可以保证链表在大多数情况下不会转红黑树。
红黑树转链表的阈值设为6的原因是:避免红黑树和链表之间频繁地来回转换。
13. 为什么使用红黑树?
首先红黑树是一棵二叉搜索树,方便查找。
相较于普通二叉搜索树,红黑树是一棵平衡树,它的每个结点的左右子树的高度差不会超过2,它的添加、删除、查找操作的最差时间复杂度为O(logn),避免了普通二叉搜索树最差情况下O(n)的复杂度。
红黑树的任何不平衡问题都可以用3次以内的操作解决(左旋、右旋、变色),如果使用严格的平衡二叉搜索树,会在调平衡上消耗更多的资源,整体性能上不如红黑树。
14. HashMap的put操作。
1) 计算插入元素的key的hash。计算方法是用key的hashCode高低16位做异或运算。这么做是为了增强hash的随机性,如果直接使用hashCode的话,如果使用的是本地(native)的hashcode方法还好,如果用户重写了hashcode方法,给了它一个糟糕的实现,那么得到的hashcode的随机性就会很差。
2) 判断数组是否为空,如果是就使用resize方法进行初始化。
3) 用hash值和(数组长度-1)做与运算,得到key在数组中对应的下标值。
4) 如果key对应的下标上没有发生哈希碰撞,就直接插入结点。如果发生了哈希碰撞,搜素该下标位置上是否存在hash值与当前hash值相等的结点,如果存在则使用新的value值覆盖原值,如果不存在就将新结点插入到链表/红黑树上。
5) 以上步骤完成后,判断当前hashMap中元素的数目是否超过了阈值,如果超过就调用resize方法进行扩容。
15. HashMap是如何进行扩容的?
hashMap扩容的阈值是桶数组容量乘以负载因子,负载因子默认是0.75。0.75是一个折衷的选择,如果负载因子较大,发生哈希碰撞的概率就更大,如果负载因子较小,存储消耗的空间就更多。
当hashMap中元素的数目超过了阈值时,将桶数组的大小扩充到2倍(实际是创建了一个新的数组)。
然后遍历已存储的结点,用结点的hash值与原数组大小做与运算(原数组大小是2的倍数,只有最高位是1,其他位都是0),若结果为0,将结点放到新数组的当前位置,若结果不为0,将结点放到新数组下标为 当前下标+原数组大小 的位置。这么做的好处是省略了重新计算结点下标的步骤。
16. 关于HashMap,JDK1.8有哪些优化?
引入了红黑树。
执行put时链表的插入方式由头插法改成了尾插法。采用头插法在扩容时会使链表发生反转(从头到尾遍历,从头到尾头插入),多线程环境下可能产生环形链表。
扩容时不再重新计算hash,而是与原数组大小做与运算。
原来先扩容再插入,改为了先插入再扩容。
17. HashMap是否是线程安全的?
不是。
一个线程执行put时导致扩容,另一个线程此时执行get,可能get的结果为null。
解决hashMap非线程安全问题的办法有:
1) 使用Collections.SynchronizedMap包装hashMap,得到一个SynchronizedMap对象。
2) 使用ConcurrentHashMap。
18. ConcurrentHashMap是如何实现的?
JDK1.8的实现:
采用CAS操作+synchronized锁,锁住链表的头结点,将锁的级别控制在table元素级别。
19. ConcurrentHashMap的get操作是否需要加锁?
不需要。因为Node的value和next是用volatile关键字修饰的,在多线程环境下,某个线程修改结点的value或者新增结点,对其他线程是可见的。
这也是ConcurrentHashMap比HashTable、Collections.SynchronizedMap效率高的原因。
20. ConcurrentHashMap的key和value为什么不支持null?
因为在多线程环境下,使用get方法得到了null,无法判断是因为value为null,还是没有找到对应的key。
21. LinkedHashMap是如何实现有序的?
LinkedHashMap结点Entry内部除了继承HashMap的Node属性,还有before和after用于标识前置结点和后置结点。
22. TreeMap是如何实现有序的?
TreeMap按照key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树实现的。
三、Java并发
1. Java线程有哪几种状态?
NEW:新建状态。创建了一个线程对象,还没有调用start方法。
RUNNABLE:可运行状态。是就绪和运行中两种状态的统称。其他线程调用线程对象的start方法后,线程对象进入RUNNABLE状态。线程未被CPU调度时为就绪状态,被CPU调度时为运行中状态,Java对这两种状态不做区分。
BLOCKED:阻塞状态,线程运行所需的资源被上锁而无法获得。
WAITING:等待状态,线程需要等其他线程做出一些特定动作(通知或中断)。
TIMED_WAITING:超时等待状态,经过指定的时间后,线程由等待状态自动转换为可运行状态。
TERMINATED:终止状态。线程执行完毕。
2. 如何创建一个线程?
继承Thread类,重写run方法,然后调用start方法启动线程。
实现Runnable接口,重写run方法。
使用线程池。
实现java.util.concurrent包中的Callable接口,重写call方法。
3. 线程池七大参数。
核心线程池大小、最大线程池大小、活跃时间、时间单位、阻塞队列、线程工厂、拒绝执行处理程序。
4. 线程池工作原理。
写太多次了,不想写了。
5. 线程池拒绝策略。
AbortPolicy:中止策略,抛出异常。
CallerRunsPolicy:调用者运行策略。
DiscardPolicy:丢弃策略。
DiscardOldestPolicy:丢弃最旧任务策略。
6. Java中常见的锁。
乐观锁、悲观锁。
偏向锁、自旋锁(轻量级锁)、重量级锁。
公平锁、非公平锁。
可重入锁、非可重入锁。
共享锁、排他锁。
7. synchronized锁的对象。
修饰普通方法:锁的是调用该方法的实例。
修饰静态方法:锁的是当前类。
直接锁对象:对象。
8. JVM对synchronized的优化。
锁膨胀:synchronized锁的四种状态为无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。会随着竞争加剧而逐渐升级。
锁消除:在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化:扩大锁的范围,避免反复加锁和解锁。
自适应自旋:自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自选时间及锁的拥有者的状态来决定。
9. synchronized锁升级过程。
新创建对象 --> 偏向锁 --> 轻量级锁 --> 重量级锁
偏向锁:给新创建对象上偏向锁:在对象的markword里记录当前线程的指针。偏向锁认为,在大部分情况下,使用synchronized上锁的对象只有一个线程要使用。只要有其他线程来争夺这个对象(轻度竞争),偏向锁就升级为轻量级锁。
轻量级锁:线程轻度竞争下,每个线程在自己的线程栈里划出一块空间,然后把对象的markword复制过来,称为锁记录(Lock Record),然后以CAS的形式尝试将对象的markword更新为指向锁记录的指针,更新成功的线程就获得了该对象的锁。
重量级锁:CAS本质上是程序在不停地循环运行,会占用CPU的资源,所以当线程之间竞争激烈的时候,轻量级锁升级为重量级锁。重量级锁将竞争激烈的线程放入等待队列,由操作系统负责线程调度。放入等待队列的线程不占用CPU资源。
10. JMM内存模型。
JMM是Java内存模型。
JMM定义程序中各个变量的访问规则,即在Java虚拟机中从内存中取出变量和将变量存储到内存中的规则。
JMM涉及到的八个原子操作为:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
11. volatile。
volatile关键字有两个作用:保证线程可见性、禁止指令重排序。
保证线程可见性:一个线程对主存的修改能及时地被其他线程观察到,这种特性被称为可见性。volatile可以保证线程可见性的原因是实现了缓存一致性协议(MESI)。
禁止指令重排序:CPU执行指令的顺序是由输入数据的可用性决定的,而不是由程序的原始数据决定的,这种技术被称为乱序执行。乱序执行在多线程下可能会出现问题。volatile可以阻止指令重排序的原因是使用了内存屏障。
12. CAS。
Compare and Swap,比较并交换。是一个原子操作。
解决ABA问题:给锁定对象加上版本号。
13. AQS。
AbstractQueuedSynchronizer,抽象队列同步器。是JUC中的类,我们常用的ReentrantLock、ReentrantReadWriteLock都是基于AQS实现的。
AQS的数据结构:volatile int + 双向链表。
AQS的内部属性:
state:volatile修饰的int类型数据,代表加锁的状态,初始为0。
exclusiveOwnerThread:Thread类型,记录当前加锁的是哪个线程,初始为null。
head:头结点,也是当前持有锁的线程。
tail:尾结点。每个新的结点进来,都插入到最后,形成了一个等待队列。
14. ThreadLocal内存泄漏的原因。
ThreadLocal是基于ThreadLocalMap实现的,其中ThreadLocalMap的Entry继承了WeakReference,Entry对象中的key使用了WeakReference封装,也就是说,Entry中的key是一个弱引用类型,对于弱引用来说,它只能存活到下次GC之前。
如果此时一个线程调用了ThreadLocalMap的set方法设置变量,当前的ThreadLocalMap就会新增一条记录,如果之前发生过一次垃圾回收,就会造成一个局面:key值被回收掉了,但是value值还在内存中,如果线程一直活着的话,它的value值就会一直存在。
解决办法:使用完key值之后,将value值通过remove方法删掉。
15. 为什么ThreadLocalMap的Entry的key要使用弱引用?
ThreadLocalMap的Entry的key是一个ThreadLocal对象,如果使用强引用,ThreadLocalMap会一直持有ThreadLocal的引用,ThreadLocal对象将无法被回收。
16. Semaphore。
信号量,可以维护当前访问自身的线程个数,并提供同步机制。
17. AtomicInteger如何保证线程安全?
通过CAS操作。
18. CountDownLatch和CyclicBarrier。
CountDownLatch通过一个计数器来实现。计数器的初始值为线程的数量,每当一个线程完成了自己的任务后,计数器的值就会减一,当计数器值达到0时,表示所有的线程都已完成任务,然后在闭锁上等待的线程就可以恢复执行任务。
CyclicBarrier的await方法每调用一次,计数便会减一,当计数减至0时,阻塞解除,所有在此CyclicBarrier上阻塞的线程开始运行。在这之后,如果再次调用await方法,新一轮重新开始。
四、Java虚拟机
1. JVM是由哪几部分组成的?
类加载子系统、执行引擎、运行时数据区、本地接口。
2. 运行时数据区的组成。
程序计数器、虚拟机栈、本地方法栈、堆、方法区。
3. 虚拟机栈帧的组成。
局部变量表、操作数栈、动态连接、方法返回地址。
4. 什么是元空间?
方法区的一种实现,和方法区的另一种实现 永久代的区别在于,永久代使用JVM内存,元空间直接使用本地内存。
5. 对象的创建过程。
类加载。当Java虚拟机遇到一条字节码new指令时,首先会去检查待创建对象的类是否已被加载、解析和初始化过。如果没有,先执行相应的类加载过程。
内存分配。虚拟机为新生对象分配内存。分配内存有“指针碰撞”和“空闲列表”两种方式,具体用哪种方式由Java堆内存是否规整决定,Java堆内存是否规整又由使用的垃圾收集器是否带有空间压缩整理的能力决定。
初始化。虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。
设置对象的头部信息。
执行构造函数。按照程序员的意愿对对象进行初始化。
6. 如何解决创建对象的并发问题。
通过CAS对分配内存空间的动作进行同步。
使用本地线程分配缓存(TLAB)。
7. 如何定位到内存中的对象?
直接指针或句柄访问。
句柄访问:在Java堆中划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例和类型数据各自的具体地址信息。
8. 对象的结构。
对象头。包含markword和类型指针两部分。
实例数据。
对齐补充。
9. 如何判断对象是否可以回收?
可达性分析。
以一系列称为GC Roots的对象作为起始点,从这些点向下搜索,搜索走过的路径被称为引用链,当一个对象到GC Roots没有引用链相连时,认为对象不可达。
10. GC Roots对象有哪些?
虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中的类静态属性引用的对象、方法区中的常量引用的对象。
11. 对象的回收过程。
JVM在回收阶段对对象进行两次标记,一次筛选。
如果经过可达性分析发现对象没有在GC Roots引用链中,进行第一次标记并筛选是否需要调用重写的finalize方法。
若待回收对象的类重写过finalize方法,且该重写方法未被虚拟机调用过,就把该对象放到一个叫F-Queue的队列中,并在稍后JVM自动创建一个低优先级的线程Finalizer去执行它。Finalizer只会触发finalize方法,但不一定等它执行结束。finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次标记,如果对象在finalize方法中重新与引用链上的对象建立联系,就可以拯救自己。
12. 四种引用类型。
强软弱虚。
13. 分代收集理论。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
一般情况下将堆区划分为新生代和老年代。新生代的特点是每次垃圾回收要回收掉大部分的对象,老年代的特点是每次垃圾回收只有少量对象需要被回收。新生代和老年代适合不同的垃圾收集算法。
目前大部分垃圾回收器对于新生代都采用复制算法,将新生代划分为一块较大的eden区域和两块较小的survivor区域,比例8:1:1,每次使用eden区和其中一块survivor区,垃圾回收时,将存活对象复制到另一块survivor区,然后清理掉eden区和from survivor区。
老年代一般使用的是标记-整理算法。
14. 有哪些垃圾收集算法?
标记-清除算法。
复制算法。
标记-整理算法。
15. 常见的垃圾收集器有哪些?
新生代收集器:Serial、ParNew、Parallel Scavenge。
老年代收集器:CMS、Serial Old、Parallel Old。
整堆收集器:G1。
16. CMS是怎么工作的?
1) 初始标记。标记GC Roots能直接到达的对象。
2) 并发标记。从GC Roots开始对堆种对象进行可达性分析,找出存活对象。期间用户线程可并发执行。
3) 重新标记。修正并发标记期间因用户程序继续运行而导致标记产生变动的那部分对象的标记记录。
4) 并发清除。堆标记的对象进行回收。
17. G1是怎么工作的?
1) 初始标记。标记GC Roots能直接到达的对象。
2) 并发标记。从GC Roots开始对堆中对象进行可达性分析,找出存活对象。期间用户线程可并发执行。
3) 最终标记。修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。
4) 筛选回收。对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
18. 触发GC的条件是什么?
1) Minor GC:从新生代回收内存被成为Minor GC,因为Java对象大多朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。触发条件很简单:当新生代空间不足时,就会触发Minor GC。
2) Major GC:老年代满时会触发Major GC,目前只有CMS回收器会单独收集老年代。
3) Full GC:对整堆和方法区进行垃圾回收。当老年代满时会引发Full GC,同时回收新生代、老年代;当永久代满时也会引发Full GC,会导致Class、Method元信息被卸载。
19. 什么是三色标记?
在垃圾回收算法中,标记是必不可少的一步。根据可达性分析,从GC Roots开始进行遍历访问,把遍历对象图过程中遇到的对象按是否访问过标记成三种颜色。
白色:尚未访问过。
黑色:已访问过,并且本对象引用的其他对象也已经全部访问过了。
灰色:已访问过,但是本对象引用的其他对象尚未全部访问完。
20. 类加载的过程。
1) 加载。通过全限定类名获取定义此类的二进制字节流,将字节流代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2) 验证。确保Class文件的字节流中包含的信息符合当前虚拟机的要求,需进行文件格式验证、元数据验证、字节码验证、符号引用验证。
3) 准备。为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
4) 解析。虚拟机将常量池内的符号引用替换为直接引用。
5) 初始化。执行类中定义的Java程序代码。此阶段执行的是<clinit>方法,该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。
21. 类初始化的顺序。
先静后动,在此前提下先父后子。
22. 有哪些类加载器?
启动类加载器,BootstrapClassLoader。负责加载JAVA_HOME/lib下的类库。
扩展类加载器,ExtensionClassLoader。负责加载JAVA_HOME/lib/ext目录中的类库。
系统类加载器,AppClassLoader。负责加载应用程序classpath目录下的所有jar和class文件。
自定义类加载器。实现自定义类加载器分两步,一是继承java.lang.ClassLoader,二是重写父类的findClass方法。
23. 双亲委派模型。
如果一个类加载器收到了类加载的请求,它不会马上去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层级的类加载器都是如此,因此所有的加载请求最终都被传送到顶层的启动类加载器中,只有父加载器无法完成加载请求时,子加载器才会尝试去加载。
24. 有哪些常见的调优工具?
JDK自带的jps、jinfo、jstat、jstack、jmap、jconsole等。
25. 有哪些常用的调优参数?
-Xms:初始堆大小。
-Xmx:最大堆大小。
-XX:NewRatio=n:设置老年代和新生代的比值。
-XX:SurvivorRatio:新生代中eden区和一个survivor区的比值。
-Xss:每个线程栈的大小。
-XX:PermSize=n:永久代初始值。
-XX:MaxPermSize=n:永久代大小。
-XX:MaxTenuringThreshold:新生代对象的最大年龄。
-XX:+UseG1GC:设置G1收集器。
-XX:+UseXXXGC:设置XXX收集器。
五、MySQL
1. MySQL有那些数据类型?
整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。
浮点数类型:FLOAT、DOUBLE、DECIMAL。
字符串类型:CHAR、VARCHAR、TEXT、BLOB。尽量避免使用TEXT/BLOB,查询时会使用临时表,导致严重的性能开销。
时间类型:DATE、DATETIME、TIMESTAMP。
更多内容:MySQL 数据类型 | 菜鸟教程
2. CHAR和VARCHAR的区别。
CHAR是定长的,对英文字符占用1字节,对中文字符占用2字节。
VARCHAR是变长的,对每个字符都使用2字节。
3. 数据库三大范式。
1NF:字段(或属性)是不可分割的最小单元,即不会有重复的列,体现原子性。
2NF:满足1NF前提下,存在一个候选码,非主属性全部依赖该候选码,即存在主键,体现唯一性。
3NF:满足2NF前提下,非主属性必须互不依赖,消除传递依赖。
4. 什么是反范式?
反范式的过程就是通过冗余数据来提高查询性能,可以减少表关联和更好地进行索引优化,但大量冗余数据会导致数据的维护成本变高。
在平时工作中,我们通常将范式和反范式结合使用。
5. 什么是索引?
索引是对数据库表中一列或多列值进行排序的数据结构,用于快速访问数据库表中的特定信息。
6. 索引的类型。
1) 从物理结构上可以分为聚集索引和非聚集索引。
聚集索引的键值的逻辑顺序与表中相应行的物理顺序一致,即每张表只能有一个聚集索引,也就是我们常说的主键索引。
非聚集索引的逻辑顺序与数据行的物理顺序不一致。
(聚集索引有点儿像字典上的拼音查找,非聚集索引有点儿像偏旁查找)
2) 从应用上可以分为以下几类:
普通索引:没有什么限制,允许在定义索引的列中插入重复值和空值。通过 ALTER TABLE table_name ADD INDEX index_name (column) 创建。
唯一索引:定义索引的列中的值必须是唯一的,但是允许为空值。通过 ALTER TABLE table_name ADD UNIQUE index_name (column) 创建。
主键索引:特殊的唯一索引,聚集索引,不允许有空值,由数据库自动创建。
组合索引:组合表中多个字段创建的索引,遵循最左前缀匹配原则。
全文索引:只有在MyISAM引擎上才能使用,只支持CHAR、VARCHAR、TEXT类型字段。
7. 索引的设计原则。
相较于普通索引,更建议使用唯一索引。
为常常作为查询条件的字段建立索引。
为经常需要排序、分组和联合操作的字段建立索引。
尽量使用数据量少的索引。如果索引的值很长,尽量使用前缀索引。
数据量较小的表不建议使用索引。如数量级在百万以内,查询花费的时间可能比遍历索引的时间还要短。
限制索引的数目。删除不再使用或很少使用的索引。
8. 索引的数据结构。
索引的数据结构和具体的存储引擎实现有关。MySQL中常用的是Hash索引和B+树索引。
一般情况下,Hash索引的查询效率更高,但是Hash索引无法进行范围查询和排序,也不支持模糊查询和多列索引,并且在某个键值大量重复时,产生严重的hash碰撞,查询效率会大大降低。而B+树索引支持范围查询和排序,而且查询效率高且稳定,所以B+树索引的使用范围更广。
9. 为什么使用B+树索引而不是B树索引?
B+树的非叶子结点只存索引关键字不存真实数据,单个内存页可以存储更多的关键字,存储相同的数据量,B+树的高度会比B树更低,磁盘I/0操作的次数也会相对较少。
B+树的叶子结点之间用链表有序连接,所以扫描全部数据只需要扫描一遍叶子结点,利于扫库和范围查询,而B树只能进行中序遍历,所以B+树的效率更高。
10. 什么是最左匹配原则?
最左优先,以最左边为起点的任何连续的索引都能匹配上,同时遇到范围查询(<、>、between、like)就会停止匹配。
如建立(a, b, c, d)索引,查询条件 a = 1 and b = 2 或 b = 2 and a = 1 都可以匹配到索引,优化器会自动调整 a, b 的顺序。但 b = 2 是匹配不到索引的,因为违背了最左前缀原则。如果是 a = 1 and b = 2 and c > 3 and d = 4,其中d是用不到索引的,因为c是一个范围查询,它之后的字段会停止匹配。
11. 覆盖索引。
覆盖索引指的是select查询的数据列只在索引中就能取得,不必读取数据行,换言之查询列要被所建的索引覆盖。
符含覆盖索引条件的一定是聚集索引。在InnoDB中,只有主键索引是聚集索引,如果没有主键,则挑选一个唯一值建立聚集索引,如果没有唯一键,则隐式地生成一个键来建立聚集索引。
12. 有哪些常见的存储引擎?如何选择?
InnoDB、MyISAM。
默认使用InnoDB。MyISAM适应于以插入为主的程序,比如博客系统、新闻门户。
13. InnoDB和MyISAM的区别。
InnoDB支持事务,而MyISAM不支持。
InnoDB支持外键,而MyISAM不支持。
InnoDB和MyISAM都支持B+树索引,但InnoDB是聚集索引,MyISAM是非聚集索引。
InnoDB支持表、行级锁,而MyISAM只支持表级锁。
InnoDB必须有唯一索引,而MyISAM可以没有。
14. InnoDB为什么推荐使用自增主键?
自增ID可以保证每次插入时B+树索引是从右边扩展的,相较于自定义ID可以避免B+树频繁地合并和分裂。
15. 什么是InnoDB的页、区、段?
InnoDB将物理磁盘划分为页(Page),每页的大小默认为16KB,是最小的存储单位。
InnoDB引入了区(Extent)的概念,一个区默认是64个连续的页组成的,也就是1MB。通过区对存储空间进行分配和回收。
B+树将数据分成了两部分,存储真实数据的叶子结点部分和存储索引的非叶子结点部分,对每个部分创建一个段(Segment)来存放。
16. 事务的四大特性。
ACID。
原子性:一个事务中的所有操作,要么全部生效,要么全部不生效。
一致性:在任意时间点,多个事务对同一数据的读取结果是一致的。
隔离性:事务执行时互不干扰,就好像只有当前事务正在执行。
持久性:事务被提交后,它对数据库中数据的改变是永久的。
17. 事务的隔离级别。
读未提交:不加锁,任何事务对数据的修改都会第一时间暴露给其它事务。可能会发生脏读、不可重复读、幻读。
读已提交:一个事务只能读到其它事务已经提交过的数据。不会发生脏读,可能会发生不可重复读、幻读。
可重复读:事务不会读到其它事务对已有数据的修改,即使其它事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。不会发生脏读、不可重复读,可能会发生幻读。
串行化:将事务的执行变为顺序执行,后一个事务执行必须等待前一个事务结束。不会发生脏读、不可重复读、幻读。
18. 什么是脏读、幻读、不可重复读?
脏读:读未提交隔离等级下,A事务(未提交)修改一条数据后,B事务能直接读取到修改后的数据,若A事务发生回滚,B事务就读到了脏数据。
不可重复读:读已提交和读未提交隔离等级下,B事务读取一条数据后,A事务(已提交)修改了这条数据,B事务再次读这条数据时,读到了修改后的数据——B事务两次读取数据的结果不一致。
幻读:可重复读、读已提交、读未提交隔离等级下,A事务(已提交)插入了一条新数据,B事务在A事务提交前后读到表中的数据总数不一样。
19. 数据库锁的分类。
1) 从锁的粒度划分,可以将锁分为表锁、行锁和页锁。
行级锁:只针对当前操作的行进行加锁。行级锁开销大、加锁慢,且会出现死锁,但锁的粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁:对当前操作的整张表加锁。实现简单,资源消耗较少。
页级锁:粒度介于行级锁和表级锁中间,一次锁定相邻的一组记录。可能出现死锁,并发度一般。
2) 从使用性质划分,可以分为共享锁、排他锁和更新锁。
共享锁:S锁,又称读锁,用于所有的只读操作。加读锁时允许其他事务加读锁,不允许加写锁。读取结束后立即释放,无需等待事务结束。
排他锁:X锁,又称写锁,用于写操作。加写锁时,不允许其他事务加读锁或写锁。直到事务结束才释放。使用 select * from table_name for update 创建写锁。
更新锁:U锁,预定要对资源加写锁,允许其他事务读,但不允许再加更新锁或写锁。当被读取的页要被更新时,升级为写锁。更新锁的作用是避免使用共享锁造成死锁。
3) 从主观上划分,可以分为乐观锁和悲观锁。
乐观锁:认为资源是不会被修改的,所以不加锁读取数据,仅当更新时用版本号机制等确认资源是否已被修改。即CAS操作。
悲观锁:认为资源一定会被其他事务修改,所以每次操作前都要上锁。
20. 隔离级别和锁的关系。
在读未提交级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突。
在读已提交级别下,读操作需要加共享锁,在语句执行完以后释放锁。
在可重复读级别下,读操作需要加共享锁,在事务提交之前不会释放锁。
在串行化级别下,锁定整个范围的键,并一致持有锁,直到事务完成。
21. 存储过程。
存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比SQL语句的效率更高。
22. 存储过程和函数的区别。
函数有1个返回值,而存储过程是通过参数返回的,可以有多个或者没有。
函数可以在查询语句中直接调用,而存储过程必须单独调用。
23. MySQL中有哪些常用的日志?
redo log:重做日志。作用是保证事务的持久性。重做日志记录事务执行后的状态,用来恢复未写入data file的已提交事务数据。
undo log:回滚日志。作用是保证数据的原子性。回滚日志保存了事务发生前的一个版本的数据,可以用于回滚,同时可以提供多版本并发控制下的读,即非锁定读。
binlog:二进制日志。常用于主从同步或数据同步中,也可用于数据库基于时间点的还原。
error log:错误日志。记录着MySQL的启动和停止,以及服务器在运行过程中发生的错误的相关信息。在默认情况下,系统记录错误日志的功能是关闭的,错误信息被输出到标准错误输出。
general query log,普通查询日志。记录服务器接收到的每一个命令,无论命令是否正确,会带来不小的开销,因此默认是关闭的。
slow query log,慢查询日志。记录执行时间过长和没有使用索引的查询语句(默认10s),只记录执行成功的语句。
relay log,中继日志。在从节点中存储接收到的binlog日志内容,用于主从同步。
24. 主从复制。
主从复制用来建立一个与主数据库完全一样的数据库环境,即从数据库。主数据库一般是准实时的业务数据库。
主从复制的作用一般有:
读写分离,使数据库能支撑更大的并发。
高可用,做数据的热备,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。
25. 主从复制的架构。
1) 一主一从或一主多从。
在主库的请求压力非常大时,可通过配置一主多从架构实现读写分离,把大量对实时性要求不是很高的读请求通过负载均衡分发到多个从库上去读取数据,降低主库的读取压力。并且在主库出现宕机时,可将一个从库切换为主库继续提供服务。
2) 主主复制、多主一从、级联复制。
26. 主从复制的实现原理。
数据库中有个binlog二进制文件,记录了数据可执行的所有SQL语句。主从同步的目标就是把主数据库的binlog文件中的SQL语句复制到从数据库,让其在从数据库的relaylog文件中再执行一次即可。
具体实现需要三个线程:
binlog输出线程:每当有从从库连接到主库时,主库创建一个线程然后发送binlog内容到从库。
从库IO线程:当START SLAVE语句在从库开始执行之后,从库创建一个IO线程,该线程连接到主库并请求主库发送binlog里面的更新记录到从库。从库IO线程读取主库的binlog输出线程发送的更新并拷贝这些更新到本地文件,其中包括relaylog文件。
从库SQL线程:从库创建一个SQL线程,读取从库IO写到relay log的更新事件并执行。
27. Where和Having的区别。
where条件的作用是在对查询结果进行分组前,将不符合条件的行过滤掉,即在分组之前过滤数据。where条件中不能包含聚组函数。
having条件的作用是筛选满足条件的组,即在分组后过滤数据,条件中经常包含聚组函数。
28. SQL关键字的执行顺序。
from - on - join - where - group by - arg func - with - having - select - distinct - order by - limit
29. Union和Union All的区别。
union对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序。
union all对两个结果集进行并集操作,包括重复行,不进行排序。
30. 一条SQL是如何执行的?
1) 连接数据库。
连接器负责跟数据库连接、获取权限、维持和管理连接,如果用户认证通过,连接器会到权限表里面查询用户拥有的权限,之后该连接的权限验证都依赖于刚查出来的权限。
MySQL既支持短连接,也支持长连接。长连接默认8小时断开。
2) 查询缓存。
获取连接后,select语句会先去查询缓存,看之前是否执行过,如果获取到缓存后就返回缓存信息,否则继续后面的步骤。
3) 词法分析。
MySQL识别出SQL语句中的字符串分别是什么,代表什么。
4) 语法分析。
根据词法分析的结果,判断SQL语句是否满足语法。
5) SQL优化。
优化器会对SQL的执行顺序,使用哪个索引进行优化,确定SQL的执行方案。
6) SQL执行。
执行器校验用户权限,如果通过,就打开表,根据表的引擎定义,使用对应引擎提供的接口,执行SQL。
31. SQL执行计划。
使用EXPLAIN命令查看执行计划,只需在查询语句开头增加EXPLAIN关键字即可。
结果中的重要参数:
id:代表执行select语句或操作表的顺序,如果包含子查询,会出现多个ID。值越大,优先级越高。
select_type:查询类型,区别普通查询、联合查询以及子查询等。
table。
type:查询扫描情况,从最好到最差依次是system、const、eq_ref、ref、range、index、All。
possible_keys:显示可能应用在这张表中的索引,这里查到的索引不一定真正地用到。
key:实际使用到的索引。
key_len:索引中使用的字节数,在不损失准确性的情况下越短越好。
ref:显示索引的哪一列被使用了。
rows:根据表统计信息及索引使用情况,估算出找到所需记录需要读取的行数。
extra。
32. 索引失效的情况。
使用多列索引不遵守最左前缀原则时,索引失效。
select * 时不使用任何索引。
索引列上有计算或索引列使用了函数时,索引失效。
当查询字段发生类型转换时,索引失效。
模糊查询通配符以%开头时,索引失效。
使用or关键字时,前面和后面的字段都要加索引,否则所有的索引都会失效。
使用not in和not exists时,索引失效。
使用order by不加where或limit,或不遵守最左前缀原则时,索引失效。
六、Redis
1. Redis数据结构。
字符串String、字典Hash、列表List、集合Set、有序集合ZSet。
2. Redis数据过期策略。
通常,我们在内存中创建一条缓存记录时,会指明这条记录在内存中的超时时间。否则除非用户手动删除,这条记录将永远存在于内存中,很可能造成内存泄漏。
实现缓存过期的方式一般有两种:被动方式和主动方式。两种方式需要结合使用。
被动方式:当客户端尝试访问一条缓存记录时,如果缓存记录已过期,就删掉这条记录并返回 null。
主动方式:每隔一段时间,从关联了超时时间的缓存记录中随机选取一些进行测试,删除已过期的记录。
3. Redis持久化策略。
RDB和AOF。
RDB是在指定的时间间隔内将内存中的数据集快照写入磁盘。
AOF是将每一个收到的写命令记录下来,随着AOF持久化文件越来越大,Redis会对AOF文件进行重写。
4. Redis保证原子操作。
支持执行Lua脚本。