Java面试笔记
温馨提示:这篇文章已超过384天没有更新,请注意相关的内容是否还可用!
Java面试笔记
Java面试笔记-网络模块
TCP的三次握手
TCP的简介:
面向连接的、可靠的、基于字节流的传输层通信协议
将应用层的数据流分割成报文段并发送给目标节点的TCP层
数据包都有序号,对方收到则发送ACK确认,未收到则重传
使用校验和来检验数据在传输过程中是否有误
TCP报文头解读:
源端口(2字节)和目的端口(2字节):
注:TCP和UDP都不包含IP地址信息,因为IP地址信息是IP层的事情,但是TCP和UDP都包含源端口和目的端口.
不同进程在计算机内的通信方式:管道、内存共享、信号量、消息队列.
两个进程能够通信的最重要因素是:进程要有唯一标识,这样才能找到对应的进程。
在本地通信可以使用PID作为进程之间的唯一标识符.
在不同计算机之间的进程进行通信可以使用端口号。IP地址可以唯一标识不同的计算机,TCP协议和端口号可以唯一标识不同主机的一个进程。
所以在网络中标识唯一进程可以使用:协议使用:IP地址+端口号作为唯一标识. 也叫socket套接字。
所以:-----------------------区分不同应用程序进程间的网络通信和连接,主要有3个参数:
通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。
序列号Sequence Number (Seq)(4字节):
表示传输字节流的顺序,如果当前报文的Seq是107,当前报文段包含100个字节,那么下一个报文段的Seq就是207 。
Acknowledgment Number ACK确认码 (4字节):
如果接收方的计算机收到了发送方的上一个报文段的第一个数据的序列号是201,报文段包含的数据总长度为300字节,那么在接收方的计算机发送给发送方的TCP报文头中就会把ACK置为501,所以总结为:ACK代表了接收方期望收到的下一个报文段的数据起始序列号。
Offset 偏移量 (4bits,即半个字节):
由于报文头的内容有可选字段,因此大小不固定,所以offset指出报文段包含的数据距整个报文开始的地方有多远,即整个报文头的大小。
Reserved 保留域 (4bits):
保留以后有特殊情况使用的,一般被置为0.
TCP Flags TCP控制位 (1字节):
有八个控制位组成,但主要的就六个
URG: 紧急指针标志. 1有效, 0忽略。
ACK * : 确认序号标志 1有效,0忽略。
PSH: push标志 1:通知接收方尽快将报文段的数据交给应用程序而不是在缓冲区排队,0忽略。
RST: 重置连接标志. 用于重置由于主机崩溃而产生错误的的连接,或者用于拒绝非法的请求。
SYN * : 同步序号,用于建立连接过程 1有效,0忽略。
FIN * : finish标志,用于释放连接 1告诉接收方发送方已经没有数据再发送,即关闭发送方数据流。
Window 滑动窗口 (2字节):
用以告诉发送端接收端缓存的大小,以此控制发送端发送数据的速率。
Checksum 奇偶校验 (2字节):
进行奇偶和校验,有发送端发送和存储,由接收端校验是否准确。
Urgent Pointer 紧急指针 (2字节):
只有当 TCP Flags 中的 URG 为1时才有效,指出本报文段中的紧急数据的字节数。
TCP Options TCP可选项:
用于定义TCP协议中的一些可选参数。长度不固定。
TCP的三次握手流程详解:
以下C代表客户端,S代表服务器端:
在通信之前:C和S都处于CLOSED关闭状态.
假设主动打开连接进程的是客户端C,被动打开连接的是服务端S。
S: 首先服务器的TCP进程先创建传输控制块TCB,时刻准备接收其他客户端发送的请求,此时服务端进入了LISTEN监听的状态。
--------------第一次握手--------------
C: 之后客户端的TCP进程也创建一个TCB传输控制块,并发送一个连接请求报文,此报文的 TCP Flags 中的同步序列号SYN设为1,同时选择一个序列号seq为x, x可为任意正整数。之后进入SYN-SENT同步已发送的状态。这次发送的报文段不会包含任何的数据,被称为SYN报文段,但是要消耗掉一个seq序号。
--------------第二次握手--------------
S: 服务器收到客户端发送的请求后,如果同意连接则发送一个确认报文,确认报文中的 TCP Flags 中的SYN和ACK都设为1,同时设置一个自己的序列号y,以及返回一个期望序列号ack = x+1。之后服务器会进入SYN-RCVD同步接收的状态。这次发送的报文段不会包含任何的数据,但是要消耗掉一个seq序号。
--------------第三次握手--------------
C: 客户端收到服务端发来的同意建立连接的报文后,会再向服务器发送一个确认报文,此报文的 TCP Flags 中的 ACK设为1,同时序列号seq为x+1,同时设期望序列号ack为y+1.这次发送的报文段可以选择包含或者不包含数据,如果不包含数据则不消耗一个序列号,如果包含数据要消耗掉一个seq序号。
之后客户端和服务器建立连接,客户端和服务器都进入ESTABLISHED状态。
TCP三次握手的相关问题:
- 为什么需要三次握手才能建立起连接: 为了初始化Sequence Number 的初始值。
- 首次握手的隐患—SYN超时: Server收到Client的SYN,回复SYN-ACK的时候未收到ACK确认 Server不断重试直至超时,Linux默认等待63秒才断开连接
Linux默认尝试五次重连,每次等待时间翻倍,所以第一次发送等待1秒后,再发送等待2秒一直到最后一次等待32秒后重连,一共发送6次SYN-ACK请求,等待63秒。
所以有可能造成SYN Flood攻击,即每次客户端发送请求后就下线,而服务器需要等待63秒,使用大量的客户端可以在短时间内让服务器的SYN队列被填满。
针对SYN Flood的防护措施:
SYN队列满后,通过tcp_syncookies参数回发SYN Cookie
若为正常连接则Client会回发SYN Cookie,直接建立连接
- 建立连接后,Client出现故障怎么办:
保活机制
向对方发送保活探测报文,如果未收到响应则继续发送
尝试次数达到保活探测数仍未收到响应则中断连接。
TCP的四次挥手
第一次挥手:
Client 发送一个 FIN,用来关闭 Client 到 Server的数据传送,Client 进入 FIN WAIT_1状态
第二次挥手:
Server 收到 FIN 后,发送一个 ACK 给 Client,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态
第三次挥手:Server 发送一个 FIN,用来关闭 Server 到 Client的数据传送,Server 进入 LAST_ACK 状态;
第四次挥手:Client 收到 FIN 后,Client 进入 TIMEWAIT状态,接着发送一个ACK 给 Server,确认序号为收到序号+1,Server 进入 CLOSED 状态,完成四次挥手。
TCP四次挥手相关的问题:
- 为什么会有TIME_WAIT状态:
确保有足够的时间让对方收到ACK包 避免新旧连接混淆
- 为什么需要四次握手才能断开连接:
因为全双工,发送方和接收方都需要FIN报文和ACK报文。
- 服务器出现大量CLOSE WAIT状态的原因:
对方关闭socket连接,我方忙于读或写,没有及时关闭连接
检查代码,特别是释放资源的代码 检查配置,特别是处理请求的线程配置。
全双工:双向同步通信。
一次tcp连接。从左到右分别为:第几次抓报的序号,抓包的时间,源IP地址,目的IP地址,协议,长度,信息。
其中信息的第一个53627 -> 80 为两边的端口号。
注:ACK确认报文不消耗Seq序列号,第四次和第六次包含了HTTP协议的TCP通信仍然序列号为1。
UDP和TCP的区别
UDP的报文结构:
Source Port: 源端口
Destination Port: 目标端口
Length: 数据包长度
Checksum: 奇偶校验值
data: 用户数据。
UDP的特点:
面向非连接
不维护连接状态,支持同时向多个客户端传输相同的消息
数据包报头只有8个字节,额外开销较小
吞吐量只受限于数据生成速率、传输速率以及机器性能
尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表
面向报文,不对应用程序提交的报文信息进行拆分或者合并
结论: UDP和TCP的区别
面向连接vs无连接
可靠性
有序性
速度
量级 TCP20个字节,UDP8个字节。
TCP的滑动窗口详解
RTT: 发送一个数据包到收到对应的ACK,所花费的时间
RTO: 重传时间间隔 — RTO并非固定数值,而是基于RTT计算得到的时间。
TCP使用滑动窗口做流量控制与乱序重排
保证TCP的可靠性
保证TCP的流控特性
TCP的window字段:
用以告诉发送端接收端缓存的大小,以此控制发送端发送数据的速率。
TCP滑动窗口的计算过程:
从最左边开始:
发送端
LastByteAcked是最新的已被服务器确认的数据序列号.
LastByteSent是最新的已经发送的数据.
LastByteWritten是最新的已经被写完的数据,即在发送端的缓存中随时能发送的数据
接收端
LastByteRead是最新的已经被服务端读取的数据,即已经从缓存中读出,不需要占用缓存空间。
NextByteExpected是最新的连续的已经接收到的数据。
LastByteRcvd是实际的已经接收到的最大的数据序列号,中间有空白是因为数据不一定按顺序到达,有可能后面的数据包先到达了。
滑动窗口的计算公式:
AdvertisedWindow(接收端发送给发送端告知自己还有多少缓存的滑动窗口值) = MaxRcvBuffer - (LastByteRcvd - LastByteRead)
EffectiveWindow(发送端发送给接收端告知接收端实际还有多少缓存的滑动窗口值) = AdvertisedWindow - (LastByteSent - LastBvteAcked)
滑动窗口的滑动原理:
发送方:
滑动窗口由category2和category3组成。
接收方:
滑动窗口由3组成。
HTTP简介
超文本传输协议HTTP主要特点:
支持客户/服务器模式:客户端通过url想服务端发送请求信息,服务端发送相应信息给客户端。
简单快速: 发送请求方法的时候只需发送请求的方法和路径, 请求的方法有get/post。程序规模小,传输速度快。
灵活:传输的数据类型多,使用content-type进行标记。
无连接: 每次连接只处理一个请求,服务器处理完客户的请求并收到客户的应答之后就断开连接,节省传输时间。http 1.1 版本之后默认使用长连接,在客户端应答之后等待一段时间才断开连接。
关于http的长连接理解:HTTP的长连接详解
无状态:协议对于事务处理没有记忆能力,如果处理的事务需要前面的信息则必须要重传。
HTTP请求结构:
请求行:GET /myo2o/local/login?usertype=2 HTTP/1.1\r\n
请求头:从Host到Cookie。
请求正文:[Full request URI:http://myo2o.yitiaojieinfo.com/myo2o/local/login?usertype=2 [HTTP request 1/6] [Response in frame: 2501 [Next request in frame: 252]
HTTP 响应结构:
HTTP 请求/响应的步骤:
客户端连接到Web服务器
发送HTTP请求 – 通过 TCP 套接字,客户端向服务器发送一个文本的请求报文。
服务器接受请求并返回HTTP响应 ,Web服务器解析该请求,定位请求资源,服务器将资源副本写到TCP套接字,由客户端读取。
释放连接TCP连接,如果连接模式是close (http1.0版本),则服务器主动关闭连接,客户端被动关闭连接,释放TCP连接。
如果连接模式是keep-alive (http1.1版本),则该连接会保持一段时间,期间会继续接收请求。
客户端浏览器解析HTML内容,客户端首先读取响应的状态码,如果成功则解析HTML文本并在浏览器窗口显示。
在浏览器窗口输入URL之后,按下回车后的经历:
DNS解析 --浏览器会根据URL逐层查询DNS服务器缓存,解析URL中对应的IP地址; DNS缓存从近到远分别是:浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存。从哪个服务器缓存找到对应的IP地址则直接返回。
TCP连接 --有了IP地址则可以根据对应的端口和服务器建立连接。
发送HTTP请求
服务器处理请求并返回HTTP报文
浏览器解析渲染页面
连接结束
HTTP 状态码:
1xx:指示信息–表示请求已接收,继续处理
2xx:成功–表示请求已被成功接收、理解、接受
3xx:重定向–要完成请求必须进行更进一步的操作
4xx:客户端错误–请求有语法错误或请求无法实现
5xx:服务器端错误–服务器未能实现合法的请求
GET请求和POST请求的区别
Http报文层面:GET将请求信息放在URL,POST放在报文体中
数据库层面:CET符合幂等性(幂等性:对数据库的一次操作和多次操作得到的结果一样)和安全性(只进行读操作,不改变数据库中的值),POST不符合
其他层面:GET可以被缓存、被存储,而POST不行
Cookie和Session的区别
Cookie:
是由服务器发给客户端的特殊信息,以文本的形式存放在客户端
客户端再次请求的时候,会把Cookie回发
服务器接收到后,会解析Cookie生成与客户端相对应的内容
Cookie的设置以及发送过程:
Session:
服务器端的机制,在服务器上保存的信息
解析客户端请求并操作sessionid,按需保存状态信息
Session的实现方式: 使用Cookie来实现。
使用URL回写来实现。
两者的区别:
Cookie数据存放在客户的浏览器上,Session数据放在服务器上
Session相对于Cookie更安全
若考虑减轻服务器负担,应当使用Cookie
HTTP和HTTPS的区别
SSL(Security Sockets Layer,安全套接层)简介:
为网络通信提供安全及数据完整性的一种安全协议
是操作系统对外的API,SSL3.0后更名为TLS
采用身份验证和数据加密保证网络通信的安全和数据的完整性
HTTPS数据传输流程:
在TCP连接建立之后:
浏览器将支持的加密算法信息发送给服务器
服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器
浏览器验证证书合法性,并结合证书公钥加密信息发送给服务器
服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
浏览器解密响应消息,并对消息进行验真,之后进行加密交互数据
注:所以原来所需要的3次连接之后发送消息,变成了3次连接+4次加密之后发送消息。
详细版本:
1.客户端发起https请求,服务端返回数字证书和公钥,服务端保留私钥(同时发送tls版本和支持的加密算法);
2.客户端收到相应后,对数字证书进行校验,通过的话本地生成一个随机数,这个随机数就是以后传输内容对称加密使用到的密钥,然后用公钥加密后发送给服务端;
3.服务端接收后用自己的私钥进行非对称解密,拿到客户端的随机数;
4.服务端将双方协定的对称密钥和加密算法发送给客户端,至此tls建立连接
HTTP和HTTPS的区别:
HTTPS需要到CA申请证书,HTTP不需要
HTTPS密文传输,HTTP明文传输
连接方式不同,HTTPS默认使用443端口,HTTP使用80端口
HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全
Socket简介
Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口
Scoket通信流程:
Java面试笔记-数据库模块
如何设计一个关系型数据库:
存储:就像os文件系统,将数据最终持久化存入磁盘中
存储管理:把逻辑存储最终映射到物理存储中,并且实现性能高效,及尽量少的做io,以为一次读一行的io和一次读多行的io消耗的性能基本差不多,所以每次读取尽量读多行或者块。
缓存机制:多读出来的数据放入缓存中,方便下次快速查找。
索引简介
为什么要使用索引
快速查询数据
什么样的信息能成为索引
主键、唯一键以及普通键等
索引的数据结构
生成索引,建立二又查找树进行二分查找
生成索引,建立B-Tree结构进行查找
生成索引,建立B±Tree结构进行查找
生成索引,建立Hash结构进行查找
密集索引和稀疏索引的区别
时间复杂度快速分析
大多数情况下,一次循环就是O(n)
双重循环 O(n^2)
三重循环 O(n^3)
二分O(logn)
有序数组查找某一个数O(logn)
需要一次排序O(nlogn)
能折半的就是O(logn)
模拟一个测试数据带进去,看看经历了几次循环,或者几次折半,看看一个100大小的数组,进去要算几次,是100次还是10000次. 如果运算的次数和数据量的趋势相同:比如数组的大小为10的运算10次,大小为100的运算100次,就是O(n). 如果大小为10的运算100次,大小为100的运算10000次,则是O(n^2)。
数据结构基础
相关考点:
数组和链表的区别;
链表的操作,如反转,链表环路检测,双向链表,循环链表相关操作;
队列,栈的应用;
二又树的遍历方式及其递归和非递归的实现;
红黑树的旋转;
内部排序:如递归排序、交换排序(冒泡、快排)、选择排序、插入排序;
外部排序:应掌握如何利用有限的内存配合海量的外部存储来处理超大的数据集,写不出来也要有相关的思路。
哪些排序是不稳定的,稳定意味着什么, 快排,堆排序不稳定
不同数据集,各种排序最好或最差的情况
如何优化算法
Java集合框架简介
集合的源码大都集中在java.util这个包下面。
注:HashSet是由HashMap实现的。
TreeSet底层是由NavigableMap实现的,NavigableMap是由TreeMap实现的.
Map详解:
Map的key是通过Set实现的,在Map中的keySet()方法返回一个Set集合,所以可以去重.
Map的value是通过Collection实现的,所以允许重复。
HashMap(Java8 以前):数组+链表,数组查询快,删改慢;链表删改快,查询慢.,并且HashMap是线程不安全的,所以效率比较高。
缺点:如果连续发生哈希碰撞,则会使一个链表不停的增长,从而使性能从O(1)变为O(n)。
HashMap(Java8 及以后):数组+链表+红黑树。
连续发生哈希碰撞的情况性能优化为:性能从O(n)提高到O(logn)。
HashMap:从获取hash到散列的过程
HashMap、HashTable、 ConccurentHashMap的区别:
HashMap是采用的lazy load在首次使用的时候才会初始化:
HashMap:put方法的逻辑:
1、若HashMap未被初始化,则进行初始化操作
2、对Key求Hash值,依据Hash值计算下标;
3、若未发生碰撞,则直接放入桶中;
4、若发生碰撞,则以链表的方式链接到后面
5、若链表长度超过闻值,且HashMap元素超过最低树化容量,则将链表转成红黑树,
6、若节点已经存在,则用新值替换旧值
7、若桶满了(默认容量16*扩容因子0.75),就需要resize(打容2倍后重排);
TREEIFY_THRESHOLD=8,MIN TREEIFY CAPACITY=64
超过8开始调用resize扩容,超过64进行树化。
HashMap:如何有效减少碰撞
扰动函数: 促使元素位置分布均匀,减少碰撞机率
使用final对象: 并采用合适的equals()和hashCode()方法。因为final对象有不可变性,比如String和Integer对象,而hashmap要求生成的键值和取出时的键值相等,所以需要使用final对象。
HashMap扩容的问题:
多线程环境下,调整大小会存在条件竞争,容易造成死锁
rehashing是一个比较耗时的过程
HashTable 简介:
早期Java类库提供的哈希表的实现
线程安全:涉及到修改Hashtable的方法,使用synchronized修饰
串行化的方式运行,性能较差
ConcurrentHashMap: 简介
CAS+synchronized使锁更细化
在ConcurrentHashMap中,synchronized只锁定当前链表或者红黑树的首节点,因此只要不发生哈希冲突就不会造成线程堵塞。
ConcurrentHashMap不允许键或值为空,HashMap可以。
ConcurrentHashMap:put方法的逻辑
- 判断Nodel数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环
- 检查到内部正在扩容,就帮助它一块扩容
- 如果f!=null,则使用synchronized锁住f元素(链表/红黑二又树的头元素)
4.1如果是Node(链表结构)则执行链表的添加操作
4.2如果是TreeNode(树型结构)则执行树添加操作
- 判断链表长度已经达到临界值8,当然这个8是默认值,大家也可以去做调整,当节点数超过这个值就需要把链表转换为树结构
ConcurrentHashMap总结:
Segment,锁拆得更细
首先使用无锁操作CAS插入头节点,失败则循环重试
若头节点已存在,则尝试获取头节点的同步锁,再进行操作
HashMap、HashTable、 ConccurentHashMap的区别总结:
HashMap线程不安全,数组+链表+红黑树
Hashtable线程安全,锁住整个对象,数组+链表
ConccurentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
二叉树
一般考点为二叉搜索树,即左边的子节点要小于根节点,右边的子节点要大于根节点。
二叉树实现(python):
此为二叉搜索树
class Node:
def __init__(self, key):
self.left = None
self.right = None
self.val = key
def insert(root, key):
if root is None:
return Node(key)
else:
if root.val
二叉平衡树(AVL):
一个空树或者一个树的两个子节点的高度差不超过1.
不满足二叉平衡树的时候会进行左旋或者右旋。
左旋
本质就是,创建一个新的节点作为根节点的左子树,然后抛弃掉原本的右子树,让原本右子树的右子树成为新的右子树。
图解左旋:
右旋思路同理。
B 树
B树的特征:
根节点至少包括两个孩子。
树中每个节点最多含有m(m为B树的阶数,如上图就是一个3阶B树)个孩子(m>=2)。
除根节点和叶节点外,其他每个节点至少有ceil(m/2)(ceil为取上值函数,如1.2用ceil函数得到2)个孩子
所有叶子节点都位于同一层。
假设每个非终端结点中包含有 n 个关键字信息(关键字就是所带的值),其中:
a) Ki (i=1…n)为关键字,且关键字按顺序升序排序 K(i-1)
b) 关键字的个数 n必须满足: [ceil(m /2)-1]eq_ref>ref>fulltext>ref_or_null>index merge>unique_subquery>index subquery>range>index>all
# extra:需要优化的参数见下图
3.修改sql或者尽量让sql走索引 # 修改sql,查找有没有合适的索引,让查询参数查询带索引的关键字。 #原sql使用name字段,不是索引关键字。 select name from person_info_large order by name desc; #查找建表语句后发现account字段有索引,修改后的sql如下。 select account from person info_large order by account desc; # 为查询的字段添加索引。 alter table person_info_large add index idx_name(name); #通过强制使用某个索引,测试使用不同索引的查询时间 explain select info larae force index (primary);
最左匹配原则
1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、between、like)就停止匹配,比如a = 3 and b = 4 and c > 5 and d = 6如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
2.=和in可以乱序,比如a = and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式.
注:B+树的联合索引是按索引顺序查找的所以当(a,b,d,c)的联合索引使用的时候,会先去查找a,根据a的结果排序,再在里面找b,然后找c以此类推。跟输入sql查询语句顺序无关。
索引是建立得越多越好吗
数据量小的表不需要建立索引,建立会增加额外的索引开销
数据变更需要维护索引,因此更多的索引意味着更多的维护成本
更多的索引意味着也需要更多的空间
锁简介
MyISAM与InnoDB关于锁方面的区别是什么
MyISAM默认用的是表级锁,不支持行级锁
InnoDB默认用的是行级锁,也支持表级锁
MyISAM引擎在读取数据的时候会对整张表上读锁(共享锁),此时如果在进行读取操作则因为共享锁的缘故可以进行同时操作。但是如果要进行写操作则要等到读取操作完成再进行增删改。
如果事先进行写操作则会对整张表上写锁(排它锁),此时无论是进查询还是增删改都要等到写操作完成,才能继续进行。
InnoDB引擎的情况和MyISAM类似,但是上的读锁和写锁都是行级锁,当前行的锁不影响其他行的读写的操作。
但是当执行没有索引的sql语句的时候InnoDB引擎会走表级锁。如果是sql语句执行普通,非唯一索引的时候,会使用gap锁。
MyISAM适合的场景:
频繁执行全表count语句,MyISAM会使用一个属性值记录整表的记录数。
对数据进行增删改的频率不高,查询非常频繁
没有事务
InnoDB适合的场景:
数据增删改查都相当频繁
可靠性要求比较高,要求支持事务
数据库锁的分类:
按锁的粒度划分,可分为表级锁、行级锁、页级锁
按锁级别划分,可分为共享锁、排它锁
按加锁方式划分,可分为自动锁、显式锁:
自动锁即执行sql语句的时候引擎自动添加的锁;
显式锁即在写sql语句时加上Select … for Share,或者 select … for update(共享锁,排它锁)。
按操作划分,可分为DML锁(对数据进行增删改查的锁)、DDL锁(对表结构改变的锁,如增加字段索引等)
锁按使用方式划分,可分为乐观锁、悲观锁:
悲观锁:全程不信任操作,所有操作将数据处于锁定状态;如数据库引擎的各种共享锁和排它锁,以及synchronized关键字。先取锁,再访问,增加系统负担,可能造成死锁。
乐观锁:一般指在数据提交时再对版本进行检查,数据处理的全程信任别的事务,一般使用时间戳或者版本号实现。比如在数据库中加入一个版本号字段。
#1.先读取test_innodb的数据,得到Version的值为versionValue select version from test innodb where id =2;#0 #2每次更新test_innodb表中的money字段时候,为了防止发生冲突,先去检查version再做更新#更新成功的话version +1 update test innodb set money = 123, version = 0 + 1 where version = 0 and id = 2; #3假设在执行更新语句前,先执行了下面的更新操作。 update test innodb set money = 345, version = 0 + 1 where version = 0 and id = 2; #那么在执行第一个更新语句就会更新失败,即返回的更新行数为0. 这时候可以根据程序中返回的count值对前端传输错误信息,提示数据更新失败。 update test innodb set money = 123, version = 0 + 1 where version = 0 and id = 2; #这种手动提交时对比版本号的锁就是实现乐观锁的一种。
数据库事物的四大特性
ACID
原子性( Atomic)
所有操作要么全都失败回滚,要么全都成功执行
一致性( Consistency)
数据库数据的完整性约束,比如一个人A给另外一个人B转账,转账前后的A+B应该相等。
隔离性 ( Isolation )
多个事务并发执行的时候,一个事物的执行不应该影响另一个事物的执行。
持久性( Durability)
一个数据的修改一旦提交,应该永久保存在数据库中。当数据库发生故障时,能对已提交事物的数据恢复。如: InnoDB会把对数据库的所有操作写入一个文件,在数据库发生故障的时候,可以通过这个文件恢复数据库的操作(redo_log file)。
事务隔离级别以及各级别下的并发访问问题:
更新丢失一mysql所有事务隔离级别在数据库层面上均可避免:
A事务和B事务同时开启查询,查到一个数据,B事务对数据修改并提交,A事务还用原来的数据进行更新,结果提交时因为更新失败而回滚。
脏读-READ-COMMITTED事务隔离级别以上可避免:
当前事务读取了其他事物还未提交的数据,比如B事务更新了一个数据,A实物查询到了这个数据,这时候B事务因为一些原因而更新失败回滚,A使用了B还未提交的数据进行更新并提交,得到了错误的新数据。
不可重复读-REPEATABLE-READ事务隔离级别以上可避免:
即在当前事务每次读取数据库的数据都有可能不一样,因为在读取完成后更新可能有别的事务更新了这个数据。
幻读SERIALIZABLE事务隔离级别可避免:
即A事务读取数据,发现只有4条,想要对这4条数据进行更新,在这之前B事务往数据库中更新了一条数据,这时候A事务对数据进行更新发现更新了5条数据。这就是幻读。
当前读和快照读:
当前读 :select…lock in share mode,select…for update
当前读:update,delete,insert
为什么update也是当前读:下图可以看出在执行mysql的update语句时,会先执行一次current read当前读来读取当前的最新数据。
快照读:不加锁的非阻塞读,select,在事务隔离级别不为SERIALIZABLE的前提下才成立的。在事务隔离级别SERIALIZABLE下所有的读都是串行读,所以即使快照读也退化成当前读。
RC、RR级别下的InnoDB的非阻塞读如何实现:
在InnoDB引擎,每行除了存储数据外,还额外有几个字段。比较关键的:
注:使用delete的时候,数据行会有专门的delete字段,这个字段设为deleted就是删除数据,并非真正的删除数据。 数据行里的DB_TRX
ID、DB ROLL_PTR、DB_ROW_ID字段。 DB_TRX ID:最后一次修改本行记录的事务ID。 DB
ROLL_PTR:回滚日志指针,记录回滚日志的信息。
DB_ROW_ID:如果数据表没有主键索引也没有唯一非空字段,那么数据库就使用这个隐藏字段作为自增密集索引。
undo日志:有两种类型insert undo log 和update undo log : insert undo
log:事务对insert新记录产生的undo log,旨在对食物回滚时需要,在事务提交后立即丢弃。 update undo
log:事务对记录进行delete或者update产生的undo
log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除。只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被删除。
回滚日志的实现步骤:
首先,在更新数据时,首先用排他锁锁住这一行,然后将当前行的值拷贝一份到undo
log中,然后修改当前行的数据,之后填写事务ID,再用回滚指针指向修改前undo log的行。
此时如果有别的事务在用快照读读取该行数据记录,那么对应的undo
log还没有被清除,此时又有一个事务对该行数据做了修改。那么就又多了一条undo log记录,这样数据就有多个版本。
read view 可见性判断:
当进行快照读select的时候,会根据查询的数据创建一个read view来决定当前事务能看到的是哪个版本的数据。read view遵循一个可见性算法,把当前的数据DB_TRX_ID取出来与其他系统活跃的事务(比如当前事务)ID作对比,如果数据的DB_TRX_ID大于或等于这些活跃事物的ID的话,就去取出undo log中的数据。(事务ID是累加的,越新ID越大)。
在RC级别下:事务中每个select语句都会创建一个新的快照,所以总能读到最新的数据版本。
在RR级别下:session会在transaction start之后的第一个select快照读后创建快照,即read view,将当前系统其他活跃的事务记录起来,此后在事务提交前执行的所有快照读都会使用这个rea view,所以即使有事务对数据做了增删改也有可能读不到。
InnoDB可重复读隔离级别下如何避免幻读:
表象:快照读(非阻塞读)–伪MVCC
内在:next-key锁(行锁+gap锁)
行锁:Record Lock.
gap锁:会锁住记录周围的几条记录;
如果where条件全部命中,则不会用Gap锁,只会加记录锁
如果where条件部分命中或者全不命中,则会加Gap锁
非唯一索引,会用Gap锁;不走索引,也会用Gap锁。
Redis相关知识
主流应用架构:
穿透查询:缓存层没有的数据,直接去存储层查询。
回种:把存储层的数据写到缓存层,方便下次后端直接查找。
熔断机制:当存储层出现故障时,可以直接让客户端所有的请求都访问缓存层,如果缓存层没有则直接返回相关信息。避免所有服务都不可用。
缓存中间件:Memcache和Redis的区别
Memcache:代码层次类似Hash
支持简单数据类型
不支持数据持久化存储
不支持主从
不支持分片
Redis:
数据类型丰富
支持数据磁盘持久化存储
支持主从
支持分片
为什么Redis能这么快:
100000+QPS(QPS即query per second,每秒内查询次数)
完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高
数据结构简单,对数据操作也简单,存储结构为键值对,类似于hashmap,查找和操作都是O(1)。
采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
使用多路I/O复用模型,非阻塞IO
多路I/O复用模型:
File Descripter 文件描述符:
一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射
传统的阻塞I/O模型:
多路I/O复用模型:
Selector可以监听不同文件描述符的可读/可写状态,当有文件描述符在可读/可写状态时,select就会返回文件描述符可读/可写的个数。
Redis采用的I/O多路复用函数:epoll/kqueue/evport/select ?
因地制宜
优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现
以时间复杂度为O(n)的select作为保底
基于react设计模式监听I/O事件
Redis的数据类型:
String:最基本的数据类型,二进制安全
Hash:String元素组成的字典,适合用于存储对象
List:列表,按照String元素插入顺序排序, 和堆栈一样后进先出。
Set:String元素组成的无序集合,通过哈希表实现,不允许重复
Sorted Set:通过分数来为集合中的成员进行从小到大的排序
用于计数的HyperLogLog,用于支持存储地理位置信息的Geo
底层数据类型基础(非重点):
- 简单动态字符串
- 链表
- 字典
- 跳跃表
- 整数集合
- 压缩列表
- 对象
面试考点:从海量Key里查询出某一固定前缀的Key
KEYS pattern: 查找所有符合给定模式pattern的key 数据量小:
比如返回所有带k1前缀的key:keys k1*
KEYS指令一次性返回所有匹配的key
键的数量过大会使服务卡顿
SCAN cursor [MATCH pattern] [COUNT count] 数据量大:
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
不保证每次执行都返回某个给定数量的元素,支持模糊查询
一次返回的数量不可控,只能是大概率符合count参数
比如:scan 0 match k1* count 10
不一定会返回count规定的数值,有概率获得相同的key,所以需要在外部做hashset去重。
如何通过Redis实现分布式锁
分布式锁:多个系统或不同系统之间共同访问共享资源的一种锁的实现,如果多个系统或者同个系统的不同主机之间共享了某个资源时,往往需要互斥以防止彼此干扰,保证一致性。
分布式锁需要解决的问题:
互斥性:同一时间,只能有一个客户端访问共享资源。
安全性:所只能由访问的客户端删除。
死锁:由于正在访问的客户端宕机而无法释放锁,导致的死锁。此时需要有一些机制来预防这种情况发生。
容错:如果有一些redis节点宕机,需要保证客户端能正常获取锁和释放锁。
实现锁的步骤:
实现的逻辑: 每次有客户端想调用共享资源的时候先执行setnx创建并设置key,此时如果有别的线程正在占用,那么key就会存在,设置就会失败,如果没有线程占用,那么key不存在可以执行共享资源,在访问完成后,设置key的自动过期时间,所以锁会自动释放。
没有满足原子性的解法:
SETNX key value: 如果key不存在,则创建并赋值
时间复杂度: O(1)
返回值: 设置成功,返回1; 设置失败,返回0
如何解决SETNX长期有效的问题:
EXPIRE key seconds 设置key的生存时间,当key过期时(生存时间为0),会被自动删除
实现锁伪代码:
#缺点:原子性得不到满足
#比如在执行完setnx后服务直接挂掉了,那么锁就不会被释放。
RedisService redisService = SpringUtils.getBean(RedisService.class); long status =
redisService.setnx(key,"1");
if(status == 1) { redisService.expire(key,expire); //执行独占资源逻辑
doOcuppiedwork() }
满足原子性的解法:
SET key value[EX seconds] [PX milliseconds] [NX|XX]
EX second:设置键的过期时间为 second 秒
PX millisecond:设置键的过期时间为 millisecond 毫秒
NX:只在键不存在时,才对键进行设置操作
XX:只在键已经存在时,才对键进行设置操作SET操作成功完成时,返OK,否则返回nil
比如:set Locktarget 12345 ex 10 nx
RedisService redisService = SpringUtils.getBean(RedisService class);
String result = redisService.set(lockkey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if ("OK".equals(result)) (
//执行独占资源逻辑
doOcuppiedWork()
}
大量的key同时过期怎么办:
集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
解放方案:在设置key的过期时间的时候,给每个key加上随机值
如何使用redis实现异步队列:
异步队列定义:异步队列允许多个生产者同时向队列中添加消息,而不需要等待消费者的响应,因为消费者可能会异步地加入并开始处理这些消息。这意味着生产者和消费者可以在不同的时刻进行交互,从而提高了系统的吞吐量。
使用List作为队列,RPUSH生产消息,LPOP消费消息
缺点:没有等待队列里有值就直接消费
弥补:可以通过在应用层引入Sleep机制去调用LPOP重试
BLPOP key [key …] timeout: 阻塞直到队列有消息或者超时
缺点:只能供一个消费者消费
pub/sub:主题订阅者模式
发送者(pub)发送消息,订阅者(sub)接收消息
订阅者可以订阅任意数量的频道
消息的发布是无状态的,无法保证可达
Redis如何做持久化:
Redis是一种内存型数据库,一旦服务器进程退出,数据库里的数据就会丢失。
RDB(快照)持久化:保存某个时间点的全量数据快照:
SAVE:阻塞Redis的服务器进程,直到RDB文件被创建完毕
BGSAVE: Fork出一个子进程来创建RDB文件,不阻塞服务器进程。
自动化触发RDB持久化的方式:
根据redis.conf配置里的SAVE m n 定时触发(用的是BGSAVE)
主从复制时,主节点自动触发
执行Debug Reload
执行Shutdown且没有开启AOF持久化
系统调用fork():创建进程,实现了Copy-on-Write。
如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变
缺点
内存数据的全量同步,数据量大会由于I/O而严重影响性能
可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据
AOF(Append-Only-File ) 持久化:保存写状态
记录下除了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
日志重写解决AOF文件大小不断增大的问题,原理如下:
调用fork(),创建一个子进程
子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件(新AOF是根据内存的数据直接生成命令,所以不需要老的AOF文件)
主进程持续将新的变动同时写到内存和原来的AOF里(防止重写失败,数据丢失)
主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动
使用新的AOF文件替换掉旧的AOF文件。
RDB和AOF的优缺点:
RDB优点:全量数据快照,文件小,恢复快
RDB缺点:无法保存最近一次快照之后的数据
AOF优点:可读性高,适合保存增量数据,数据不易丢失
AOF缺点:文件体积大,恢复时间长
RDB-AOF混合持久化方式:
BGSAVE做镜像全量持久化,AOF做增量持久化。
Pipeline的优点:
相当于用一个buffer先存储一些从客户端发送的请求,然后批量发送给服务端进行处理,然后在批量发回给客户端,以此减少服务端和客户端之间的通信次数,提升处理性能。
Pipeline和Linux的管道类似
Redis基于请求/响应模型,单个请求处理需要一一应答
Pipeline批量执行指令,节省多次IO往返的时间
有顺序依赖的指令建议分批发送
Redis的同步机制:
全同步过程:
Slave发送sync命令到Master
Master启动一个后台进程,将Redis中的数据快照保存到文件中
Master将保存数据快照期间接收到的写命令缓存起来
Master完成写文件操作后,将该文件发送给Slave
使用新的RDB文件替换掉日的RDB文件
Master将这期间收集的增量写命令发送给Slave端
增量同步过程:
Master接收到用户的操作指令,判断是否需要传播到Slave
将操作记录追加到AOF文件
将操作传播到其他Slave : 1、对产主从库;2、往响应缓存写入指令
将缓存中的数据发送给Slave
哨兵机制 Redis Sentinel:
解决主从同步Master宕机后的主从切换问题:
监控:检查主从服务器是否运行正常
提醒:通过API向管理员或者其他应用程序发送故障通知
自动故障迁移:主从切换
流言协议Gossip:
每个节点都随机地与对方通信,最终所有节点的状态达成一致
种子节点定期随机向其他节点发送节点列表以及需要传播的消息
不保证信息一定会传递给所有节点,但是最终会趋于一致
Redis的集群原理:
分片:按照某种规则去划分数据,分散存储在多个节点上
常规的按照哈希划分无法实现节点的动态增减
致性哈希算法:对2^32取模,将哈希值空间组织成虚拟的圆环
将数据key使用相同的函数Hash计算出哈希值:
Node C宕机:
新增服务器NodeX:
Hash环的数据倾斜问题:
引入虚拟节点解决数据倾斜的问题:
Java 虚拟机 JVM
面试考题:
谈谈你对Java的理解
设计的知识点:
平台无关性
GC
语言特性
面向对象
类库异常处理
平台无关性
Compile Once, Run Anywhere如何实现:
在配置了环境变量后,在执行javac指令的时候系统会直接去指定的jdk/bin目录下找到javac程序并执行。
class文件保存的就是java文件编译后的字节码, 之后使用java指令就能执行class文件并打印输出。
javap: jdk自带的反汇编器,可以查看java编译器生成的字节码。
javap -c: 反汇编,把字节码文件转成能看懂的java文件。
为什么JVM不直接将源码解析成机器码去执行
准备工作:每次执行都需要各种检查
兼容性:也可以将别的语言解析成字节码
JVM如何加载.class文件:
JVM是内存中的虚拟机:
Class Loader:依据特定格式,加载class文件到内存
Execution Engine:对命令进行解析
Native Interface:融合不同开发语言的原生库为Java所用;如果某些场景需要执行较高运算性能的操作的时候可以调用别的语言实现的方法或库。
具体实现方法是在Native Method Stack中登记native方法,在Execution执行时加载Native Libraies即其他语言的库。
会实现一个native的接口。
Runtime Data Area:JVM内存空间结构模型
所以总结一下:JVM主要有Class Loader、Runtim Data Area、Execution Engine、Native Interface组成。首先通过Class Loader将符合其要求的Class文件加载到Runtime Data Area中,并通过Execution Engine去解析Class文件中的字节码,之后提交给操作系统去执行。
谈谈反射:
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个对象都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。
反射的例子:
public class Robot {
private String name;
public void sayHello(String helloSentence){
System.out.println(helloSentence+name);
}
private String sayHello2(String tag){
return "hello" + tag;
}
}
public class Reflect {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 获取类
Class rc = Class.forName("com.example.reflactexample1.demos.web.Robot");
// 创建类的实例,并强转,因为Class.forName返回的是T泛型。
Robot r = (Robot) rc.newInstance();
System.out.println("Class Name is:" + rc.getName());
// 获取方法对象,getDeclaredMethod能够获取private、 public、 protected的方法,或者所实现的接口的方法,不能获取继承的方法。
Method getHello2 = rc.getDeclaredMethod("sayHello2", String.class);
//访问私有方法时,要设置accessible为true,默认为false,如果不设置就会报错。
getHello2.setAccessible(true);
//调用方法,需要传入类实例和类的方法参数,invoke默认返回Object。
Object str = getHello2.invoke(r, "user001");
System.out.println("私有方法getHello2的输出为:" + str);
// getMethod方法只能获取到类的public方法或者继承的方法和所实现的接口的方法。
Method getHello1 = rc.getMethod("sayHello", String.class);
getHello1.invoke(r, "Hello aaa");
// 获取类的私有属性
Field name = rc.getDeclaredField("name");
//设置accessible为true,因为是私有属性
name.setAccessible(true);
name.set(r, "XiaoMing");
getHello1.invoke(r, "Hello aaa");
}
}
反射的目的:
满足xml配置,在方法中,比如Class.forName、getDeclaredMethod或者getMethod中写入xml中定义的变量,想要获取什么类直接在xml中修改配置就行,不需要改变代码。
满足xml配置只是其中之一,我理解最关键在于反射能够让我们更灵活去写出更通用的算法,试想如果想要动态地将某些实现类实例注入到接口里,而且是要在运行时候通过字面量传入,并设置一系列条件最终才选出某个类,这种方法要写得特别通用,通常就离不开反射了,一个很简单的例子,就是当代码出现很多if elseif之类的情况,就会变得难以维护,如果我们设置成一个hashMap形式,每个key对应一种策略实现类,这样就能封装这种if else的情况,简化代码。
反射的作用:
反射有一个比较重要的作用就是实现ioc容器,对服务进行注入,一个比较典型的应用就是spring容器,里面的service层接口都是通过反射去获取配置创建对应的实例并注入。
反射一般多用于框架(spring里面一大堆反射),我们项目中也有,比如说要写一些通用的库,去调用一些回调方法的时候(比如说任务执行完成后,按照传过来的回调方法名还有类去回调对应类的方法,通知业务完成或者执行任务完成后的逻辑),也会经常调用。
谈谈ClassLoader:类从编译到执行的过程
编译器将Robot.java源文件编译为Robot.class字节码文件
ClassLoader将字节码转换为JVM中的Class对象
JVM利用Class对象实例化为Robot对象
ClassLoader定义:
ClassLoader在Java 中有着非常重要的作用,它主要工作在 Class 装载的加载阶段,其主要作用是从系统外部获得 Class二进制数据流。它是 Java 的核心组件所有的 Class 都是由 ClassLoader 进行加载的ClassLoader负责通过将Class 文件里的二进制数据流装载进系统,然后交给 Java 虚拟机进行连接、初始化等操作。
ClassLoader的种类:
用户不可见classLoader:
BootStrapClassLoader:C++编写,加载核心库java.
用户可见classLoader:
ExtClassLoader:Java编写,加载扩展库javax.* ,ext加载的是%JAVA_HOME%中lib/ext文件下的jar包和class类文件
AppClassLoader:Java编写,加载程序所在目录,即参数名为java.class.path的路径。实际是去往Users/IdeaProjects/javabasic/out/production/javabasic目录下,而这个目录保存着com开头的项目文件。
自定义ClassLoader:Java编写,定制化加载
ClassLoader源码解析:
通过classLoader类的源码可以看出,负责加载类的loadClass方法,通过传入类的名字,返回代表这个类的Class类实例。
//源码第521行。
// 具体loadClass的实现,在之后的段落中。
// 关于参数resolve,也就是loadClass的第二个参数。如果之前使用过这个类,则设置为true,否则设置为false。If the class was found using the above steps, and the resolve flag is true, this method will theninvoke the resolveclass(Class) method on the resulting class object.
/**
* Loads the class with the specified binary name.
* This method searches for classes in the same manner as the {@link
* #loadClass(String, boolean)} method. It is invoked by the Java virtual
* machine to resolve class references. Invoking this method is equivalent
* to invoking {@link #loadClass(String, boolean) loadClass(name,
* false)}.
*
* @param name
* The binary name of the class
*
* @return The resulting {@code Class} object
*
* @throws ClassNotFoundException
* If the class was not found
*/
public Class loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//Params:
//name – The binary name of the class
//resolve – If true then resolve the class
//Returns:
//The resulting Class object
注意:ClassLoader不同版本的变化:
Java 8的ClassLoader流程:
bootstrap classloader加载rt.jar,jre/lib/endorsed
ext classloader加载参数为java.ext.dirs的路径:/Users/Library/Java/Extensions
application classloader加载-cp指定的类
所有用户可见classLoader定义在Launcher.class类中。
java9及之后的classloader流程:
bootstrap classloader加载lib/modules
ext classloader更名为platform classloader,加载lib/modules
application classloader加载-cp,-mp指定的类
同时,我们注意到,JDK9开始,AppClassLoader父类不再是 URLClassLoader,而是BuiltinClassLoader
所有用户可见classLoader定义在定义在ClassLoaders类中。
jdk 11版本源码(ClassLoader.java):
// the built-in class loaders
private static final BootClassLoader BOOT_LOADER;
private static final PlatformClassLoader PLATFORM_LOADER;
private static final AppClassLoader APP_LOADER;
每次用到class时,classLoader都会去这些路径下面查看是否有对应的文件。如果有文件就加载进系统。
类加载器的双亲委派机制:
注:如果底层的classLoader已经加载过类,则直接返回之前加载的类,如果没有加载过这个类,则首先把这个加载向上抛出,一直抛到最顶层的BootStrap ClassLoader,所以对于没加载过的类,最顶层的classLoader会首先开始尝试加载,如果无法加载再向下交给下层的classLoader完成。
使用双亲委派机制去加载类的目的:
避免多份同样字节码Class的加载。
保护程序安全
防止核心API被随意篡改。通过委托方式不会去篡改核心.CLASS,即使篡改也不会去加载,即使加载了也不再是同一个.CLASS对象了。不同的加载器加载同一个.CLASS也不是同一个CLASS对象。当自己程序中定义了一个和Java.lang包同名的类,因为使用的是双亲委派机制,会由启动类加载器去加载JAVA_HOME/lib中的类,而不是加载用户自定义的类。
类的加载方式:
隐式加载:new
显式加载:loadClass,forName等
当显式加载获取到class对象后,需要调用class对象的newInstance方法去生成对象的实例。
隐式加载支持通过参数的构造器函数,但是显式加载的newInstance方法不支持传入参数,要想使用带参数构造器,需要通过反射获取带参构造器函数的实例,才能支持参数。
loadClass和forName的区别:
类的装载过程。
Class.forName得到的class是已经初始化完成的。
Classloder.loadClass得到的class是还没有链接的。
loadClass的作用:在Spring IoC中,在资源加载器获取要读入的资源的时候,比如读取一些bin的配置文件的时候,如果使用ClassPath的方式来加载,就要以Classloder.loadClass的方式来加载,这和Spring IoC的lazy load有关,Spring IoC为了加快初始化速度,大量使用延迟加载技术,使用loadClass方法不需要执行连接和初始化的步骤,加快初始化速度,把类的初始化工作放到使用类的时候再做。
Java 内存模型:
内存简介:
32位处理器:2^32 的可寻址范围
64位处理器:2^64 的可寻址范围
地址空间的划分:
内核空间
主要的操作系统程序和C运行时的空间,包括用于连接计算机硬件,调度程序,提供联网和虚拟内存服务的逻辑和C的进程。
用户空间
Java 实际运行时使用的内存空间。
寻址空间非重点问题:
JVM内存模型 – JDK8版本:
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:MetaSpace、Java堆。
程序计数器(Program Counter Register):
当前线程所执行的字节码行号指示器(逻辑)
改变计数器的值来选取下一条需要执行的字节码指令
和线程是一对一的关系即“线程私有”,为了线程切换后能恢复正确的执行位置,每个线程都有一个独立的程序计数器。
对Java方法计数,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果是Native方法则计数器值为Undefined
由于只记录行号,不会发生内存泄露。
Java虚拟机栈(Stack):
Java方法执行的内存模型
包含多个栈帧,每个方法在执行时都会创建一个栈帧,即方法运行期间的基础数据结构,栈帧用于存储局部变量表、操作栈、动态链接返回地址等。
每个方法执行中对应虚拟机栈帧从入栈到出栈的过程。
Java虚拟机栈用来存储栈帧。而栈帧持有局部变量,部分结果以及参与方法的调用与返回。
当方法调用结束时,帧才会被销毁。所以栈的内存不需要通过gc去回收,而是会自动销毁。
局部变量表和操作数栈:
局部变量表:包含方法执行过程中的所有变量
操作数栈:入栈、出栈、复制、交换、产生消费变量
操作数栈在执行字节码指令时被用到,类似于原生CPU寄存器,大部分JVM字节码把时间花费在操作数栈的操作上。
例子:执行add(1,2):
执行方法时,虚拟机栈会按照程序计数器从大到小依次压入栈中,根据栈后进先出的顺序,执行的时候就是按照从小到大的顺序去执行。
从上图的执行栈帧可以看出,局部变量表主要为操作数栈提供数据支撑。
递归为什么会引发java.lang.StackOverflowError异常:
每次执行一个方法,就会往虚拟机栈中压入一个栈帧。
递归过深,栈帧数超出虚拟栈深度。
虚拟机栈过多会引发java.lang.OutOfMemoryError异常:
虚拟机栈如果可以动态扩容,就可能因为扩容过大而内存不足,导致java.lang.OutOfMemoryError异常。
本地方法栈
与虚拟机栈相似,主要作用于标注了native的方法。
元空间(MetaSpace)与永久代(PermGen)的区别
元空间:在JDK 8 之后,开始把类的源数据放到本地堆内存中,这一块区域就叫做MetaSpace元空间。这块区域在JDK 7 及以前属于永久代。元空间和永久代都是用来存储class的相关信息,包括class的方法method和属性field等。
元空间和永久代都是方法区的实现。 注:方法区只是jvm的一种规范,在 JDK 7 之后原先位于方法区中的字符串常量池已被移动到Java堆中,因为永久代中内存极为有限,如果频繁调用intern方法创建字符串对象会使得字符串常量池被挤爆,从而引发内存溢出异常。 并且在JDK 8 之后,使用元空间替代了永久代。
元空间使用本地内存,而永久代使用的是jvm的内存
java.lang.OutOfMemoryError : PermGen space 在使用元空间之后将不存在,因为默认的类的源数据的分配只受本地内存大小的限制。也就是本地内存剩余多少,理论上metaspace就可以有多大。JVM会在运行时根据其需要动态的分配内存的大小。
MetaSpace相比PermGen的优势:
字符串常量池存在永久代中,容易出现性能问题和内存溢出
类和方法的信息大小难易确定,给永久代的大小指定带来困难
永久代会为GC带来不必要的复杂性
方便HotSpot与其他JVM如Jrockit的集成
Java堆(Heap):
对象实例的分配区域
GC管理的主要区域
JVM调优:
JVM 三大性能调优参数-Xms -Xmx-Xss的含义:
java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar
-Xss:规定了每个线程虚拟机栈(堆栈)的大小,一般来说256k就够了。
-Xms:堆的初始值,一旦Java堆的容量超过初始大小,Java堆就会自动扩容,直到Xmx设置的最大堆值。
-Xmx:堆能达到的最大值,一般设置的和Xms一样大,因为堆自动扩容可能发生内存抖动影响程序执行。
Java内存模型中堆和栈的区别一内存分配策略:
静态存储:编译时确定每个数据目标在运行时的存储空间需求,要求代码中不允许有可变数据结构的存在,也不允许有嵌套和递归的结构存在。因为编译程序无法准确的计算存储空间。
栈式存储:数据区需求在编译时未知,运行时模块入口前确定;是一种动态分配,是由一个运行栈实现的,规定在进入一个程序模块的时候,必须知道该程序模块所需要的数据区大小。
程序模块主要指的是加载到内存待执行的程序,需要了解运行它所需要的内存资源,主要指的是栈帧大小。
堆式存储:编译时或运行时模块入口都无法确定,动态分配,比如可变长度串,或者对象实例。堆中的内存可以按照任意的顺序分配和释放。
栈:(1)在函数中定义的基本类型变量
(2)在函数中定义的对象的引用变量
堆:new产生的对象和数组
方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址。所以栈中保存的是引用变量,指向堆中保存的对象实例的地址,引用变量在运行到作用域之外后就会释放掉,而堆中的对象数据不会被释放,一直到没有引用变量指向这个对象实例后,这个数据就会变成垃圾数据等待GC回收。
管理方式:栈自动释放,堆需要GC
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆
分配方式:栈支持静态和动态分配,而堆仅支持动态
分配效率:栈的效率比堆高,因为操作简单,只需要入栈,出栈操作。
Class: 指Class对象。
Object: 指对象实例。
一些关于内存的补充:
首先是字符串常量池,在HotSpot VM里实现的string
pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot
VM的实例只有一份,被所有的类共享;
其次是Class常量池,我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量和符号引用;
字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
第三个是运行时常量池,运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
贴出这么多概念主要是先把编译时和运行时分开,编译的时候会创建出字符串常量池;
具体在第一次引用该项的 ldc 指令被第一次执行到的时候。 解析 CONSTANT_String 时(该代码也就是执行到 Strings = “hello”; 的时候),根据 index 去运行时常量池查找 CONSTANT_UTF8,然后找到对应的 Symbol 对象, 去到StringTable,StringTable 支持以 Symbol 为 key 来查询是否已经有内容匹配的项存在与否,
存在则直接返回匹配项,不存在则创建出内容匹配的java.lang.String 对象,然后将其引用放入 StringTable
不同JDK版本之间的intern()方法的区别-JDK6 VS JDK6+:
String s= new String( original: "a"); s.intern();
JDK6:当调用 inter方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用 intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用:如果堆中不存在,则在池中创建该字符串并返回其引用。
注:jdk8以后,字符串常量池里可以保存实际的值或者指向堆里面字符串的指针。
JDK6:
输出:false
false
注:“a"的时候会在字符串常量池中创建出一个"a”,new的时候会在Java Heap中创建一个"a"。 s3.intern在字符串常量池中放的是副本,所以地址不同。 
JDK 7:
输出: false
true
s3.intern在字符串常量池中放的是引用变量,所以地址相同。 
Java 垃圾回收机制
判断对象为垃圾的标准:
没有被其他对象引用。
引用计数算法
判断对象的引用数量:
通过判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
任何引用计数为0的对象实例可以被当作垃圾收集
优点:执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致内存泄露。
比如两个对象实例互相引用。
可达性分析算法:
通过判断对象的引用链是否可达来决定对象是否可以被回收。
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析箅法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象 才是存活对象。
可以作为GC Root的对象
虚拟机栈中引用的对象(栈帧中的本地变量表)
方法区中的常量引用的对象
方法区中的类静态属性引用的对象
本地方法栈中JNI( Native方法)的引用对象
活跃线程的引用对
几种垃圾回收算法:
标记-清除算法(Mark and Sweep):
标记:从根集合进行扫描,对存活的对象进行标记。
清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
缺点:碎片化,容易造成碎片化。标记清除不需要对象移动,并且仅对不存活的对象处理,很容易造成不连续的内存碎片。空间碎片较多,可能导致以后程序运行时,需要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾回收工作。如果一直找不到足够的内存,collector一直尝试回收垃圾,builder一直尝试创建对象,可能会造成内存溢出OOM异常。
复制算法(Copying):
分为对象面和空闲面,按可用的内存容量的一定比例分配划分为两块或多个块,并选择其中的一块或两块作为对象面,其余的作为空闲面。
对象在对象面上创建
当对象面的内存用完的时候,存活的对象被从对象面复制到空闲面
将对象面所有对象内存清除
适用于对象存活率低的场景,比如年轻代。
解决碎片化问题
顺序分配内存,简单高效
适用于对象存活率低的场景
缺点:
对于对象存活率高的场景不适用。
对象存活率高,要进行较多的复制操作,效率较低。
会浪费掉一半的空间,除非有额外的空间担保。
标记-整理算法(Compacting):
标记:从根集合进行扫描,对存活的对象进行标记
清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
避免内存的不连续行
不用设置两块内存互换
适用于存活率高的场景,比如老年代。
主流算法 - 分代收集算法(Generational Collector):
垃圾回收算法的组合拳
按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
目的:提高JVM的回收效率
jdk6 , jdk7:
Jdk8及其以后的版本:
去掉了永久代,保留了年轻代和老年代。
GC的分类:
Minor GC:
发生在年轻代的GC,使用复制算法。年轻代几乎是所有的Java对象出生的地方,Java对象申请的内存以及存放都是在这个地方进行的,Java中的大部分对象不需要长久地存活,年轻代是GC频繁收集垃圾的区域。
Full GC:
对老年代的回收一般会伴随着年轻代的垃圾收集,所以被称为Full GC。
年轻代:尽可能快速地收集掉那些生命周期短的对象
Eden区,对象刚被创建出来的时候,首先被分配在Eden区,如果Eden区放不下,新创建的对象也可能被放在Survivor,甚至可能是老年代中。
两个Survivor区,分别被定义为: From区和To区。两个区域不是固定的,会随着垃圾回收的进行相互转换。
注:8:1:1的比例 为默认比例,因为新生代中的98%新创建对象都是朝生夕死的对象,所以对于Eden的比例要大些,而存放存活对象的survivor区比例要小些。
每次使用Eden和其中的一块Survivor, 当进行垃圾回收时,一次性的将Eden和Survivor中的存货对象复制到另一块Survivor区去。然后清理掉Eden和一块Survivor,当Survivor中的内存不够,则需要依赖老年代分配的空间担保。
对象如何晋升到老年代:
经历一定Minor次数依然存活的对象,默认是15次。
Survivor区中存放不下的对象。
新生成的大对象(-XX:+PretenuerSizeThreshold )。
常用的调优参数:
-XX:SurvivorRatio:Eden和Survivor的比值,默认8:1。
-XX:NewRatio:老年代和年轻代内存大小的比例。
-XX:MaxTenuringThreshold:对象从年轻代晋升到老生代经过GC次数的最大阈值。
老年代:存放生命周期较长的对象:
标记-清理算法
标记-整理算法
老年代:
Full GC和Major GC
FuIl GC比Minor GC慢,但执行频率低
触发FuIl GC的条件:
老年代空间不足
永久代空间不足,JDK7 及以前版本。
CMS GC时出现promotion failed ,concurrent mode failure
promotion failed:在进行Minor GC时,Survivor放不下了,对象只能放入老年代,而老年代也放不下,就会报这个错误。
concurrent mode failure:在进行CMS GC的时候,同时有对象要放入老年代中,而老年代中又放不下,就会报这个错误。
Minor GC晋升到老年代的平均大小大于老年代的剩余空间。
调用System.gc()。
使用RMI来进行RPC或管理的JDK应用,每小时执行1次FuIlGC。
Stop-the-World:
JVM由于要执行GC而停止了应用程序的执行
任何一种GC算法中都会发生
多数GC优化通过减少Stop-the-world发生的时间来提高程序性能
Safepoint:
在对根对象进行可达性分析过程中对象引用关系不会发生变化的点,到达这个点才会执行GC。
产生Safepoint的地方:方法调用;循环跳转:异常跳转等, 因为这些指令是复用指令,执行时间长,不容易让程序产生变化。
安全点数量得适中,太少会让GC等待太长,太多会让运行程序负荷过大。
JVM的运行模式:
Server:启动慢,但是进入稳定期后,运行速度要更快。因为虚拟机更好,优化更多。
Client:启动快,但是进入稳定期后,运行速度更慢。
常见的垃圾收集器:
年轻代常见的垃圾收集器:
Serial收集器(-XX:+UseSerialGc,复制算法):
单线程收集,进行垃圾收集时,必须暂停所有工作线程
简单高效,Client模式下默认的年轻代收集器
ParNew收集器(-XX:+UseParNewGC,复制算法):
多线程收集,其余的行为、特点和Serial收集器一样,默认线程和CPU数量一样,在多CPU场景下,也可以通过参数限制线程数量。
单核执行效率不如Serial, 因为存在线程交互开销,在多核下执行才有优势。
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法):
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
比起关注用户线程停顿时间,更关注系统的吞吐量
在多核下执行才有优势,Server模式下默认的年轻代收集器
老年代常见的垃圾收集器:
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法):
单线程收集,进行垃圾收集时,必须暂停所有工作线程
简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法):
多线程,吞吐量优先
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法):
初始标记:stop-the-world, 从根对象开始,并且只扫描到和根直接关联的对象。
并发标记:并发追溯标记,程序不会停顿
并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
并发清理:清理垃圾对象,程序不会停顿
并发重置:重置CMS收集器的数据结构
缺点:1.如果垃圾的产生是在标记后,那么只能等到下次再回收。
2. 使用标记-清除算法,可能发生内存碎片化问题。
G1收集器(-XX:+UseG1GC, 复制+标记-整理算法):
Garbage First收集器的特点:
并行和并发
分代收集
空间整合 – 解决碎片化问题
可预测的停顿 – 可以设置不超过多少停顿时间
将整个Java堆内存划分成多个大小相等的Region
年轻代和老年代不再物理隔离
GC相关的面试题:
Object的finalize()方法的作用是否与C++的析构函数作用相同:
与C++的析构函数不同,析构函数调用确定, 即对象离开作用域后就会被delete掉,而它的是不确定的
将未被引用的对象放置于F-Queue队列
虚拟机触发的finalize()方法,优先级较低,方法执行随时可能会被终止
给予对象最后一次重生的机会
Java中的强引用,软引用,弱引用,虚引用有什么用:
强引用(Strong Reference ):
最普遍的引用:Object obj=new Object()
抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
通过将对象设置为null来弱化引用,使其被回收。设置为null主要是为了取消对某个对象的引用,使得该对象不可达,从而被GC回收了。
软引用(Soft Reference):
对象处在有用但非必须的状态
只有当内存空间不足时,GC会回收该引用的对象的内存
可以用来实现高速缓存
String str=new String( original:"abc");// 强引用 SoftReference softRef=new SoftReference(str);// 软引用
软引用使用场景:对象缓存就是把这些对象存在某个数据结构里,该数据结构(queue等)就是缓存池,然后这些对象由于属于弱引用或者软引用,会被随时回收,就符合缓存元素的特质,用的话先去数据结构实例里找,找不到再创建。
弱引用(Weak Reference):
非必须的对象,比软引用更弱一些
GC时会被回
被回收的概率也不大,因为GC线程优先级比较低
适用于引用偶尔被使用且不影响垃圾收集的对象
String str=new String( original: "abc"); WeakReference abcWeakRef = new WeakReference(str);
弱引用使用场景:比如一些需要预先加载到系统里的静态资源,比如图片之类,用一个list作为对象来承接,如果数量很大并且是强引用就容易OOM,这时候就考虑使用非强引用了。此外,比如说你要测试一些GC的效率,也可以用弱引用来检查。
虚引用(PhantomReference):
不会决定对象的生命周期
任何时候都可能被垃圾收集器回收
跟踪对象被垃圾收集器回收的活动,起哨兵作用, 回收垃圾之前,会首先将虚预支相关的引用放入引用队列中,所以程序可以通过队列中有没有该对象的虚引用判断该对象是否被GC回收。
必须和引用队列ReferenceQueue联合使用
String str=new String( original: "abc"); ReferenceQueue queue = new ReferenceQueue(); PhantomReference ref= new PhantomReference(str, queue);
强引用 >软引用 >弱引用 >虚引用
类层次结构:
引用队列(ReferenceQueue):
无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达。
可以理解为Queue为一个链表的容器,自身只存储一个head节点。后面的节点由reference节点的next保持即可。
例子源码理解:
// ReferenceQueue源码第51行,可以看出队列只保存了一个head变量,剩下的链表通过enqueue方法的next储存。
static private class Lock{};
private Lock lock = new Lock();
private volatile Referencequeue =r.queue;
if((queue ==NULL) || (queue == ENQUEUED)){
return false;
}
assert queue == this;
r.queue = ENQUEUED;
r.next=(head== null)?r:head; //传入的Reference对象的next指向head,也就是当前引用队列保存的值。
head = r;
queueLength++;
// Reference类的源码。类中有next属性值。
private T referent; /* Treated specially by GC */
volatile ReferenceQueue
















































































