Java
基础
Java中的集中基本数据类是什么?对应的包装类型是什么?各自占用多少字节?
基本数据类型 | 包装类 | 占用字节 | 默认值 |
---|---|---|---|
byte |
Byte |
1 | 0 |
short |
Short |
2 | 0 |
int |
Integer |
4 | 0 |
long |
Long |
8 | 0L |
char |
Character |
2 | 'u0000' |
float |
Float |
4 | 0f |
double |
Double |
8 | od |
boolean |
Boolean |
1 | false |
String
、StringBuilder
、StringBuffer
的区别是什么?
String
是不可变字符串,因为String
类和底层实现的char
数组都用final
关键词进行修饰,一旦确定后就不能进行改变了。StringBuilder
和StringBuffer
是可变长字符串,都继承自AbstractStringBuilder
类。可以通过append
、insert
函数进行字符串修改。不同的是:StringBuilder
不是线程安全的,而StringBuffer
是线程安全的(对方法加了同步锁),因此StringBuilder
效率更高一点。
String s1 = new String("abc");
这段代码创建了几个字符串对象?
会创建1或2个字符串对象
- 如果字符串常量池中不存在字符串对象”abc”的引用,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共2个字符串对象。
- 如果字符串常量池中已经存在字符串对象“abc”的引用,则只会在堆中创建一个字符串对象”abc”
==
和equals
的区别?
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值 - 对于引用数据类型来说,
==
比较的是对象的内存地址
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals
不能用于判断基本类型的变量,只能用来判断两个对象是否相等。equals
方法存在于Object
类中,因此所有的类都有equals
方法。
当类没有重写
equals
方法时:使用equals
比较两个对象,相当于使用==
比较两个对象。使用的是默认Object
类中的equals
方法。1
2
3
4// Object 类中的equals方法
public boolean equals(Object obj){
return (this == obj);
}当前类重写了
equals
方法,一般我们都重写equals
方法来比较两个类中的某些属性是否相等,如果相等就返回true
。
hashCode
和equals
的关系?
hashCode
的作用是获取哈希码(int
整数),也称散列码,这个哈希码的作用是确定该对象在哈希表中的索引位置。
一般我们在重写equals
方法时必须重写hashCode
方法,原因是:
两个相等的对象的hashCode
值必须是相等。也就是说如果equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。
总结:
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
包装类型的缓存机制?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
、Short
、Integer
和Long
这4种包装类型默认创建了数值[-128, 127]
的相应类型的缓存数据。Character
创建了数值在[0, 127]
范围的缓存数据,Boolean
直接返回True
orFalse
示例:为什么结果输出false
1 | Integer i1 = 40; |
Integer i1 = 40;
发生装箱,等价于Integer i1 = Integer.valueOf(40)
。因此i1
直接使用的是缓存中的对象。而Integer i2 = new Integer(40);
会直接创建新的对象。所以i1
和i2
是两个不同的对象,结果返回false
。
自动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来。原理是调用包装类的
valueOf()
方法。 - 拆箱:将包装类型转换为基本数据类型。原理是调用包装类的
xxxValue()
方法。
1 | Integer i = 10; // 装箱,等价于 Integer i = Integer.valueOf(10); |
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
深拷贝和浅拷贝的区别了解吗?什么是引用拷贝?
- 浅拷贝:会在堆上创建一个新的对象(区别引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:会完全复制整个对象,包括这个对象所包含的内部对象。
- 引用拷贝:就是两个不同的引用指向同一对象。
谈谈对Java注解的理解,解决了什么问题?❗
Annotation
(注解)是Java5开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation
的特殊接口:
1 |
|
Exception
和Error
有什么区别?
在Java中,所有的异常都有一个共同的祖先java.lang
包中的Throwable
类,Throwable
有两个重要的子类:
Exception
:程序本身可以处理的异常。可以通过catch
进行捕获。Exception
又可以分为两类:Checked Exception
:受检查异常,Java代码在编译过程中,如果受检查异常没有被catch
或者throws
关键字处理的话,就没办法通过编译。例如:
int i = 10 / 0;
如果不使用catch
或者throws
关键字进行处理则无法通过编译。Unchecked Exception
:不受检查异常,Java代码在编译过程中吗,我们即使不处理不受检查异常也可以正常通过编译。
Error
:程序无法处理的错误。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Java反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?❗
Java反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。
- 优点:让代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
- 缺点:存在安全问题,例如无视泛型参数的安全检查。性能较差。
使用反射后,增加了程序的灵活性,避免将代码写死,降低了耦合性。
Java泛型了解吗?什么是类型擦除?介绍一下常用的通配符?❗
泛型:是JDK5中引入的一个新特性,使用泛型参数,可以增强代码的可读性以及稳定性。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
泛型擦除机制
Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说的类型擦除。
编译期间会动态的将泛型T
擦除为Object
,或者将T extends xxx
擦除为其限定类型xxx
1 | public void print(List<String> list) { // 因为下面“重写”,所以报错 |
原因是:泛型擦除后,List<String>
和List<Integer>
在编译以后都变成了List<Object>
。因此会报错。
通配符
泛型类型是固定的,某些场景下使用起来不太灵活,于是通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。
通配符?
和常用的泛型T
有什么区别?
T
可以用于声明变量或常量,?
不行T
一般用于声明泛型类或方法,通配符?
一般用于泛型方法的调用代码或形参。T
在编译期会被擦除为限定类型或Object
,通配符用于捕获具体类型。
上边界通配符extends
List<? extends Person>
限制必须是Person
的子类。
下边界通配符super
LIst<? super Employee>
限制必须是Employee
的父类。
? extends xxx
和? super xxx
的区别?
两者接收参数的范围不同。
? extends xxx
声明的泛型参数只能调用get()
方法返回xxx
类型,调用set()
报错。? super xxx
声明的泛型参数只能调用set()
接收xxx
类型,调用get()
报错。
T extends xxx
和? extends xxx
的区别?
T extends xxx
用于定义泛型类和方法,擦除后为xxx
类型? extends xxx
用于声明方法形参,接收xxx
和其子类类型。
内部类了解吗?匿名内部类了解吗?
内部类分为下面4种:
- 成员内部类
- 静态内部类
- 局部(方法)内部类
- 匿名内部类
BIO、NIO、AIO有什么区别?
- BIO属于同步阻塞IO模型,应用程序发起read调用后,会一直阻塞,知道在内核把数据拷贝到用户空间。
- NIO:Java中的NIO可以看作是I/O多路复用模型。
- AIO:异步IO模型。
Java集合框架
说说List
、Set
、Map
三者的区别?三者底层的数据结构?
List
:存储的元素是有序的,可以重复的。ArrayList
:底层是Object[]
数组。Vector
:底层是Object[]
数组。LinkedList
:双向链表(JDK1.6之前是循环链表,JDK1.7后取消了循环)
Set
:存储的元素是无序的,不可重复的。HashSet
(无序,唯一):基于HashMap
实现的。底层使用HashMap
来保存元素。LinkedHashSet
:是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。TreeSet
(有序,唯一):红黑树(自平衡的排序二叉树)
Map
:存储的是键值对(key-value
),其中key是无序的,不能重复的;值是无序的,可以重复。灭个键最多映射到一个值。HashMap
:JDK1.8之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则主要是为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认是8),会将链表转换为红黑树,以减少搜索时间。LinkedHashMap
:继承自HashMap
,所以他的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双链表,使得上面的机构可以保证键值对的插入顺序。HashTable
:数组+链表组成的,数组是HashTable
的主体,链表则是主要为了解决哈希冲突而存在的。TreeMap
:红黑树(自平衡的排序二叉树。
有哪些集合是线程不安全的?怎么解决呢?
线程不安全的集合:HashMap
、LinkedHashMap
、TreeMap
、HashSet
、LinkedHashSet
、TreeSet
选用线程安全的集合:ConcurrentHashMap
、HashTable
和ConcurrentHashSet
.
比较HashSet
、LinkedHashSet
、TreeSet
三者的异同
- 都是
Set
接口的实现类,都能保证元素唯一,并且都是线程不安全大的。 - 三者的主要区别在于底层数据结构不同。
HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashMap
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 - 根据三者的底层实现,三种结构的使用场景也不相同。
HashMap
和HashTable
的区别?HashMap
和HashSet
的区别?HashMap
和TreeMap
的区别?
HashMap
和HashTable
的区别?
线程是否安全:
HashMap
是非线程安全的,HashTable
是线程安全的,因为HashTable
内部的方法基本都经过synchronized
修饰。(如果需要保证线程安全的话就使用ConcurrentHashMap
)效率:因为线程安全的问题,
HashMap
要比HashTable
效率高一点。另外,HashTable
基本被淘汰,不要在代码中使用!!!对NULL key和Null value的支持:
HashMap
可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个;HashTable
不允许有null键和null值,否则会抛出NullPointerException
。初始容量大小和每次扩容大小的不同:
创建时如果不指定容量初始值,
HashTable
默认的初始大小为11,之后每次扩容,容量变为原来的zn + 1
。HashMap
默认的初始化大小为16。之后每次扩容,容量变为原来的2倍。如果给定了容量初始值,那么
HashTable
会直接使用给定的大小,而HashMap
会将其扩充为2的幂次方(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
底层数据结构:JDK1.8以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换成红黑树),以减少搜索时间。
HashMap
和HashSet
的区别?
HashSet
底层就是基于 HashMap
实现的。
HashMap |
HashSet |
---|---|
实现了Map 接口 |
实现Set 接口 |
存储键值对 | 进存储对象 |
调用put() 向map中添加元素 |
调用add 方法向Set 中添加元素 |
HashMap 使用键(Key)计算hashCode |
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
HashMap
和TreeMap
的区别?
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。
定制排序示例代码:
1 | /** |
HashMap
的底层实现
JDK1.8之前
底层实现是数组和链表结合在一起使用也就是链表散列。
HashMap
通过key的hashCode
经过扰动函数处理过后得到hash值,然后通过(n - 1) & hash
判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
这里的扰动函数就是HashMap
的hash
方法。使用hash
方法也就是扰动函数是为了防止一些实现比较查的hashCode()
方法,换句话说,使用扰动函数之后可以减少碰撞。
1 | // 扰动函数 |
拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。(类似于邻接表)
JDK1.8之后
底层实现是数组和链表/红黑树。相比于JDK1.8之前主要实在解决哈希冲突时有了较大的变化:
当链表长度大于阈值(默认是8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,会选择优先进行数组扩容,而不是转换成红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap
的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length == hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
ConcurrentHashMap
和HashTable
的区别?
主要体现在实现线程安全的方式上不同。
- 底层数据结构:JDK1.7的
ConcurrentHashMap
底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑树。HashTable
和JDK1.8之前的HashMap
的底层数据结构类似都是采用数组+链表的形式,数组是HashMap
的主体,链表则是为了解决哈希冲突而存在的。 - 实现线程安全的方式(重要):
- JDK1.7的
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment
,分段锁),每一把锁都只锁容器的一部分。 - JDK1.8的
ConcurrentHashMap
摒弃了Segment
的概念,底层实现改为了数组+链表/红黑树。并发控制使用synchornized
和CAS来操作。 HashTable
(同一把锁):使用synchornized
来保证线程安全,效率非常低。
- JDK1.7的
ConcurrentHashMap
线程安全的具体实现方式/底层具体实现?
JDK1.8之前
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment
继承了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
1 | static class Segment<K,V> extends ReentrantLock implements Serializable { |
一个 ConcurrentHashMap
里包含一个 Segment
数组,Segment
的个数一旦初始化就不能改变。 Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
JDK1.8之后
ConcurrentHashMap
取消了Segment
分段锁,采用了Node + CAS + synchornized
来保证并发安全。
Java 8 中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
JVM❗
JVM内存结构
JVM内存结构呆滞分为五个部分:程序计数器、虚拟机栈、本地方法栈、堆和方法区。除此之外,还有由堆中引用的JVM外的直接内存。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。是线程私有的。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
Java虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈。也是线程私有的。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
可能抛出的异常:
- 当线程请求的栈深度超过最大值,会抛出
StackOverflowError
异常,这种异常在无停止条件的递归情况下会发生。 - 栈进行动态扩展时如果无法申请到足够内存,会抛出
OutOfMemoryError
异常。
本地方法栈
一些带有native
关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带native
关键字的方法。
堆
所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。通过new
关键字创建得对象会被放在堆内存。
方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError
异常。
运行时常量池:
运行时常量池是方法区的一部分。常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
JVM调优参数
堆内存相关
堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
显示指定堆内存-Xms
和-Xmx
1 | -Xms<heap size>[unit] # 最小堆大小 |
heap size
表示要初始化内存的具体大小unit
表示要初始化内存的单位。例如:g(GB),m(MB),k(KB)。
显示指定永久代/元空间的大小
从Java8开始,如果我们没有指定Metaspace的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代不会出现这种情况)。
1 | -XX:MetaspaceSize=N # 设置Metaspace的初始大小 |
垃圾收集相关
垃圾回收器
JVM具有四种类型的GC实现:
- 串行垃圾收集器:
-XX:UseSerialGC
- 并行垃圾收集器:
-XX:UseParallelGC
- CMS垃圾收集器:
-XX:UseParNewGC
- G1垃圾收集器:
-XX:UseG1GC
GC日志记录
1 | # 必选 |
什么是类加载?何时类加载?类加载流程?
类的生命周期
类从被加载到虚拟机内存中开始到卸载出内存开始,它的整个生命周期可以简单概括为7个阶段:加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中前三个阶段可以统称为连接(Linking)。
类加载流程
class文件需要加载到虚拟机中之后才能运行和使用。虚拟机加载这些class文件的步骤如下:
系统加载Class类型的文件主要三步:加载 -> 连接 -> 初始化
。连接过程又可以分为三步:验证 -> 准备 -> 解析
加载
类加载过程的第一步,主要完成以下工作:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段主要由四个检验阶段组成:
- 文件格式验证(Class文件格式检查):验证字节流是否符合Class文件格式的规范。
- 元数据验证(字节码语义检查):对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java虚拟机规范》的要求。
- 字节码验证(程序语义检查):通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
- 符号引用验证(类的正确性检查):验证该类的正确性。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。
初始化
初始化阶段是执行初始化方法
<clinit>()
方法的过程,是类加载的最后一步,这一步JVM才开始真正执行类中定义的Java程序代码(字节码)
虚拟机严格规范了有且只有5中情况下,必须对类进行初始化
- 当遇到
new
、getstatic
、putstatic
或invokestatic
这4条字节码指令时。 - 使用
java.lang.reflect
包的方法对类进行反射调用时。如:Class.forname("...")
、newInstance()
等等。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类(包含
main
方法的那个类),虚拟机会先初始化这个类。 MethodHandle
和VarHandle
可以看作是轻量级的反射调用机制。而要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类。
知道哪些类加载器?类加载器之间的关系?
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
类加载器的作用:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中加载这一步。
- 每个Java类都有一个引用指向加载它的
ClassLoader
。 - 数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由JVM直接生成的。
简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
JVM中内置了三个重要的ClassLoader
BootStrapClassLoader
(启动类加载器):最顶层的加载类,由C++实现,通常表示为null,并且没有父级,主要用来加载JDk内部的核心类库(%JAVA_HOME%/lib
目录下的rt.jar
、resources.jar
等jar包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。ExtenstionClassLoader
(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext
目录下的jar包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。AppClassLoader
(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
类加载器的双亲委派了解吗?结合Tomcat说一下双亲委派(Tomcat如何打破双亲委托机制?)❗
双亲委派模型是用来明确哪个类加载器加载。
ClassLoader
类使用委托模型来搜索类和资源。每个ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
为什么需要双亲委派?
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。
Java内存模型
栈中存放什么数据?堆中呢?
- 虚拟机栈:存放方法、局部变量、运行数据。线程私有。
- 本地方法栈:存储
Native
方法。线程私有。 - 堆:存放所有创建的对象,数组。所有线程共享区域。
大对象放在哪个内存区域?
大对象就是需要大量连续内存空间的对象。比如:字符串、数组。
大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
堆被划分为两个不同的区域:新生代(Young)(1/3堆空间)、老年代(Old)(2/3堆空间)。其中新生代(Young)又被划分为三个区域:Eden(8/10)、From Survivor(1/10)、To Survivor(1/10)。
堆区如何分类?
在JDK7版本及JDK7版本之前,堆内存被通常分为下面三部分:
- 新生代内存:占1/3堆空间。
- 老生代内存:占2/3堆空间。
- 永久代
下图所示的Eden
区、两个Survivor
区S0和S1都属于新生代,中间一层属于老年代,最下面一层属于永久代。
JDK8版本之后PermGen(永久代)已经被Metaspace(元空间)取代,元空间使用的是直接内存。
垃圾回收有哪些算法?
标记-清除算法
标记-清除(Mark-and-Sweep)算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片。
标记-清除过程:
- 当一个对象被创建时,给一个标记位,假设为0/false。
- 在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为1/true。
- 扫描阶段清除的就是标记位0/false的对象。
复制算法
为了解决标记-清除算法的效率和内存碎片问题,复制(Copying)收集算法出现了。
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
依然存在下面这些问题:
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记-整理算法
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
因为多了整理这一步,因此效率也不高,适合老年代这种回收频率不高的场景。
分代收集算法💡
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
GC的全部流程
死亡对象判断
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计算器就加1。
- 当引用失败,计数器就减一。
- 任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。
可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
下图中的 Object 6 ~ Object 10
之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
垃圾回收
参考上面的垃圾回收算法。
GC中老年代用什么回收方法?
老年代中的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清楚”或“标记-整理”算法进行垃圾回收。
多线程
线程和进程的区别💡
进程
进程是程序一次执行过程,是系统运行程序的最基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源。但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
进程和线程的关系
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。💡
什么是上下文切换
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用CPU状态中退出。
- 主动让出CPU,比如调用了
sleep()
、wait()
等。 - 时间片用完,因为操作系统要防止一个线程或进程长时间占用CPU导致其他线程或进程饿死。
- 调用了阻塞类型的系统中断。比如请求IO,线程被阻塞。
- 被终止或结束运行。
其中前三种都会发生线程切换,线程切换意味着要保存当前线程的上下文,留待线程下次占用CPU的时候恢复线程。并加载下一个将要占用CPU的线程上下文。这就是所谓的上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
什么是线程死锁?如何避免死锁?
线程死锁
多个线程同时阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁产生的四个条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何预防和避免死锁
预防死锁:破坏死锁的产生的必要条件即可。
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放,
避免死锁:
在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,时期进入安全状态。
乐观锁和悲观锁了解吗?
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停的执行,无需加锁也无需等待,只是在提交修改的时候区验证对应的资源(也就是数据)是否被其他线程修改。
悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿这个资源就会阻塞直到锁被上一个持有者释放。也就是说:共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
说说sleep()
方法和wait()
方法的区别和共同点
共同点:两者都可以暂停线程的执行。
区别:
sleep() |
wait() |
---|---|
没有释放锁 | 释放了锁 |
通常用于线程间的交互/通信 | 通常被用于暂停执行 |
该方法执行完之后,线程会自动苏醒,也可以使用wait(long timeout) 超时后线程会自动苏醒 |
方法被调用后,线程不会自动苏醒,需要别的线程调用同一对象上的notify() 或者notifyAll() 方法。 |
是Thread 类的静态本地方法 |
是object 类的本地方法。 |
拓展1:为什么wait()
方法不定义在Thread
中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入WAITING状态,自然是要操作对应的对象Object
而非当前线程Thread
。
拓展2:为什么sleep()
方法定义在Thread
中?
因为sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?新线程添加的流程?❗
实现Runnable
接口和Collable
接口的区别
讲一下JMM(Java内存模型)。volatile
关键字解决了什么问题?说说synchronized
关键字和volatile
关键字的区别。
AQS原理了解吗?AQS组件有哪些?
用过CountDownLatch
吗?什么场景下用的?
数据库
MySQL基础
非关系型数据库和关系型数据库的区别?
事务的四大特性
事务需要遵循ACID四个特性
- A(atomicity)原子性:是指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都成功,整个事务的执行才算成功。
- C(consistency)一致性:一致性指事务将数据库从一种状态转变成另一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- I(isolation)隔离性:要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事务提交对其他事务都不可见,通常使用锁来实现。
- D(durability)持久性:事务一旦提交,其结果就是永久性的,即使发生宕机,数据库也能将数据恢复。保证了事务系统的高可靠性,而不是高可用性。
MySQL事务隔离级别?默认是什么级别?
SQL定义了四种隔离级别:
- 读未提交RU(READ UNCOMMITTED)
- 读提叫RC(READ COMMITTED)
- 可重复读RR(REPEATABLE READ)。InnoDB存储引擎默认的支持隔离级别
- 串行化(SERIALIZABLE)
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交RU | 可能 | 可能 | 可能 |
读提交RC | 不可能 | 可能 | 可能 |
可重复读RR | 不可能 | 不可能 | 不可能 |
串行化 | 不可能 | 不可能 | 不可能 |
MySQL中,InnoDB存储引擎默认的隔离级别是可重复读RR。
乐观锁和悲观锁的区别
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停的执行,无需加锁也无需等待,只是在提交修改的时候区验证对应的资源(也就是数据)是否被其他线程修改。
悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿这个资源就会阻塞直到锁被上一个持有者释放。也就是说:共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
使用场景不同
- 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。
- 乐观锁通常多用于写比较少的情况下(多读场景,竞争比较少),这样可以避免频繁枷锁影响性能。
MySQL数据库两种存储引擎的区别
MyISAM和InnoDB的数据结构都是B+树。B+树的非叶子结点不存储数据,只有叶子结点才会存储数据。
区别 | MyISAM | InnoDB |
---|---|---|
结构 | 每个MyISAM在磁盘上存储成三个文件(扩展名指出文件类型)。.frm 文件存储表定义,也就是存储结构的信息。.MYD 文件存放数据文件,.MYI 存放索引文件。 |
表空间数据文件和他的日志文件。没有了myd 和myi ,只有.idb 放了索引位置以及表的信息位置。 |
事务 | 强调的是性能,不提供事务支持 | 提供事务支持事务,外部键等高级数据库功能。 |
CRUD | 查询很合适 | 增加或更新更合适,删除的时候,就是一行一行的删除。 |
表的具体行数 | select count(*) from table ,可以很好的读取 |
不保存表的具体行数,要扫描一遍整个表来计算有多少行 |
AUTO_INCREMENT | 可以和其他字段一起建立联合索引 | 必须包含只有该字段的索引 |
全文索引 | 支持 | 不支持 |
外键 | 不支持 | 支持 |
锁 | 表锁 | 提供行锁(执行一个SQL语句时MySQL不能确定要扫描的范围,同样会锁全表,例如:update table set num=1 where name like "%aaa%" ) |
索引查询方法 | 会先根据索引查找到数据地址,再根据地址查询到具体的数据。并且主键索引和辅助索引没有区别。 | 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值; |
B+树叶子结点存储内容 | 叶子结点存放的是地址 | 叶子结点的数据区域存储的是数据记录。辅助索引存储的是主键值。 |
MySQL索引
为什么索引能够提高查询速度?
索引:数据库索引,是数据库管理系统(DBMS)中一个排序的数据结构,以协助快速查询、更新数据库表中数据。
使用索引进行查询时,只需要在索引里面去检索这条数据就行了,因为它是一种特殊的专门用来快速检索的数据结构,我们找到数据存放的磁盘地址以后,就可以拿到数据了。
聚集索引和非聚集索引的区别?非聚集索引一定回表查询码?
聚集索引
聚集索引即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB中的主键索引就属于聚集索引。
优点:
- 查询速度非常快:B+树本身就是一颗多叉平衡树,叶子结点都是有序的,定位到索引的结点,就相当于定位到了数据。相比于非聚集索引,聚集索引少了一次读取数据的IO操作。
- 对排序查找和范围查找优化:聚集索引对于主键的排序查找和范围查找速度非常快。
缺点:
- 依赖于有序的数据:
- 更新代价大:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,
非聚集索引
非聚集索引即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚集索引。MySQL的MyISAM引擎,不管主键还是非主键,使用的都是非聚集索引。
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
优点:
更新代价比聚集索引要小。
缺点:
- 依赖于有序的数据:
- 可能会二次查询(回表):这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
非聚集索引一定回表查询吗?
非聚集索引不一定回表查询。
当SQL查询的字段恰好建立了索引,则会直接查找返回即可,无需回表查询。
为什么不对表中的每一列创建一个索引呢?(使用索引一定能提高查询性能吗?)
索引的缺点:
- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL的执行效率。
- 索引需要使用物理文件存储,也会耗费一定空间。
使用索引也不一定能提高查询效率
大多数情况下,索引查询都是比全表扫描要快的,但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大的提升。
索引底层的数据结构了解吗?Hash索引和B+数索引优劣分析
MySQL索引底层数据结构是B+树。
- B+树只有叶子结点存放key和data,其他内节点只存放key。
- B+树的叶子结点有一条引用链只想与它相邻的叶子结点。
- B+树的检索效率很稳定,任何查找都是从根节点到叶子结点的过程,叶子结点的顺序检索很明显。
- B+树的范围查询,只需要对链表进行遍历即可。
虽然MySQL中,MyISAM引擎和InnoDB引擎都是使用B+树作为索引结构,但是两者的实现方式不太一样。
- MyISAM引擎中:B+树的叶节点的data域存放的是数据记录的地址。(非聚集索引)
- InnoDB引擎中:B+树的叶节点的Data域保存了完整的数据记录。这个索引的key是数据表的主键。(聚集索引)不同的是使用辅助索引查找时,需要先取出主键的值,然后再走一遍主索引。
B+树做索引比红黑树好在哪里?
红黑树的缺点:和AVL树(自平衡二叉查找树)不同,红黑树并不追求严格的平衡,而是大致的平衡。正因如此,红黑树的查询效率稍有下降,因为红黑树的平衡性相对较弱,可能会导致树的高度教高,这可能会导致一些数据需要进行多次磁盘IO操作才能查询到。
红黑树的优点:红黑树的插入和删除操作效率大大提高了,因为红黑树再插入和删除节点时只需要进行O(1)次数的旋转和变色操作,即可保持基本平衡状态,而不需要向AVL树一样进行O(logn)次数的旋转操作。
最左前缀匹配原则了解吗?
最左匹配原则指的是:在使用联合索引时,MySQL会根据联合索引中的字段顺序,从左到右依次到查询中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如>
、<
)才会停止匹配。对于>=
、<=
、between
、like
前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多的数据。
什么是覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引。
覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。
MySQL进阶
一条SQL语句在MySQL中如何执行的?
explain
命令了解吗?
通过explain
的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、那些索引可以被命中、那些索引实际会命中、每个数据表有多少行记录被查询等信息。
需要注意的是explain
语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。
explain
执行计划支持select
、delete
、insert
、replace
以及update
语句。我们一般多用于分析select
查询语句,使用起来非常简单,语法如下:
1 | explain select 查询语句; |
1 | mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_emp GROUP BY emp_no HAVING COUNT(emp_no)>1); |
可以看到,执行计划结果中共有12列,各列代表的含义总结如下表:
列名 | 含义 |
---|---|
id | select查询的序列标识符 |
select_type | select关键字对应的查询类型 |
table | 用到的表名 |
partitions | 匹配的分区,对于未分区的表,值为NULL |
type | 表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际用到的索引 |
key_len | 所选的索引长度 |
ref | 当使用索引等值查询时,与索引作比较的列或常量 |
rows | 预计要读取的行数 |
filtered | 按表条件过滤后,留存的记录数的百分比 |
Extra | 附加信息 |
简单说一下SQL调优思路
简单说一下大表优化的思路
分库分表了解吗?为什么要分库分表?有哪些常见的分库分表工具?(sharding-jdbc
、TSharding
、MyCAT
…)
Redis
分布式缓存常见的技术选型方案有哪些?说一下Redis和Memcached的区别和共同点
说一下有缓存情况下查询数据和修改数据的流程
Redis有哪些数据结构?SDS了解吗?
Redis内存满了怎么办?
Redis内存淘汰算法除了LRU还有哪些?
Redis给缓存数据设置过期时间有啥用?Redis是如何判断数据是否过期的呢?
Redis事务了解吗?(Redis可以通过MULTI
、EXEC
、DISCARD
、WATCH
等命令来实现事务功能)
Redis批量操作的方式有哪些?
缓存穿透和缓存雪崩问题了解吗?有哪些解决办法?
如何基于Redis实现分布式锁?
什么是Sentinel?有什么用?
Sentinel如何检测节点是否下线?主观下线与客观下线的区别?
Sentinel是如何实现故障转移的?
Sentinel是如何选择出新的master(选举机制)?
如何从Sentinel集群中选择出Leader?
Sentinel可以防止脑裂吗?
为什么需要Redis Cluster?解决了什么问题?有什么优势?
Redis Cluster是如何分片的?
为什么Redis Cluster的哈希槽时16384个?
如何确定给定key的应该分布到哪个哈希槽中?
Redis Cluster支持重新分配哈希槽吗?
Redis Cluster扩容缩容期间可以提供服务吗?
Redis Cluster中的节点是怎么进行通信的?
ES
项目中用ES做了什么?ES可以帮助我们做什么?
Lucene是什么?为什么不直接用Lucene?
为什么用ES不用MySQL?(两者应用场景不同)
为什么用ES不用Hbase?(两者应用场景不同)
为什么ES检索比较快?倒排索引和正排索引是什么?倒排索引由什么组成?两者区别是什么?
分词器什么用?项目用的是什么分词器?如果我们要基于拼音搜索应该如何做?
项目中ES和MySQL的数据是如何进行同步的?
ES集群中的数据是如何被分配的(分片)?自定义路由有什么好处?
网络
网络分层模型
OSI与TCP/IP各层的结构与功能
OSI七层模型
OSI七层模型是国际标准化组织提出的一个网络分层模型。
结构 | 功能 |
---|---|
应用层 | 为计算机用户提供服务 |
表示层 | 数据处理(编解码、加密解密、压缩解压缩) |
会话层 | 管理(建立、维护、重连)应用程序之间的会话 |
传输层 | 为两台主机进程之间的通信提供通用的数据传输服务 |
网络层 | 路由和寻址(决定数据在网络的游走路径) |
数据链路层 | 帧编码和误差纠正控制 |
物理层 | 透明地传送比特流传输 |
TCP/IP四层模型
TCP/IP四层模型是目前被广泛采用的一种模型。
结构 | 功能 |
---|---|
应用层 | 由OSI七层模型中的应用层、表示层、会话层组成。功能也是三层功能之和 |
传输层 | 为两台主机进程之间的通信提供通用的数据传输服务 |
网络层 | 路由和寻址(决定数据在网络的游走路径) |
网络接口层 | 由OSI七层模型中的数据链路层和物理层组成。 |
为什么网络要分层?
复杂的系统需要分层,因为每一层都需要专注于一类事情。网络分层的原因也是一样,每一层只专注于做一类事情。
分层原因:
- 各层之间相互独立:各层之间不需要关注其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了。
- 提高了整体的灵活性:每一层都可以使用最合适的技术来实现,只需要保证提供的功能以及暴露的接口的规则没有改变就行了。
- 大问题化小:分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。
OSI与TCP/IP各层都有哪些协议?
应用层
- HTTP超文本传输协议:
- SMTP简单邮件发送协议:
- POP3/IMAP邮件接收协议:
- FTP文件传输协议:
- Telnet远程登陆协议:
- SSH安全的网络传输协议:
- RTP实施传输协议:
- DNS域名管理系统:
传输层
- TCP传输控制协议:提供面向连接的,可靠的数据传输服务。
- UDP用户数据协议:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性),简单高效。
网络层
- IP网际协议:
- ARP地址解析协议:
- ICMP互联网控制报文协议:
- NAT网络地址转换协议:
- OSPF开放式最短路径优先:
- RIP路由信息协议:
- BGP边界网关协议:
TCP与UDP
TCP的三次握手与四次挥手的内容?TCP为什么连接是三次握手而断开是四次握手?
三次握手
三次握手的目的是建立可靠的通信信道,最主要的目的就是双方确认自己与对方的发送和接收是正常的。
第一次握手:客户端发送带有SYN(SEQ=x)标志的数据包到服务端。然后客户端进入SYN_SEND状态,等待服务器的确认。
第二次握手:服务端发送带有SYN+ACK(SEQ=y,ACK=x+1)标志的数据包到客户端,然后服务端进入SYN_RECV状态。
传回ACK还要再传回SYN的原因:传回ACK只是为了告诉客户端从客户端到服务端的通信是正常的,回传SYN是为了建立并确定从服务端到客户端的通信是否正常。
第三次握手:客户端发送带有ACK(ACK=y+1)标志的数据包到服务端。然后客户端和服务端都进入ESTABLISHED状态,完成TCP三次握手。
当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!
四次挥手
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
- 第一次挥手:客户端发送一个FIN(SEQ=x)标志的数据包到服务端,用来关闭客户端到服务器的数据传送。然后客户端进入FIN-WAIT-1状态。
- 第二次挥手:服务器收到这个FIN(SEQ=x)标志的数据包,它发送一个ACK(ACK=x+1)标志的数据包到客户端。然后此时服务端进入CLOSE-WAIT状态。客户端进入FIN-WAIT-2状态。
- 第三次挥手:服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包到客户端请求关闭连接,然后,服务端进入 LAST-ACK 状态。
- 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包到服务端并且进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。
只要四次挥手没有结束,客户端和服务端就可以继续传输数据!
为什么要四次挥手?
TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
TCP与UDP的区别及使用场景
TCP与UDP的区别
TCP | UDP | |
---|---|---|
是否面向连接 | 是 | 否 |
是否可靠 | 是,在传递数据之前,会有三次握手来建立连接,而且数据传递时,有确认、窗口、重传、拥塞控制机制。可以保证数据无差错、不丢失、不重复、并且按序到达。 | 否,远地主机在收到UDP保温后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。 |
是否有状态 | 是 | 否 |
传输效率 | 较慢 | 较快 |
传输形式 | 字节流 | 数据报字段 |
首部开销 | 20~60 bytes | 8 bytes |
是否提供广播或多播服务 | 否,只支持点对点通信 | 是,支持一对一、一对多、多对一、多对多; |
TCP和UDP的使用场景
- UDP一般用于即时通信:比如语音、视频、直播等等。这些场景对传输数据的准确性要求不是特别高。
- TCP用于对传输准确性要求特别高的场景:比如文件传输、发送和接收邮件、远程登录等等。
TCP是如何保证传输的可靠性?❗
- 基于数据块传输:应用数据被分割成TCP认为最适合发送的数据块,再传输到网络层,数据块被称为报文段或段。
- 对失序数据包重新排序以及去重:
- 校验和:
- 超时重传:
- 流量控制:
- 拥塞控制:当网络拥塞是,减少数据的发送。
HTTP基于TCP还是UDP?
HTTP 是应用层协议,它以 TCP(传输层)作为底层协议。
HTTP
HTTP状态码有哪些?
类别 | 原因短语 | |
---|---|---|
1XX | 信息性状态码 | 接收的请求正在处理 |
2XX | 成功状态码 | 请求正常处理完毕 |
3XX | 重定向状态码 | 需要进行附加操作已完成请求 |
4XX | 客户端错误状态码 | 服务器无法处理请求 |
5XX | 服务器错误状态码 | 服务器处理请求出错 |
3XX 重定向状态码
- 301 Moved Permanently:资源被永久重定向了,比如网站的网址更换了。
- 302 Found:资源被临时重定向了。
4XX 客户端错误状态码
- 400 Bad Request:发送的HTTP请求存在问题,比如参数不合法、请求方法错误等。
- 401 Unauthorized:未认证却请求需要认证之后才能访问的资源。
- 403 Forbidden:直接拒绝HTTP请求,不处理,一般针对非法请求。
- 404 Not Found:你请求的资源未在服务端找到。
- 409 Conflict:表示请求的资源与服务端当前的状态存在冲突,请求无法被处理。
5XX 服务端错误状态码
- 500 Internal Server Error:服务端出问题了(通常是服务端出Bug了)。
- 502 Bad Gateway:我们的网关将请求转发到服务器,但是服务端返回的确是一个错误的相应。
一次完整的HTTP请求所经过的步骤
- DNS解析:浏览器向DNS服务器请求解析该URL中的域名所对应的IP地址。(通过URL得到IP地址)
- TCP连接:根据解析出IP地址与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。
- 发送HTTP请求:
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
HTTP协议了解吗?HTTP是基于TCP还是UDP的?
HTTP协议,全称超文本传输协议。因此,HTTP协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息。具体来说,主要是来规范浏览器和服务器端的行为。
HTTP是一个无状态协议。也就是说服务器不维护任何有关客户端过去所发请求的消息。
HTTP 是应用层协议,它以 TCP(传输层)作为底层协议。
HTTP报文的内容简单说一下!HTTP请求报文和响应报文中有哪些数据?
HTTP报文是面向文本的,因此在报文中的每一个字段都是一些ASCII码串。
HTTP请求报文
HTTP请求报文由3部分组成:请求行 + 请求头 + 请求体。
HTTP响应报文
HTTP响应报文由3部分组成:响应行 + 响应头 + 响应体。
HTTP和HTTPS的区别了解吗?
- 端口号:HTTP默认是80,HTTPS默认是443。
- URL前缀:HTTP的URL前缀是
http://
,HTTPS的URL前缀是https://
。 - 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
- SEO(搜索引擎优化)
HTTP/1.0和HTTP/1.1有什么区别?
- 连接方式:HTTP/1.0为短连接,HTTP/1.1支持长连接。
- 状态响应码:HTTP/1.1中新加入了大量的状态码。
- 缓存机制:在HTTP/1.0中主要使用Header里的if-Modified-Since,Expires来作为缓存判断的标准。HTTP/1.1则引入了更多的缓存控制策略,例如:Entity tag,If-Unmodified-Since,If-Match,If-None-Match等更多可供选择的缓存头来控制缓存策略。
- 带宽:HTTP/1.0 中,存在一些浪费带宽的现象。HTTP/1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- Host头(Host Header)处理:HTTP/1.1 引入了 Host 头字段,允许在同一 IP 地址上托管多个域名,从而支持虚拟主机的功能。而 HTTP/1.0 没有 Host 头字段,无法实现虚拟主机。
HTTP/1.1和HTTP/2.0有什么区别?
- IO多路复用
- 二进制帧
- 头部压缩
- 服务器推送
HTTP/2.0和HTTP/3.0有什么区别?
HTTP长连接和短连接了解吗?
Cookie和Seesion的关系
URI和URL的区别是什么?
- URI是统一资源标志符,可以唯一标识一个资源。
- URL是统一资源定位符,可以提供该资源的路径。它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
PING
PING命令的作用是什么?
PING命令是一种常用的网络诊断工具,经常用来测试网络中主机之间的连通性和网络延迟。
PING命令的输出结果通常包括以下几部分:
- 请求报文信息:序列号,TTL(Time To Live)值。
- 目标主机的域名或IP地址:输出结果的第一行。
- 往返时间RTT:从发送请求报文到接收到响应报文的总时间,用来衡量网络连接的延迟。
- 统计结果:包括发送的ICMP请求数据包数量、接收到的ICMP响应数据包数量、丢包率、往返时间的最小、平均、最大和标准偏差值。
1 | > ping www.baidu.com # 发送请求数据包到 www.baidu.com |
PING命令的工作原理是什么?
PING是基于网络层的ICMP(互联网控制报文协议),其主要原理就是通过在网络上发送和接收ICMP报文实现的。
ICMP报文包含了类型字段,用于标识ICMP报文类型。ICMP报文的类型有很多种,大致可以分为两类:
- 查询报文类型:向目标主机发送请求并期望得到响应。
- 差错报文类型:向源主机发送错误信息,用于报告网络中的错误情况。
PING用到的ICMP Echo Request(类型为8)和ICMP Echo Reply(类型为0)属于查询报文类型。
- PING命令会向目标主机发送ICMP Echo Request;
- 如果两个主机的连通性正常,目标主机会返回一个对应的ICMP Echo Reply。
IP
IP协议的作用是什么?
IP(网际协议)是TCP/IP协议中最重要的协议之一,属于网络层协议。
主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。
目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。
什么是IP地址?IP寻址如何工作?
IP地址
每个连入互联网的设备或域(如计算机、路由器、服务器等)都被分配一个IP地址,作为唯一标识符。每个IP地址都是一个字符序列。如 192.168.1.1(IPv4)。
IP寻址
当网络设备发送 IP 数据包时,数据包中包含了 源 IP 地址 和 目的 IP 地址 。源 IP 地址用于标识数据包的发送方设备或域,而目的 IP 地址则用于标识数据包的接收方设备或域。这类似于一封邮件中同时包含了目的地地址和回邮地址。
网络设备根据目的 IP 地址来判断数据包的目的地,并将数据包转发到正确的目的地网络或子网络,从而实现了设备间的通信。
IPv4和IPv6有什么区别?❗
IPv4是目前广泛使用的IP地址版本,其格式是由四组由点分隔的数字组成。大约由42亿个可用的IP地址。
IPv6 地址使用更复杂的格式,该格式使用由单或双冒号分隔的一组数字和字母,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334 。IPv6 使用 128 位互联网地址,这意味着越有 2^128(3 开头的 39 位数字,恐怖如斯) 个可用 IP 地址。
操作系统
进程和线程的区别?
- 进程:是指计算机中正在运行的一个程序实例。
- 线程:也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且专享进程的资源比如内存空间、文件句柄、网络连接等。
区别:
一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8之后的元空间)资源。但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
总结:
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
进程有哪几种状态?
进程大致分为5种状态:
- 创建状态(new):进程正在被创建,尚未到就绪状态。
- 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running):进程正在处理器上运行(单核CPU下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting):又称为等待状态,进程正在等待某一时间而暂定运行如等待某资源为可用或等待IO操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程间的通信方式
线程间的同步方式
- 互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。例如:
synchronized
关键词和各种Lock
- 读写锁:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量:允许同一时刻多个线程访问统一资源,但是需要控制统一时刻访问此资源的最大线程数量。
- 屏障:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。
- 事件:Wait/Notify,通过通知操作的方式来保持多线程同步,还可以方便实现多线程优先级的比较操作。
PCB
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。
进程的调度算法
- 先到先服务调度算法FCFS:从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 短作业优先调度算法SJF:从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 时间片轮转调度算法RR:时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级反馈队列调度算法MFQ:多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
- 优先级调度算法Priority:为每个进程分配优先级,首先执行具有最高优先级的进程,依此类推。
什么是死锁?死锁的四个必要条件?解决死锁的方法
多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止
四个必要条件
- 互斥:资源是非共享的,一次只能有一个进程可以使用。
- 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
- 非抢占:资源不能被抢占
- 循环等待:
注意 ⚠️:这四个条件是产生死锁的 必要条件 ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
常见的内存管理机制
内存碎片
分段机制和分页机制的区别和共同点
分段机制和分页机制下的地址翻译过程分别是怎样的
单级页表有什么问题?为什么需要多级页表?
TLB有什么用?使用TLB之后的地址翻译流程是怎样的?
页缺失,常见的页面置换算法有哪些?
硬链接和软链接有什么区别?
常见的磁盘调度算法有哪些?
算法和数据结构
算法
LRU算法了解吗?你能实现一个吗?
LRU算法又称最近最少使用算法,它的基本思想是长期不被使用的数据,在未来被用到的几率也不大,所以当新的数据进来时我们可以优先把这些数据替换掉。
1 | class LRUCache { |
写排序算法(快排、堆排)
快速排序
1 | void quickSort(int[] a, int l, int r) { |
归并排序
1 | void mergeSort(int[] a, int l, int r) { |