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 = Noneself.right = Noneself.val = keydef insert(root, key):if root is None:return Node(key)else:if root.val < key:root.right = insert(root.right, key)else:root.left = insert(root.left, key)return rootroot = None
keys = [50, 30, 20, 40, 70, 60, 80]for key in keys:root = insert(root, key)
二叉平衡树(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)< Ki — (就是上图的8,12,26, 30, 65, 87 左边的数都小于右边)
b) 关键字的个数 n必须满足: [ceil(m /2)-1]<= n <= m-1
c) 非叶子结点的指针 : P[1], P[2],…, P[M] ;其中 P[1]指向关键字小于 K[1]的子树,P[M]指向关键字大于 K[M-1]的子树,其它P[i]指向关键字属于(K[i-1],K[)的子树 — (就是比左边值小的放左边的子节点,比右边值大的放右边的子节点,在两个关键字中间的值放中间的子节点。)
B+树:
B+树的特征:
非叶子节点的子树指针与关键字个数相同 (如上图所示,每个非叶子节点有3个关键字也有3个指针)
非叶子节点的子树指针[Pi],指向关键字值[K[i],K[i+1])的子树
非叶子节点仅用来索引,数据都保存在叶子节点中 (如上图所示即使查找20,也需要找到最后一层叶子节点的第一个值,在非叶子节点没有数据)
所有叶子节点均有一个链指针指向下一个叶子结点 (如上图最下面的Q,这个指针使得B+树具有范围查找的能力,比如第一次查到18,想查找比18大的数据,不用再从根节点找起,可以直接用这个指针查找右边的叶子节点)
B+树为什么更适合做储存索引:
B+树的磁盘读写代价更低 (非叶子节点不在存储数据,只存储索引,导致节点更小,从一块存储中读取的索引更多)
B+树的查询效率更加稳定 (因为所有查询的数据都要到叶子节点查找,不会出现在非叶子节点就返回的情况,所以每次查找的时间都基本差不多。稳定的Olog(n)。)
B+树更有利于对数据库的扫描 (只需要使用链指针遍历叶子结点,即可扫描数据库,更高效快速)
Hash索引:
特点:
一次运算即可定位到数据所在的桶(bucket),理论上性能高于B+树。
缺点:
仅仅能满足 “=”, “IN”, 不能使用范围查询
无法被用来避免数据的排序操作,就是无法直接使用索引的哈希值进行排序,需要取出数据再进行排序。
不能利用部分索引键查询,在组合查询时,哈希索引是计算所有索引的哈希值,不是单独计算每个索引的哈希值。所以不能单独通过组合索引的一个或几个索引进行查询。
不能避免表扫描
遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
BitMap位图索引简介:
特点:
和B+树相似,统计效率特别高。一个叶子位可以存储非常多的bit位,来表示不同的行。用来统计非常快,加载到内存中后,基本只用执行CPU的纯叠加操作。
缺点:
位图索引不是主流数据库索引,只有少数数据库如oracle等支持。
只能查询固定几个字段的数据,比如性别,由于只有4个关键字,并且每个关键字只有0,1是与不是,所以没法存储复杂信息的数据。除此之外,位图索引锁的力度非常大,当场是新增或修改某个数据的时候,和它在同一个位图的数据都会被锁住,所以不适合并发较高的系统,而适合统计较高的系统。
数据库索引部分总结:主流的索引是B+树,哈希索引和位图索引较小众。
索引模块:
密集索引和稀疏索引的区别:
密集索引文件中的每个搜索码值都对应一个索引值,即图中的1001,1002,1003等都有一个索引值。
稀疏索引文件只为索引码的某些值建立索引项,具体来说是为某一个数据块建立一个索引值,比如图中的1001和1002,稀疏索引只保存1001的索引值,如果查询1002的数据则先找到1001的数据再通过偏移量找到1002的数据。
MySql的主要存储引擎:
MyISAM: 不论是主键索引,还是普通索引都属于稀疏索引。
InnoDB: 必须有且仅有一个密集索引规则如下:
若一个主键被定义,该主键则作为密集索引
若没有主键被定义,该表的第一个唯一非空索引则作为密集索引
若不满足以上条件,innodb内部会生成一个隐藏主键(密集索引),为一个六字节的自增主键。
非主键索引存储相关键位和其对应的主键值,包含两次查找
MyISAM和InnoDB的区别:
InnoDB使用主键进行查询的时候,直接通过B+树的步骤定位到叶子结点的数据,比如 where id = 18这样的语句。
InnoDB的辅助索引为稀疏索引并且只保存了相关的键位和其主键值,查询时先查询到其主键值,再通过主键值查询到数据。如下图如果通过姓名Ellison查找其数据,则会先通过姓名这个稀疏索引查找到其主键,再通过主键值查找到他的数据。
MyISAM的主键索引和辅助索引都一样为稀疏索引,所以存储的值都一样。
InnoDB为聚簇索引,索引和数据都在一个文件里,而MyISAM为非聚簇索引,索引和数据分别在两个文件里。
InnoDB为ibd文件,MyISAM的索引在MYI文件中,数据在MYD文件中。
MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,根据索引找到地址进行查询。InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这个时候叫聚簇索引;辅助索引查询的是主键id,如果不在索引中就是需要回表到主键索引查询。
不同的存储引擎设计都是根据其适用场景来的,通过对底层数据结构的设计,使对应的存储引擎可以更适应场景需求。InnoDB使用聚簇索引和辅助索引结合,能够实现高效的查询和更新;正是有了不同的索引,InnoDB通过给索引项加锁才实现了行级锁;行锁的实现才使得事务实现更简单
MyISAM查询效率更高,但是InnoDB可以实现行级锁,所以增删改效率更高。
索引模块考点:
- 为什么要使用索引
因为可以避免全表扫描。- 什么样的信息能成为索引
主键,唯一键等能让数据具有一定区分性的字段都可以成为索引。- 索引的数据结构
Mysql一般使用 B+ 树,不支持BitMap,不显示支持Hash索引。- 密集索引和稀疏索引的区别
密集索引文件中的每个搜索码值都对应一个索引值
稀疏索引文件只为索引码的某些值建立索引项,具体来说是为某一个数据块建立一个索引值。
如何优化慢查询Sql:
分三步,
进入到数据库可视化界面中:
1. 根据慢日志定位慢查询sql
## 查询和查询相关的变量
show variables like '%quer%';
## 在显示的变量中, slow_query_log代表是否开启慢日志,
## long_query_time代表设置执行时间超过多少秒的sql记录到慢日志中。
## slow_query_log_file表示慢日志文件的位置。# 显示慢日志的数量
show status like '%slow queries%';# 打开慢日志并设置记录时间为1秒钟。
# 注:在Query界面中用命令设置的参数在数据库重启后就会恢复默认设置,想要永久改变参数就要在ini文件中配置参数。
set global slow_query_log = on
set global long_query_time = 1# 执行查询语句
select count(id) from person_info_large;
select name from person_info_large order by name desc;2.使用explain等工具分析sql# 在sql语句前加上explain即可分析sql查询语句情况。
explain select name from person info large order by name desc;
# explain返回的参数,id表示sql执行的顺序,id越大的越先执行。
# type:表示mysql找到数据行需要的方式,其中index和all为全表扫描。
# system>const>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 <a href="#binary-name">binary name</a>.* 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 <a href="#binary-name">binary name</a> 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 loadersprivate 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<String> softRef=new SoftReference<String>(str);// 软引用
软引用使用场景:对象缓存就是把这些对象存在某个数据结构里,该数据结构(queue等)就是缓存池,然后这些对象由于属于弱引用或者软引用,会被随时回收,就符合缓存元素的特质,用的话先去数据结构实例里找,找不到再创建。
弱引用(Weak Reference):
非必须的对象,比软引用更弱一些
GC时会被回
被回收的概率也不大,因为GC线程优先级比较低
适用于引用偶尔被使用且不影响垃圾收集的对象String str=new String( original: "abc"); WeakReference<String> abcWeakRef = new WeakReference<String>(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 Reference<? extends T> head = null;
private long queueLength=0;
//注意enqueue方法是传入一个reference对象
boolean enqueue(Reference<?extends T>r)
synchronized(lock){
ReferenceQueue<?>queue =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<? super T>queue;
/* When active:NULL
* pending:this
* Enqueued:next reference in queue (or this if last)
* Inactive:this
*/
/rawtype5/
Reference next;
存储关联的且被GC的软引用,弱引用以及虚引用。
代码例子:// 队列中保存的并非str这个强引用,而是abcWeakRef这个弱引用。 String str = new String( original: "abc"); WeakReference<String> abcWeakRef = new WeakReference<String>(str);
多线程和并发
进程和线程的由来:
进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程是抢占处理机的调度单位;线程属于某个进程,共享其资源。
每个进程都有自己独有的内存空间,不同进程拥有不同的虚拟地址空间,所有线程共享进程的内存空间。
线程只由堆栈寄存器、程序计数器和TCB组成。
堆栈寄存器只能存储线程内的局部变量,但不能存储其他线程的相关变量。
进程和线程的区别:
线程不能看做独立应用,而进程可看做独立应用
进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径,线程挂掉了,它所在的进程也会挂掉。
线程没有独立的地址空间,多进程的程序比多线程程序健壮
进程的切换比线程的切换开销大
Java进程和线程的关系:
Java对操作系统提供的功能进行封装,包括进程和线程
运行一个程序会产生一个进程,进程包含至少一个线程
每个进程对应一个JVM实例,多个线程共享JVM里的堆
Java采用单线程编程模型,程序会自动创建主线程
主线程可以创建子线程,原则上要后于子线程完成执行
注:一个程序是一个可执行的文件,而一个进程则是一个执行中程序的实例。
JVM虚拟机并非单线程,虽然只创建一个主线程用于执行任务,但是也会创建其他的线程如GC垃圾回收线程。
Thread中的start和run方法的区别:
调用start()方法会创建一个新的子线程并启动去运行run方法。
run()方法只是Thread的一个普通方法的调用。
调用run方法只会使用主线程执行线程。
调用start方法会创建一个非main的线程执行任务。
下图代码例子中,执行t.run()会在控制台输出:main。
执行t.start()会在控制台输出:Thread-0。
private static void attack(){
System.out.println("Fight")System.out.println("current Thread is :"+ Thread.currentThread().getName());
}public static void main(String[]args){Thread t=new Thread(){public void run(){attack();}
};
System.out.println("current main thread is :" + Thread.currentThread().getName());
//t.run();
t.start();
Thread和Runnable是什么关系:
实现业务多线程的两种方式:
- 让业务继承Thread类,重写Thread里面的run(实际也是因为,Thread实现了Runnable接口,所以Thread重写了Runnable里面的run方法)方法。然后创建这个类的实例,之后因为继承了Thread类,所以这个类的实力也能调用start方法,执行run里面的业务代码。
- 因为多线程是通过Thread类的start方法实现的,如果一个业务实现了Runnable接口,并重写了里面的run()方法,那么就可以传入到Thread中,通过构造方法创建一个线程实例,并调用,Thread类中的start方法,实现业务的多线程。
Thread是实现了Runnable接口的类,使得run支持多线程
因类的单一继承原则,推荐多使用Runnable接口。因为继承了Thread类就没法继承别的类了。
// Thread类和Runnable接口的源码
public Thread(Runnable target) {this(null, target, "Thread-" + nextThreadNum(), 0);}public interface Runnable {abstract void run();}
如何给run()方法传参:
实现的方式主要有三种:
构造函数传参
成员变量传参
回调函数传参
如何实现处理线程的返回值:
实现的方式主要有三种:
主线程等待法。
优点:实现简单。
缺点:子线程的值一多,就需要写很多循环,代码量就变多。并且不知道需要循环多久,没法做到精准的控制。
使用Thread类的join()阻塞当前线程以等待子线程处理完毕。
优点:控制粒度更精准,实现更简单。
缺点:控制粒度还是不够细。
通过Callable接口实现:通过FutureTask Or 线程池获取。
// 如果子线程还未处理完成,主线程直接继续执行,那么就无法得到子线程的返回值,所以可以使主线程循环等待,查询子线程执行完成,继续执行主线程的程序。
public class CycleWait implements Runnable{private String value;public void run(){try {Thread.currentThread().sleep(millis:5000);}catch(InterruptedException e){e.printStackTrace();}value ="we have data now";
}
// 使用主线程等待法之前的main函数,输出Null。
public static void main(String[] args){CycleWait cw = new CycleWait();Thread t = new Thread(cw);t.start();System.out.println("value : " + cw.value);
}//使用主线程等待法之后的main函数,输出:we have data now
public static void main(String[] args){CycleWait cw = new CycleWait();Thread t = new Thread(cw);t.start();while(cw.value == null){Thread.currentThread().sleep( millis:100);}System.out.println("value : " + cw.value);
}//使用t.join法之后的的main函数,输出:we have data now
public static void main(String[] args){CycleWait cw = new CycleWait();Thread t = new Thread(cw);t.start();t.join();System.out.println("value : " + cw.value);
}
//Callable接口源码代码。
public interface Callable<V>{V call()throws Exception;
}// FutureTask.java的源码第124行。可以看出FutureTask类的构造方法能够接收Callable类。
public FutureTask(Callable<V> callable) {if (callable == null)throw new NullPointerException();this.callable = callable;this.state = NEW; // ensure visibility of callable}
// FutureTask.java的源码第160行,能够判断Callable实力里面的call方法是否执行完成。
public boolean isDone() {return state != NEW;}
// FutureTask.java的源码第254行,FutureTask<V> implements RunnableFuture<V> {}实现了RunnableFuture,
// RunnableFuture<V> extends Runnable, Future<V>{} RunnableFuture接口实现了Runnable接口,所以
// FutureTask中重写了Runable的run方法。
// 而FutureTask中的run方法又执行了callable中的call方法。
// 所以,如果在创建FutureTask实例的时候传入callable实例,从而执行带参构造函数,就可以通过FutureTask中的run方法执行实现了callable接口的实例的call方法。
public void run() {if (state != NEW ||!RUNNER.compareAndSet(this, null, Thread.currentThread()))return;try {Callable<V> c = callable;if (c != null && state == NEW) {V result;boolean ran;try {result = c.call();ran = true;} catch (Throwable ex) {result = null;ran = false;setException(ex);}if (ran)set(result);}} finally {// runner must be non-null until state is settled to// prevent concurrent calls to run()runner = null;// state must be re-read after nulling runner to prevent// leaked interruptsint s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}}// FutureTask.java的源码第185行,如果子线程没完成进行等待,完成了就返回值。
public V get() throws InterruptedException, ExecutionException {int s = state;if (s <= COMPLETING)s = awaitDone(false, 0L);return report(s);}// 带时间控制的获得返回值方法。
public V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException {if (unit == null)throw new NullPointerException();int s = state;if (s <= COMPLETING &&(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)throw new TimeoutException();return report(s);}
代码例子:callable和futureTask。
public class MyCallable implements Callable<String>{@Overridepublic string call()throws Exception{String value="test";System.out.println("Ready to work");Thread.currentThread().sleep( millis:5000);System.out.println("task done");return value;}
}public class FutureTaskDemo{public static void main(String[] args)throws ExecutionException, InterruptedException{FutureTask<String> task= new FutureTask<String>(new MyCallable());new Thread(task).start();if(!task.isDone()){System.out.println("task has not finished, please wait!");}System.out.println("task return:"+ task.get());}
}
线程池的例子。
public cassThreadPoolDemo{public static void main(String[]args){ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();Future<String> future = newCachedThreadPool.submit(new MyCallable());if(!future.isDone()){System.out.println("task has not finished, please wait!");}try {System.out.println(future.get());} catch (InterruptedException e){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();} finally {newCachedThreadPool.shutdown();
}
}
}
线程的状态:
六个状态:
- 新建(New):创建后尚未启动的线程的状态,创建了线程对象但还没有调用start方法。
- 运行(Runnable):包含Running和Ready。Running:
位于可运行线程池中,等待被CPU调度和选中获取CPU的使用权;处在Ready状态的线程在获取CPU之后就变为Running状态的线程。- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒。
让线程陷入无限等待状态的方法: 没有设置 Timeout参数的0bject.wait()方法。 没有设置 Timeout 参数的 Thread.join()方法。
LockSupport.park()方法。- 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒。
让线程进入限期等待的方法: Thread.sleep()方法。 设置了 Timeout 参数的 0bject.wait()方法。
设置了 Timeout 参数的 Thread.join()方法。 LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。- 阻塞(Blocked):等待获取排它锁。
- 结束(Terminated):已终止线程的状态,线程已经结束执行。
当线程的run方法或者main方法执行完成,我们认为线程执行终止。线程对象也许还存活,但是调用start方法会抛出异常。
sleep和wait的区别:
sleep是Thread类的方法,wait是Object类中定义的方法。
sleep()方法可以在任何地方使用。
wait()方法只能在synchronized方法或synchronized块中使用。最本质的区别:
Thread.sleep只会让出CPU,不会导致锁行为的改变 。
Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。
notify和notifyAl的区别:
锁池EntryList:
假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池WaitSet:
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
notifyALL 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。
notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
yield:
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。并且yield不会使当前线程让出已占用的锁。
如何中断线程:
- 已经被抛弃的方法:
通过调用stop()方法停止线程。 通过调用suspend()和resume()方法。
可以使一个线程直接停止另一个线程,另一个线程会直接终止并释放锁,有可能造成数据不同步的问题。- 目前使用的方法:
调用interrupt(),通知线程应该中断了。
① 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
需要被调用的线程配合中断:
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
线程转换图:
synchronized
线程安全问题的主要诱因:
存在共享数据(也称临界资源)
存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有-一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。
互斥锁的特性
互斥性:
即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
可见性:
必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized 锁的不是代码,锁的都是对象。
根据获取的锁的分类:获取对象锁和获取类锁:
获取对象锁的两种用法;
同一个类的不同对象之间的锁互不干扰。
- 同步代码块(synchronized(this),synchronized(类实例对象)),锁的是小括号()中的实例对象。
- 同步非静态方法(synchronized method),锁的是当前对象的实例对象。
获取类锁的两种用法,类所也是一种对象锁,只不过锁的是Class对象。
每个类只有一个Class对象,所以每个类只有一个类锁。
- 同步代码块(synchronized(类.class)),锁是小括号()中的类对 象(Class 对象)。
- 同步静态方法(synchronized static method),锁是当前对象的类对象(Class 对象)。
两种锁互相不干扰,一个线程执行带对象锁的方法,另一个线程可以同时执行带类锁的方法。
对象锁和类锁的总结:
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
- 类锁和对象锁互不干扰。
实现synchronized的基础:
Java对象头
Monitor
对象在内存中的布局:
对象头
实例数据
对齐填充
对象头的结构:
MarkWord:
Monitor:每个Java对象天生自带了一把看不见的锁。本身也是一个对象。
对象头的重量级锁(也就是synchronized):记录的是指向Monitor的起始地址的指针。每个对象都有一个Monitor与之关联。
Monitor锁的竞争、获取与释放:
什么是重入:
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。
为什么会对 synchronized 嗤之以鼻:
早期版本中,synchronized属于重量级锁,依赖于MutexLock实现
线程之间的切换需要从用户态转换到核心态,开销较大。
Java6以后,synchronized性能得到了很大的提升:
Adaptive Spinning
Lock Eliminate
Lock Coarsening
Lightweight Locking
Biased Locking
自旋锁与自适应自旋锁:
自旋锁:
许多情况下,共享数据的锁定状态持续时间较短,切换线程状态如挂起或阻塞等不值得
通过让线程执行忙循环等待锁的释放,不让出CPU时间片
缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
PreBlockSpin:设置自旋锁的尝试次数。
自适应自旋锁:
自旋的次数不再固定
由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
锁消除:
更彻底的优化:
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化:
另一种极端:
通过扩大加锁的范围,避免反复加锁和解锁。
synchronized的四种状态:
无锁、偏向锁、轻量级锁、重量级锁。
锁膨胀方向:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
无锁:既没有锁。
**偏向锁:**减少同一线程获取锁的代价
大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作即获取锁的过程只需要检査Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。不需要进行CAS操作。不适用于锁竞争比较激烈的多线程场合。
轻量级锁:
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适应的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁锁的内存语义:
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
锁总结:
补充:
synchronized和ReentrantLock的区别:
ReentrantLock(再入锁):
位于java.util.concurrent.locks包
和CountDownLatch、FutureTask、Semaphore-样基于AQS实现
能够实现比synchronized更细粒度的控制,如控制fairness
调用lock()之后,必须调用unlock()释放锁
性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性的设置:
ReentrantLock fairLock= new ReentrantLock(true);
参数为true时,倾向于将锁赋予等待时间最久的线程
公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
非公平锁:抢占的顺序不一定,看运气
synchronized是非公平锁
公平性可能带来额外的开销,从而导致吞吐量的下降。
ReentrantLock将锁对象化:
判断是否有线程,或者某个特定线程,在排队等待获取锁
带超时的获取锁的尝试
感知有没有成功获取锁
是否能将wait\notify\notifyAl对象化:
java.util.concurrent.locks.Condition
ArrayBlockingQueue.java
ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列
线程安全:队列内部通过互斥锁保护线程安全,互斥锁是通过ReentrantLock实现的;实现了多线程对队列资源的互斥访问。有界是指ArrayBlockingQueue队列是有界限的。阻塞是指,当一个线程已经获取队列资源时,其他线程想要获取队列资源需要阻塞等待。
ArrayBlockingQueue和Condition是组合的关系。public ArrayBLockingQueue(int capacity, boolean fair){if(capacity <= 0)throw new IllegalArgumentException();this.items = new 0bject[capacity];lock = new ReentrantLock(fair);notEmpty =Lock.newCondition();notFull =lock.newCondition(); }
总结:
synchronized是关键字,ReentrantLock是类
ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活地实现多路通知
机制:sync操作Mark Word,lock调用Unsafe类的park()方法
什么是Java内存模型中的happens-before:
Java内存模型JMM:
Java内存模型(即Java Memory
Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JVM运行程序的实体是线程,每个线程创建时,JVM都会为其创建一个工作内存,有些地方称为栈空间,用于存储线程私有的数据;而Java内存模型规定,所有变量都存储在主内存中,主内存是线程共享区域,所有线程都可以访问。但线程对变量的操作,即读取复制等必须在工作内存中进行。首先将主内存中的变量拷贝到工作内存中,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存变量的副本拷贝,工作内存是每个线程的私有区域,因此不同线程间无法访问对方的工作内存,线程间的传值必须通过主内存来完成。
JMM中的主内存和工作内存:
JMM中的主内存:
存储Java实例对象
包括成员变量、类信息、常量、静态变量等
属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存:
存储当前方法的所有本地变量信息,本地变量对其他线程不可见
字节码行号指示器、Native方法信息
属于线程私有数据区域,不存在线程安全问题
JMM与Java内存区域划分是不同的概念层次:
JMM描述的是一组规则,围绕原子性,有序性、可见性展开
相似点:存在共享区域(内存模型中的主内存,对应JVM中的堆和方法区)和私有区域(内存模型中的工作内存,对应JVM中的程序计数器,虚拟机栈,本地方法栈)
主内存与工作内存的数据存储类型以及操作方式归纳:
方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
成员变量、static变量、类信息均会被存储在主内存中
主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。
JMM如何解决可见性问题:
指令重排序需要满足的条件:
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
无法通过happens-before原则推导出来的,才能进行指令的重排序。
A操作的结果需要对B操作可见,则A与B存在happens-before关系:
happens-before的八大原则:
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
happens-before的概念:
如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。
代码例子:
private int value = 0;
//线程1执行写操作
public void write(int input ){value = input;
}
//线程2执行读操作
public int read(){return value;}
由于上面的代码不满足任意的一个happens-before原则,所以无法得知最后value的值是什么,这段代码线程不安全。
修改的方法,让代码满足第2或3条原则,即给方法加上锁,或者对变量加上volatile关键字。volatile:JVM提供的轻量级同步机制:
保证被volatile修饰的共享变量对所有线程总是可见的
禁止指令重排序优化
volatile的可见性:
volatile保证可见性,即一个线程修改了一个变量值,立马反应到别的线程中。
但是volatile并不保证安全性,比如下面这个++的例子,++操作并不是原子性的,++操作是先读取值,再写回一个新值,分两步完成,如果第一个线程读取了值,在还没有+1之前就被另一个线程也读取了相同的值,这个时候两个线程同时+1,则最后变量值只被加了一次,出现线程安全问题。public class VolatileVisibility {public static volatile int value =0;public static void increase(){value++;} }//加上synchronized后,可以保证其他线程不会获取这个对象的Moniter锁,并且会创建一道内存屏障,保证这个线程会在后面获取到这个锁的线程之前执行。 public class VolatileVisibility {public static volatile int value =0;public synchronized static void increase(){value++;} }
由于synchronize也具有可见性,所以使用之后可以省去volatile关键字。
下面的例子中,由于对shutdown赋值的操作是原子性的,所以volatile可以保证线程安全。public class VolatileSafe {volatile boolean shutdown;public void close(){shutdown=true;}public void doWork(){while(!shutdown){System.out.println("safe....");}} }
volatile变量为何立即可见 ?
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效。
volatile如何禁止重排优化:
内存屏障(Memory Barrier)
1.保证特定操作的执行顺序
2.保证某些变量的内存可见性。
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。volatile和synchronized的区别:
- volatile本质是在告诉JM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
CAS( Compare and Swap ):
一种高效实现线程安全性的方法:
支持原子更新操作,适用于计数器,序列发生器等场景
属于乐观锁机制,号称lock-free
CAS操作失败时由开发者决定是继续尝试,还是执行别的操作。竞争的线程不会被阻塞或挂起。
CAS操作:
包含三个操作数-- 内存位置(V)、预期原值(A)和新值(B)。
当旧的预期值A和内存位置值V相同时,才会修改内存值V为新值B并返回true,否则返回false。
CAS多数情况下对开发者来说是透明的:
J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患
Java9以后,可以使用Variable Hande API来替代Unsafe
缺点:
若循环时间长,则开销很大
只能保证一个共享变量的原子操作
ABA问题,即线程一开始读取的是A但是在之后被别的线程改成B又改回了A,那么CAS就能认为没改过。
解决:AtomicStampedReference
通过控制变量值的版本来保证CAS的一致性。
CAS原理:
截图来源网址
Java线程池:
利用Executors创建不同的线程池满足不同场景的需求:
- newFixedThreadPool(int nThreads) 指定工作线程数量的线程池
- newCachedThreadPool() 处理大量短时间工作任务的线程池,
(1)试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
(2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
(3)系统长时间闲置的时候,不会消耗什么资源- newSingleThreadExecutor() 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
- newWorkStealingPool() 内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序
Fork/Join框架:
把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架.
Work-Stealing算法:某个线程从其他队列里窃取任务来执行
Fork/Join会把每个任务放到不同的队列里,每个队列由一个单独的线程执行任务。
为了避免有的线程执行完任务,有的线程还没执行完任务,闲置的线程会从别的队列窃取正在等待的任务,为了避免正在执行任务的线程和准备窃取任务的线程竞争导致消耗增多,所有队列都为双端队列,执行任务的线程从队列头部获取任务,窃取任务的线程从队列尾部获取任务。
为什么要使用线程池:
降低资源消耗,避免创建和销毁线程的性能消耗。
提高线程的可管理性Executor的框架:
J.U.C的三个Executor接口:
Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善有submit方法(能传入Callable接口,能接收线程的返回值)
ScheduledExecutorService:支持Future和定期执行任务// 新建并运行线程的伪代码 Thread t = new Thread(); t.start(); // 新建线程,并将线程丢给线程池执行的伪代码 Thread t = new Thread(); executor.execute(t);
ThreadPoolExecutor:
ThreadPoolExecutor的构造函数
corePoolSize:核心线程数量
maximumPoolSize:线程不够用时能够创建的最大线程数
workQueue:任务等待队列
keepAliveTime:抢占的顺序不一定,看运气
threadFactory:创建新线程,Executors.defaultThreadFactory()
handler:线程池的饱和策略:
AbortPolicy:直接抛出异常,这是默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
实现RejectedExecutionHandler接囗的自定义handler
新任务提交execute执行后的判断:
如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
如果线程池中的线程数量大于等于corePoolSize 且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
如果设置的corePoolSize 和maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。
线程池的状态:
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN:不再接受新提交的任务,但可以处理存量任务
STOP:不再接受新提交的任务,也不处理存量任务
TIDYING:所有的任务都已终止
TERMINATED:terminated()方法执行完后进入该状态
状态转换图:
工作线程的生命周期:
线程池的大小如何选定:
CPU密集型:线程数=按照核数或者核数+1设定
I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)
String ,StringBuffer ,StringBuilder的区别:
- String类是不可变的,每次对String对象进行修改都会创建一个新的String对象,因此在需要对字符串进行大量修改的场景下,使用String类会产生很多的开销。
- StringBuffer和StringBuilder类是可变的,可以对其进行修改,而不会创建新的对象。
- StringBuffer类是线程安全的,而StringBuilder类不是线程安全的。
- 在单线程环境下,StringBuilder类的性能比StringBuffer类更高。
Java 异常机制知识:
异常处理机制主要回答了三个问题:
What:异常类型回答了什么被抛出
Where:异常堆栈跟踪回答了在哪抛出
Why:异常信息回答了为什么被抛出
Error和Exception的区别:
Java的异常体系:
从概念角度解析Java的异常处理机制:
Error:程序无法处理的系统错误,编译器不做检查。一般是指与JVM相关的问题,如系统崩溃,虚拟机错误,内存空间不足。
Exception:程序可以处理的异常,捕获后可能恢复
总结:前者是程序无法处理的错误,后者是可以处理的异常
RuntimeException:不可预知的,程序应当自行避免,比如数组下标越界,访问空指针等。使用if(value == null)来判断,避免问题。
非RuntimeException:可预知的,从编译器校验的异常,如果不处理就无法通过编译,比如I/O异常等。从责任角度看:
1.Error属于JVM需要负担的责任;
2.RuntimeException是程序应该负担的责任;
3.Checked Exception可检査异常是Java编译器应该负担的责任。
常见Error以及Exception:
RuntimeException:
1.NullPointerException-空指针引用异常
2.ClassCastException-类型强制转换异常
3.IllegalArgumentException-传递非法参数异常
4.IndexOutOfBoundsException-下标越界异常
5.NumberFormatException-数字格式异常
非RuntimeException:
1.ClassNotFoundException-找不到指定class的异常
2.IOException-IO操作异常
Error:
1.NoClassDefFoundError-找不到class定义的异常
2.StackOverflowError-深递归导致栈被耗尽而抛出的异常
3.OutOfMemoryError-内存溢出异常
NoClassDefFoundError的成因
1.类依赖的class或者jar不存在
2.类文件存在,但是存在不同的域中
3.大小写问题,javac编译的时候是无视大小写的,很有可能编译出来的class文件就与想要的不一样
Java的异常处理机制:
抛出异常:创建异常对象,交由运行时系统处理
捕获异常:寻找合适的异常处理器处理异常,否则终止运行
抛出异常后,最多只能被一个异常处理器处理。
Java异常的处理原则:
具体明确:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因;
提早抛出:应尽可能早的发现并抛出异常,便于精确定位问题
延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
高效主流的异常处理框架:
在用户看来,应用系统发生的所有异常都是应用系统内部的异常:
设计一个通用的继承自RuntimeException的异常来统一处理
其余异常都统一转译为上述异常AppException
在catch之后,抛出上述异常的子类,并提供足以定位的信息
由前端接收AppException做统一处理
org.springframework.core.NestedRuntimeException。
Java异常处理消耗性能的地方:
try-catch块影响JVM的优化
异常对象实例需要保存栈快照等信息,开销较大
BlockingQueue:提供可阻塞的入队和出队操作:
//尝试往队列尾部去添加元素,添加成功返回true,添加失败抛出异常信息。
boolean add(@NotNull E e);
//尝试往队尾添加元素,成功返回true,失败返回false。
boolean offer( @NotNull E e);
//尝试往队列尾部去添加元素,如果队列满了则阻塞当前线程一直到添加成功。
void put( @NotNull E e)throws InterruptedException;
//尝试往队列尾部添加元素,如果队列满了,则等待timeout时间,如果时间到了还没添加成功,则返回false。
boolean offer(E e,long timeout, @NotNull TimeUnit unit)
throws InterruptedException;
// 从队列头部取元素,如果队列为空,则一直尝试取元素直到成功为止。
@NotNull E take()throws InterruptedException;//尝试从队列头部获取元素,如果队列为空则等待timeout时间,如果时间到了还没取到头部元素,则返回空。
@Nullable E poll(long timeout, @NotNull TimeUnit unit)
throws InterruptedException;
//获取当前队列可获取元素数量。
int remainingCapacity();
//从队列中删除指定的对象。
boolean remove(Object o);
//查看队列中是否包含指定的对象
boolean contains(Object o);
// 把对象转移到指定的集合中。
int drainTo( @NotNull Collection<? super E> c);
如果空间满了,入队操作将阻塞直到有空间可用;
如果空间空了,出队操作将阻塞直到有元素。
主要用于生产者-消费者模式,在多线程场景时生产者线程在队列尾部添加元素,而消费者线程则在队列头部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。1、ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列;
2、LinkedBlockingQueue:一个由链表结构组成的有界/无界阻塞队列
3、PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
4、DealyQueue:一个使用优先级队列实现的无界阻塞队列
5、SynchronousQueue:一个不存储元素的阻塞队列;
6、LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
7、LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列;
Java的IO机制:
BIO、NIO、AIO的区别:
Block-IO:InputStream和OutputStream,Reader和Writer。
NonBlock-IO:构建多路复用的、同步非阻塞的IO操作。
NIO的核心:
Channels
Buffers
Selectors
NIO-Channels:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
transferTo:把FileChannel中的数据拷贝到另外一个Channel
transferFrom:把另外一个Channel中的数据拷贝到FileChannel
避免了两次用户态和内核态间的上下文切换,即”零拷贝’,效率较高.
NIO-Buffers:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Mapped
ByteBuffer
NIO-Selector:
IO多路复用:调用系统级别的select\polI\epoll:
select、poll、epoll的区别:
支持一个进程所能打开的最大连接数:
select底层是数组实现的,所以链接少。
FD剧增后带来的IO效率问题:
消息传递方式:
Asynchronous IO:基于事件和回调机制:
AIO 如何进一步加工处理结果:
基于回调:实现CompletionHandler接口,调用时触发回调函数
返回Future:通过isDone()查看是否准备好,通过get()等待返回数据
NIO适用于连接比较多,且连接短的架构,比如聊天服务器。
AIO使用与连接比较多,且连接长的架构,比如相册服务器。
IOC(Inversion of Control):控制反转
Spring Core最核心的部分
需要先了解依赖注入(DependencyInversion)
含义:把底层类作为参数传递给上层类,实现上层对下层的”控制。
依赖注入的方式:
Setter
Interface
Constructor
AnnotationIOC容器的优点:
避免在各处使用new来创建类,并且可以做到统一维护
创建实例的时候不需要了解其中的细节。
BeanFactory与ApplicationContext的比较:
BeanFactory是Spring框架的基础设施,面向Spring
ApplicationContext面向使用Spring框架的开发者
ApplicationContext的功能(继承多个接口):
BeanFactory:能够管理、装配Bean
ResourcePatternResolver:能够加载资源文件
MessageSource:能够实现国际化等功能
ApplicationEventPublisher:能够注册监听器,实现监听机制
refresh方法:
为IOC容器以及Bean的生命周期管理提供条件
刷新Spring上下文信息,定义Spring上下文加载流程
getBean方法的代码逻辑:
转换beanName
从缓存中加载实例
实例化Bean
检测parentBeanFactory
初始化依赖的Bean
创建Bean
Spring Bean的作用域:
singleton:Spring的默认作用域,容器里拥有唯一的Bean实例。
prototype:针对每个getBean请求,容器都会创建一个Bean实例。
request:会为每个Http请求创建一个Bean实例。
session:会为每个session创建一个Bean实例。
globalSession:会为每个全局Http Session创建-个Bean实例该作用域仅对Portlet有效。
Bean的生命周期:
创建过程:
实例化bean
Aware(注入Bean IDBeanFactory和AppCtx
BeanPostProcessor(s)postProcessBeforenitialization
InitializingBean(s).afterPropertiesSe
定制的Bean init方法
BeanPostProcessor(s)postPcocessAfterInitialization
Bean初始化完毕
销毁过程:
若实现了DisposableBean接口,则会调用destroy方法
若配置了destry-method属性,则会调用其配置的销毁方法
Spring AOP知识:
面向切面编程AOP正是此种技术的体现
通用化功能代码的实现,对应的就是所谓的切面(Aspect)
业务功能代码和切面代码分开后,架构将变得高内聚低耦合
确保功能的完整性:切面最终需要被合并到业务中(Weave)
AOP的三种织入方式:
编译时织入:需要特殊的Java编译器,如AspectJ
类加载时织入:需要特殊的Java编译器,如AspectJ和AspectWerkz
运行时织入:Spring采用的方式,通过动态代理的方式,实现简单
AOP的主要概念:
Aspect:通用功能的代码实现
Target:被织入Aspect的对象
Join Point:可以作为切入点的机会,所有方法都可以作为切入点
Pointcut:Aspect实际被应用在的Join Point,支持正则
Advice:类里的方法以及这个方法如何织入到目标方法的方式
Weaving:Aop的实现过程
Advice的种类:
前置通知(Before)
后置通知(AfterReturning)
异常通知(AfterThrowing)
最终通知(After)
环绕通知(Around)
AOP的实现:JdkProxy和Cglib:
由AopProxyFactory根据AdvisedSupport对象的配置来决定
默认策略如果目标类是接口,则用JDKProxy来实现,否则用后者
JDKProxy的核心:InvocationHandler接口和Proxy类
Cglib:以继承的方式动态生成目标类的代理
JDKProxy:通过Java的内部反射机制实现
Cglib:借助ASM实现
反射机制在生成类的过程中比较高效
ASM在生成类之后的执行过程中比较高效
代理模式:接口 +真实实现类 +代理类
真实实现类的逻辑包含在了getBean方法里
getBean方法返回的实际上是Proxy的实例
Proxy实例是Spring 采用 JDK Proxy 或 CGLIB 动态生成的
ACID
隔离级别
事务传播