Java工程师面试脑图
2020-11-26 11:16:06 0 举报
AI智能生成
Java工程师面试脑图
作者其他创作
大纲/内容
LinuxOS
Linux常用命令
Shell脚本编程
中间软件
Redis
使用场景
4+X
注意事项
缓存雪崩
大量可以在同一时间失效
Redis集群宕机
缓存穿透
缓存击穿
热点key问题
缓存和数据库双写一致性问题
用作分布式锁的缺陷问题
丢数据问题
主从复制
脑裂
技术选型
数据结构
集群
持久化
内存使用率
效率
线程模型
常用数据结构
常用数据结构及其使用场景
常用数据结构及其实现内幕
String
OBJ_ENCODING_INT
OBJ_ENCODING_EMBSTR
OBJ_ENCODING_RAW
Hash
OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_HT
扩容
数组第一纬的长度(触发扩容,但是bgsave进行时不触发)
数组第一纬长度的5倍(强制触发扩容)
就是HashMap的size和Node[]的长度比较
缩容
不足数组第一纬大小的10%
概要
渐进式rehash
两个触发条件
List
OBJ_ENCODING_QUICKLIST
Set
OBJ_ENCODING_INTSET
ZSet
OBJ_ENCODING_SKIPLIST + OBJ_ENCODING_HT
底层数据结构
8个字节的长整型
long
SDS
embstr
raw
ziplist
quicklist
dict
intset
skiplist
内存模型(RedisObject)
RedisServer
RedisDb
dictht
dictEntry
key
value
那个图
缓存过期策略
惰性删除
定期删除
缓存淘汰机制
缓存淘汰算法
数据持久化机制
rdb
aof
aof重写流程
主从复制原理
全量复制
部分复制
异步复制
集群原理
slot
RocketMQ
技术选型(RabbitMQ、RocketMQ、KAFKA)
从几个点额外对比RocketMQ和KAFKA
大数据场景,如日志
业务与系统
集群协调
KAFKA:ZK
RocketMQ:NameServer
性能方面
纯性能对比
KAFKA消息打包批量压缩传输
RocketMQ消息单条传输(大于4k也会压缩),业务系统嘛
也支持批量
topic数量与性能
topic数量多,单机的partition也必然多
单机64个partition是个转折点
开发语言
社区活跃度
基本功能
都支持
扩展功能
顺序消息
延时消息
事务消息
只有RocketMQ支持事务消息
消息回溯
KAFKA支持
RocketMQ支持
RabbitMQ不支持
消息查询
只有RocketMQ支持消息查询
高性能
单请求耗时
单机吞吐量
高可用
可伸缩
RabbitMQ完败
MQ原理
MQ实现
基本功能
消息发送
同步
CountdownLatch
异步
SendCallback
消息存储
数据库存储
并发问题
单机10w级别 = 50个
文件存储
KAFKA
partition
consumer_offsets
commitlog
消息
consumequeue
正常topic队列
延时topic队列
消费组重试队列
死信队列
config
消息消费offset
index
过期文件删除机制
定时任务没隔10分钟检查过期文件
过期文件删除
默认每天四点
存储空间剩余不足10%
手动执行删除命令
消息消费
订阅消息
TopicA
%RETRY%ConsumerGroupX
指定发送的哪个Broker+哪个ConsumerQueue
普通消息轮询Broker+ConsumerQueue发送
消费失败了咋办?
SCHEDULE_TOPIC_XX
延时级别
架构设计
写:顺序写 + page cache(页缓存) + mmap(0 内核空间缓存区和用户空间缓缓冲区之间的拷贝)
读:基于Netty,0 CPU 拷贝 -> sendfile(Linux 2.6.23 之后)
单条消息大于4k压缩传输
批量传输
KAFKA常用
RocketMQ一般不用
集群协调高可用
服务端高可用
acks=-1,要求写leader和ISR集合中的副本都成功才算法成功
副本数为3 + min.insync.replicas=2(ISR集合中的最小副本数)
unclean.leader.election.enable=false 不要从非ISR集合中选出leader副本
刷盘策略为同步刷盘:log.flush.interval.messages 和 log.flush.interval.ms
同步刷盘
同步复制
消息全链路可靠传输
生产者异步+回调
服务端同步复制+同步刷盘
消费者手动ACK
加机器就可以扩容
加机器就可以提高吞吐量
NameServer
分布式集群协调(类似ZK,又比ZK简单)
几个Map
topicQueueTable
brokerAddrTable
brokerLiveTable
Broker管理
发现
移除
Routing管理
常见问题
消息积压的处理方案
MQ集群宕机的简单备用方案
异地多活根本解决
Zookeeper
命名服务
注册中心
配置中心
分布式锁
分布式队列
分布式协调
集群HA
为什么旧的leader重新连回来之后不根据epoch判断将自己改为Follower
羊群效应
工作原理
从Paxos到ZAB
ZAB协议
广播模式
恢复模式
Leader选举机制
epoch
zxid
sid
节点角色
Leader
Learner
Follower
Observer
节点状态
Leading
Following
Observing
全局事务
高低位
副本机制
读写流程
$7.8
持久机制
$7.9$8.6.1
事务日志
数据快照
多少次记录事务日志后dump快照
系统模型
数据模型
znode
树形结构
数据结构(DataTree + DataNode)
节点类型
监听机制
exists
getData
getChildren
典型的Reactor主从模型
性能分析
16核32G:单机几万QPS
每个节点都可以读
适合小集群部署
半数机制
写请求无法扩展
适合读多写少的场景
Observer节点扩展
ElasticSearch
倒排索引
term
term dictionary
term index
trie树
posting list
docId集合
每个字段对应一个倒排索引
写入时,refresh(每秒)过程就会创建索引
集群组建
数据分片几副本
故障恢复
集群扩容
分片数量
副本数量
副本复制原理
读写原理
写
读
查询
搜索
性能优化
利用操作系统缓存(1:1)
索引只存必要的字段(ES+MySQL或HBase)
数据预热
冷热分离
避免复杂的关联查询
设计合理的Document模型(mapping)
分页优化
web前端性能优化
终端优化
传输优化
反向代理
动静分离
CDN加速
应用服务性能优化
缓存
代码
并发编程
资源复用
分析工具
Arthas
Btrace
Byteman
JVM
数据存取性能优化
SQL优化
索引优化
数据库架构+分库分表
架构技能
分布式架构
Keepalive+Nginx/LVS
RPC
Dubbo
SpringCloud
服务治理
服务熔断
服务降级
服务限流
服务隔离
MQ
Memcached
分布式数据一致性
微服务架构
Docker
数据库架构
主备架构
主从架构
双主架构
架构的要素
可扩展
安全性
BigData
数据收集
网络爬虫
Flume/Logstash/Beats
Flume+KAFKA+ELK
数据存储
HDFS
Hive
Hbase
MongoDB
数据检索
Elasticsearch
数据处理
MapReduce
Storm
Spark
Flink
数据挖掘
机器学习
解决方案
技术解决方案
业务解决方案
其他技能
开发工具
Intellij IDEA
Eclipse
项目构建
Maven
Gradle
版本控制
Git
SVN
Java工程师知识图谱
专业基石
4+1+2+1+3=11
算法
算法思想
分治算法
贪心算法
回溯算法
动态规划
常用算法
3+1+X
LRU&LFU
Snowflake
限流算法
负载均衡算法
一致性Hash算法
TopK算法
Java基础
基础
反射
注解
Stream
ThreadLocal
线程隔离资源
无法解决共享资源的更新
线程生命周期内,可以在任意地方获取使用
用户登录后存一个token到ThreadLocal,之后哪里用到哪里获取
Spring声明式事务
thread.threadLocals
ThreadLocal.ThreadLocalMap
Entry extends WeakReference<ThreadLocal<?>>
Entry[]
set
threadLocalMap.set
get
threadLocalMap.getEntry
remove
threadLocalMap.remove
initialValue
默认是返回null
可以在创建ThreadLocal时传入一个initialValue的实现
这是一个延迟调用方法,在未set先get时调用
重要的点
早期版本和8之后的版本对比
如果用的是线程池+静态的Class生命周期的ThreadLocal,那么真没多大区别
真要强说区别,就是1.7及之前,如果线程很多,ThreadLocal的ThreadLocalMap会很大
通常ThreadLocal没有线程多
nextHashCode()
ThreadLocalMap
用完remove
集合
ArrayList
Vector
CopyOnWriteArrayList
CopyOnWriteArraySet
LinkedList
SynchronizedList
Collections.synchronizedList(list)
Map
HashMap
四个点
hash函数
hash冲突+防止复杂度退化
负载因子+扩容
安全问题
内部结构
Node
TreeNode extends LinkedHashMap.Entry extends HashMap.Node
Node[] + Node链表或TreeNode红黑树
常用方法
put
size
hash值高16位和低16位异或
为什么要这么设计
hash冲突
ThreadLocalHashMap
防止复杂度退化 -> 红黑树(>=8&>=64)
扩容策略
优化死循环
hash&oldCap
合理设置初始容量
线程安全问题
HashTable
ConcurrentHashMap
ForwardingNode extends Node(MOVED=-1)
扩容时,大用处
TreeBin extends Node(TREEBIN=-2)
有料,控制读写并发问题
waiter:Thread
lockState:int
1,2,4的巧妙设计,想一下位就明白了
WRITER(1)
WAITER(2)
READER(4)
TreeNode extends Node
ReservationNode extends Node(RESERVED=-3)
volatile int sizeCtl
0
-1
-N
正数
size&mappingCount
sumCount
线程安全
写写(put)
table为空,自旋+cas初始化
Node[x]为null(bin的头节点为null),自旋+cas插入
Node[x]为ForwardingNode,帮助扩容
其他情况(Node[x]为Node或TreeBin时),synchronized锁
读写
链表时,没问题
红黑树时,有问题
sizeCtl
transferIndex
多线程扩容时,按stride分配bin给线程
transferIndex=oldTab.length; transferIndex-= stride;
helpTransfer
transfer
开始扩容之前都做了哪些工作
sizeCtl+1
设置nextTable
计算transferIndex
1. 空结点或迁移完的桶(bin)的头结点上插入forward节点
2. 桶(bin)搬移的时候,将 bin 的头结点锁住
3. 最后一个扩容线程会负责重新检查一遍数组查看是否有遗漏的桶
判断最后一个扩容线程就是看sizeCtl是否等于启动扩容时设置的基数((rs << RESIZE_STAMP_SHIFT) + 2)
4. 扩容结束后,将 table 指向新数组,sizeCtl 设置为扩容阈值
负载因子默认0.75f不可通过构造方法设置
容量计数(baseCount&counterCells)
类似LongAdder
为什么要用LongAdder而不用AtomicLong
LinkedHashMap
LinkedHashMap.Entry extends HashMap.Node
Entry[] + Entry链表或红黑树
head&tail
newNode -> linkNodeLast -> 将新节点添加到链表尾节点
afterNodeInsertion -> removeEldestEntry -> 若达到容量,则将头节点删除
afterNodeAccess(需要构造方法传入accessOrder=true) -> 把被访问的节点调到尾部
get -> afterNodeAccess(需要构造方法传入accessOrder=true)-> 把被访问的节点调到尾部
remove -> afterNodeRemoval -> 将删除的节点从链表中移除(由于结构设计的好,只要改变前后指针即可)
排序不是常规的按照某种方式排序,只是把最近使用的放到队尾
Entry extends WeakReference<ThreadLocal<?>>
replaceStaleEntry
找到key为null的Entry,将新的Entry放到这个位置
清理过去数据
cleanSomeSlots
expungeStaleEntry
清理过期数据
碰到正常数据,重新计算索引位置
getEntry
getEntryAfterMiss
hash算法
private final int threadLocalHashCode = nextHashCode();
每个ThreadLocal对象只有一个hash值,创建ThreadLocal对象时就定了
nextHashCode() -> return nextHashCode.getAndAdd(HASH_INCREMENT);
初始为0(nextHashCode)
静态方法,每调用一次(new ThreadLocal())就在原来值(nextHashCode)的基础上加上一个Fibonaci数作为新的hash值
Fibonaci数是一个跟Fibonaci数列有关的黄金分割数,就为了散列更均匀(最大程度散列到一个2的n次方的数组里)
线性探测法(开放寻址法的一种)
这种方式不适合数据量大的情况
扩容前会先进行过期数据的清理
没有桶,就是数组扩容
size不宜太大
hash冲突解决办法决定 + 常用方法决定
TreeMap
红黑树
key没有hash
key不能重复
HashSet
TreeSet
就是在插入数据时,先遍历一下数组快照,看是否存在,其他就是调用CopyOnWriteArrayList相关方法(addIfAbsent)
Stack
先进后出
数组
Stack extends Vector
push(E)
pop()
数据量不要很大
线程安全,但是效率不高
Queue
用ReentrantLock保证安全,没多大意思
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
ConcurrentLinkedQueue
volatile + cas + 自旋
适合并发不大的情况
SynchronousQueue
ArrayDeque
双端队列,非线程安全,只有顺序实现,没有链式实现
IO/NIO
BIO
阻塞(accept()、read())-> 连接:线程=1:1 -> C10K问题
阻塞唤醒:硬中断或软中断
NIO
非阻塞(accept()、read())+ 自旋(多个连接可以用一个线程) -> 海量syscall -> 海量用户空间和内核空间切换和数据拷贝
零拷贝
多路复用IO
man 2 select | poll | epoll(需要yum install -y man-pages)
select
poll
epoll
信号驱动IO
AIO
+ Reactor主从模型(IO多路复用模型)= Netty
网络编程
网络模型
TCP三次握手和四次挥手
为何需要三次握手
拆包粘包问题
http v.s. https
网络问题排查
并发基石
JMM
MESI
MESI底层实现原理
内存屏障
cas
原子性
ABA
底层cas是没有ABA问题,上层应用cas才有ABA问题
上层cas
1、获取共享变量
2、修改共享变量
3、cas回写共享变量
compareAndSwap -> 调底层cas操作
volatile
可见性
JVM层面
底层层面
有序性
synchronized
JVM底层实现(monitorentry(),monitorexit())
偏向锁
为什么要引入偏向锁
加锁流程
重入
解锁流程
升级流程
撤销偏向锁为什么要等到safe point再执行
轻量级锁
为什么要引入轻量级锁
重量级锁
可能触发升级重量级锁的情况
轻量级锁升级
wait()
hashCode()
objectMonitor(mutex结构体)
_owner
cas操作_owner
lock前缀指令
_recursions
三个队列
_cxq
_EntryList
_WaitSet
锁优化
偏向锁 -> 轻量级锁(自旋 -> 自适应自旋) -> 重量级锁
锁粗化 -> 连续获取同一把锁
锁消除 -> 对局部变量加锁
锁分段
锁分解
锁自动超时(lock)
可中断锁(lock)
synchronized对比ReentrantLock(3+3+1)
公平?
限时?
中断?
读写锁
更灵活
线程状态
Condition更灵活
一个Lock可以创建多个Condition
偏向锁状态下
synchronized优于ReentrantLock
轻量级锁状态下
自旋次数
CAS操作
重量级锁状态下
基本没多大区别,套路都是一样的
整体来说没多大区别,如果不是灵活控制的场景就用synchronized
线程基础
Java线程实现
用户线程和系统轻量级进程(或线程)1:1映射实现
NEW
RUNNABLE
READY
RUNNING
WAITING
TIMED_WAITING
BLOCKED
TERMINATED
线程中断机制(一种协作机制)
Thread.currentThread().interrupt() -> 设置线程的中断状态为true
没有任何效果
只会设置线程中断状态为true
响应中断
一旦响应中断,抛出InterruptedException,线程的中断状态就会清除,为false -> 对吗?
中断抛异常的情况
sleep(time)
wait()&wait(time)
join()&join(time)
中断不抛异常的情况,只是唤醒线程,并设置中断标记
LockSupport.park()
LockSupport.parkNanos(time)
LockSupport.parkUtil(time)
join()
lock.lockInterruptibly()
wait(time)
join(time)
lock.tryLock(time)
Thread.currentThread().isInterrupted() -> 仅仅返回线程的中断状态
Thread.interrupted() -> 返回线程的中断状态并将中断状态设置为false
多线程
线程池
如何设置线程池的大小
计算密集型
n+1
IO密集型
那个公式
线程池原理
线程池状态(ctl:高3位-线程池状态,低29位-线程池有效线程数)
RUNNING(-1)
TIDYING(2)
TERMINATED(3)
SHUTDOWN(0)
STOP(1)
各种参数
corePoolSize
maximumPoolSize
keepAliveTime+timeUnit
workQueue
threadFactory
handler
先后顺序
corePoolSize > workQueue > maximumPoolSize > handler
线程池实现
ThreadPoolExcutor
ctl:AtomicInteger
workQueue:BlockingQueue<Runnable>
workers:HashSet<Worker>
Worker
thread
Worker继承了AQS,执行任务前加锁,完事解锁
这个锁感觉就是为了表示Worker在工作
启动流程
addWorker
new Worker(..).thread.start():thread是线程工厂创建的Thread传入的Runnable是Worker
worker.run():Worker也是Runnable
worker.runWorker():步入正轨
getTask():。。
allowCoreThreadTimeOut
execute(..)
addWorker(..)
remove(..)
reject(..)
shutdown() -> SHUTDOWN
advanceRunState(SHUTDOWN)
停止接收新的任务
interruptIdleWorkers()
遍历workers,并interrupt空闲的线程(w.tryLock()成功才中断)
idle的工作线程会在getTask方法中等待从workQueue中获取任务,如果此时线程接到interrupt会响应中断,进而线程退出
processWorkerExit(..)
tryTerminate()
工作中的线程则会继续工作,直到工作队列空了,再响应中断,退出线程
工作线程空了,队列空了,紧接着清理,然后终止
SHUTDOWN -> TIDYING -> TERMINATED
shutdownNow():List<Runnable> -> STOP
advanceRunState(STOP)
interruptWorkers()
遍历workers,并interrupt
tasks = drainQueue()
将等待执行的任务出队,保存到tasks,并返给调用方
这一步就能保证,后续把interrupt的线程给退出
STOP -> TIDYING -> TERMINATED
prestartCoreThread()
预先启动一个核心线程
prestartAllCoreThreads()
预先启动所有核心线程
allowCoreThreadTimeOut(..)
允许核心线程超时退出
ScheduledThreadPoolExecutor
ScheduledFutureTask
负责包装任务和任务执行后从新加入到任务队列(延时、周期)
DelayedWorkQueue
Leader/Follower模式
堆
无界,可以扩容
take()
schedule(..)
scheduleAtFixedRate(..)
scheduleWithFixedDelay(..)
几个点吧
Leader定时挂起
Follower逐个唤醒
Timer与ScheduledThreadPoolExecutor的比较
绝对时间,相对时间
单线程,多线程
异常处理
ScheduledThreadPoolExecutor对比ThreadPoolExecutor
先创建Worker还是先入队列
很好理解,延时
ScheduledThreadPoolExecutor正常情况下maximumPoolSize没用,除非corePoolSize为0,此时会创建一个线程来执行,脑残才会这么用
线程池使用心得
自定义线程池ExecutorService executor = new ThreadPoolExecutor(...)
用ExecutorService 是因为它既有submit也有execute方法
线程工厂生产有意义名称的线程
方便排错
注意线程池吞异常的问题
任务的run()自己抓住处理
这个好
重写ThreadPoolExecutor的afterExecute(..)方法
很少用
submit获取线程池执行结果的原理
构建FutureTask并返回
private Callable<V> callable;
若submit的是Runnable,则先适配成Callable,再设置callable属性,并可以指定返回值,默认返回null
若submit提交的是Callable,则直接设置callable属性
private Object outcome;
线程池问题分析与解决思路
使用无界队列带来的问题
使用无限线程带来的问题
有限队列,有限线程时,如何自定义拒绝策略
还是根据业务场景
扔到MQ
持久化到任务表
机器宕机如何防止队列中的任务丢失
适合有状态的任务
sleep和wait的区别
4+1
wait会将锁升级为重量级锁,sleep不会
如何获取多线程的执行结果
并行线程把结果写到一个地方(Result),再去取,还可以结合CountdownLatch和ReentrantLock.Condition
Runnable + Future + Callabl = Futhertask
state:状态及流转
NEW(0)
COMPLETING(1)
NORMAL(2)
EXCEPTIONAL(3)
CANCELLED(4)
INTERRUPTING(5)
INTERRUPTED(6)
run()
set():NEW -> COMPLETING -> NORMAL
finishCompletion
setException():NEW -> COMPLETING -> EXCEPTIONAL
cancel(boolean)
逐个唤醒挂起的线程
true:NEW -> INTERRUPTING -> INTERRUPTED
false:NEW -> CANCELLED
get()/get(time)
report(awaitDone:返回状态):根据状态返回结果
当前线程是否中断?
state > COMPLETING?
state == COMPLETING?
入队挂起当前线程
WaitNode链表
入队出队:CAS头节点
原子类
LongAdder
为什么不用AtomicLong而用LongAdder
base&cells
Cell
value:long
cellsBusy
cas锁
创建cells并初始化一个cell
创建cell
add(long x)
increment() -> add(1L)
decrement() -> add(-1L)
sum() -> base + sum(cells)
热点数据拆分思想
线程hash值存在一个ThreadLocal(LongAdder#threadHashCode)中
初始是一个随机整数
并发冲突时,改变线程的hash值,从新索引cell
cells初始容量为2,每次扩容翻倍,直到大于cpu核数不再扩容
AtomicLong
AtomicLongArray
AtomicReference<E>
AtomicReferenceArray<E>
AtomicReferenceFieldUpdater
AtomicStampedReference<E>
解决ABA问题
AQS(AbstractQueuedSynchronizer)
state
独占模式下
=0:代表没有线程占用锁
>0:代表有线程占用锁,具体数值代表重入的次数,释放时需要一一减掉,确保回到0
共享模式下
<0:代表获取锁失败
=0:代表获取锁成功,但没有剩余可用资源
>0:代表获取锁成功,且有剩余资源
head
tail
waitStatus
CANCELLED(1)
SIGNAL(-1)
CONDITION(-2)
PROPAGATE(-3)
Node SHARED = new Node()
新建Node时,赋值给nextWaiter
Node EXCLUSIVE = null
prev
next
nextWaiter
等待队列,单向链表
ConditionObject
firstWaiter:head
lastWaiter:tail
核心方法
实现方法
acquire
tryAcquire
acquireQueued
addWaiter
Node.EXCLUSIVE
尝试获取成功?setHead:继续
shouldParkAfterFailedAcquire
parkAndCheckInterrupt
挂起,只记录中断标记
cancelAcquire
被中断或者限时挂起到时间,取消
acquireInterruptibly
doAcquireInterruptibly
挂起,响应中断,抛异常
tryAcquireNanos
doAcquireNanos
release
tryRelease
unparkSuccessor
清理取消的节点,并唤起最头上,满足条件的线程
acquireShared
tryAcquireShared
doAcquireShared
Node.SHARED
尝试获取成功?setHeadAndPropagate:继续
acquireSharedInterruptibly
doAcquireSharedInterruptibly
doReleaseShared
tryAcquireSharedNanos
doAcquireSharedNanos
releaseShared
tryReleaseShared
模版方法
常用实现
Lock
ReentrantLock
FairSync extends Sync extends AQS
NonfairSync extends Sync extends AQS
syn
lock
模版方法实现
sync.lock
lockInterruptibly
tryLock
tryLock(time)
unlock
ReentrantReadWriteLock
sync
tryReadLock
tryWriteLock
写写互斥很好理解,读写互斥设计的很巧妙
高16位是读锁标记,低16位是写锁标记
如果state等于0,那么既没有读锁,也没有写锁
ReadLock
WriteLock
FairSync extends Sync extends AQS
NonfairSync extends Sync extends AQS
writeLock
readLock
newCondition:ConditionObject
await
释放锁,加入等待队列,挂起
await(time)
释放锁,加入等待队列,限时挂起
signal
从头开始,将第一个CONDITION状态(非取消状态)的节点(线程)移到同步队列,如果状态为CANCEL或者cas修改前置节点为SIGNAL失败,则唤醒
signalAll
从头开始,将所有CONDITION状态(非取消状态)的节点(线程)移到同步队列,如果状态为CANCEL或者cas修改前置节点为SIGNAL失败,则唤醒
Semaphore
acquireUninterruptibly
tryAcquire(time)
CountDownLatch
Sync extends AQS
countDown
并发编程思维模型
是否必须共享变量?
是否可以用final修饰?
是否可以线程封闭
是否可以volatile+cas自旋
是否可以使用基于AQS实现的各种安全组件
BlockingQueue(Lock)
CopyOnWriteArrayList(Lock)
ConcurrentHashMap(CAS+Lock)
使用synchronized锁
编译器
.java --javac--> .class
类加载
类加载的过程
加载
通过类的全限定名,获取类的二进制字节流
解析类的二进制字节流所描述的静态数据结构为方法区中的运行时数据结构
在堆中创建一个java.lang.Class类型的对象来存储类的信息数据
验证
两个文件规范
一个程序语义
符号引用验证
准备
静态变量分配空间,并赋初值
初值,比如int的初值是0
解析
将常量池中的符号引用解析为直接引用:#xx -> 具体内存地址
初始化
<clinit>()
静态变量赋值
静态代码块执行
初始化的几个时机
访问一个类的静态属性,或为静态属性赋值(被final修饰、已在编译期把结果放入常量池的静态字段除外)
访问一个类的静态方法
new一个类的实例
初始化一个类的时候,若发现父类还没有初始化,则需要先触发其父类的初始化
类包含main方法,使用java命令来运行某个主类
使用java.lang.reflect包内的方法对类进行反射调用的时候
基于静态内部类实现单例
JVM保证子类的<clinit>()执行之前父类的<clinit>()已执行
JVM保证一个类的<clinit>()在多线程环境中能正确执行(加锁)
双亲委派机制
模型
Custom ClassLoader
Application ClassLoader
Extension ClassLoade
Bootstrap ClassLoader
优点
避免同一个类的重复加载
安全考虑,java核心类库中定义的类,不会被随意替换加载
常见违反双亲委派模型的操作
SPI
JDBC(java.sql.Driver属于核心类库)
本来是由Bootstrap ClassLoader加载的但是,其具体实现是在第三方jar中,在classpath下,所以要想加载具体实现就得违反
热部署
类加载器
Bootstrap ClassLoader
%JAVA_HOME%\\jre\\lib\t.jar
Extension ClassLoader
%JAVA_HOME%\\jre\\lib\\ext\\*.jar
%ClassPath%\\*
自定义类加载器(CustomClassLoader extends ClassLoader)
遵循双亲委派机制
构造方法:super()或super(parent)
加载方法:调用loadClass()方法
违反双亲委派机制
super(null) -> loadClass()
重写并暴露findClass()方法 -> findClass()
热部署:用不同的自定义类加载器对象加载同名类
URLClassLoader是现成的
一个问题
判断Class对象是否相等,不仅要判断类的全限定名是否相同,还要判断是否是由同一个类加载器加载
运行时数据区
内存划分
线程共享
方法区
线程隔离
PC寄存器
java方法:记录当前线程正在指向的字节码指令的内存地址
native方法:空(Undefined)
栈
栈帧(方法)
局部变量表
操作数栈
动态连接
类是用到加载的,比如:方法中new一个对象,就会有类的加载过程,其中的解析过程就是动态连接
方法出口
附加信息
本地方法栈
对象探秘
对象的创建过程(X x = new X();)
加载?
分配内存
指针碰撞
空闲列表
多线程并发安全问题
CAS分配
并发竞争激烈:TLAB(Thread Local Allocation Buffer)
设置对象头
这一步完成之后,从虚拟机的视角来看,一个新的队形已经产生了
对象初始化
<init>()
执行构造方法
执行构造代码块
引用赋值给变量
new X();
对象的内存布局
对象头
MarkWord
类型指针
指向类元数据的指针
实例数据
对齐填充
对64位机器:8字节的整数倍或者64位的整数倍
对象的访问定位
句柄访问
reference -> 句柄(对象实例数据的执指针,对象类型数据的指针)
直接指针访问
reference -> 对象内存地址(对象实例数据,对象类型数据的指针)
垃圾收集器
各种引用
强引用
软引用
弱引用
虚引用
无法通过虚引用来取得一个对象的实例,为一个对象设置虚引用关联的唯一目的就是在这个对象被GC时收到一个系统通知
类回收条件
对象
类对象
ClassLoader
对象晋升原则
晋升年龄阈值
动态对象年龄
大对象
young gc时survivor放不下
GCRoot(三色标记算法)
静态变量
常量
局部变量
为什么把不使用的对象引用及时设置为null可以帮助gc
存在跨代引用的对象
和GC Root处于同一CardTable的对象
垃圾收集算法
复制算法
标记清除算法
标记整理算法
常用垃圾收集器
Parallel Scavenge + Parallel Old
-Xmn512m
-XX:SurvivorRatio=8
-XX:+UseAdaptiveSizePolicy
GC活动周期
常用参数
ParNew + CMS + Serial Old
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:UseCMSCompactAtFullCollection
-XX:CMSFullGCBeforeCompaction=5
-XX:+CMSPrecleaningEnabled
-XX:+CMSScavengeBeforeRemark
G1
Region(-XX:G1HeapRegionSize)
E
S
O
H
-XX:G1NewSizePercent
-XX:G1MaxNewSizePercent
-XX:+PrintAdaptiveSizePolicy
YoungGC:复制算法
MixedGC:复制算法
FullGC:标记清理 + 压缩整理 = 类似标记整理算法,多线程
-XX:+UseG1GC
-XX:MaxGCPauseMillis
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1HeapWastePercent=5
-XX:G1OldCSetRegionThresholdPercent=10
-XX:G1MixedGCCountTarget=8
-XX:G1ReservePercent
CMS对比G1
三色标记算法的优化(了解)
CMS:Incremental Update
所以在重新标记还会从头开始遍历一遍
内存越大越慢
G1:SATB(Snapshot At The Beginning)
还要结合RSet
ZGC:Colored Pointers(颜色指针)
通用参数
-Xms1g
-Xmx1g
日志
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCCause
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
-XX:+PrintReferenceGC
打印清理各类引用的时长
尤其注意FinalReference,重写了finalize方法
-Xloggc:/opt/java/xx/xx.gc
日志切割
按大小
按时间
OOM
-XX:+HeapDumpOnOutMemoryError
-XX:HeapDumpPath=/opt/heap/xx.hprof
TLAB
-XX:+UseTLAB
-XX:TLABSize=x
-XX:+PrintTLAB
-verbose:class
对象晋升
-XX:MaxTenuringThreshold=15
-XX:TargetSurvivorRatio=50,动态年龄判定用,你懂得
-XX:PretenureSizeThreshold=0,默认0,不管多大都优先则Eden分配
禁用System.gc()
-XX:+DisableExplictGC
关于YoungGC
触发条件
Eden满了
对于G1
-XX:SurvivorRatio
-XX:MaxGCPauseMillis
YoungGC过于频繁
统计单位时间产生的对象的速度
是否是必要的对象在产生
这个问题一般很简答,就是Eden小了
YoungGC耗时过长
GCRoot存活对象标注?
新生代空间太大
对象引用链较长,可达性分析时间长
复制存活对象耗时?
GC时晋升的对象,存留的对象和回收的对象具体值
新生代Survivor区太小,清理后剩余的对象不能装进去需要移动到老年代,造成移动开销
存活对象太多
copy到s区
移动到老年代,动态年龄
是否有大对象
如果设置了总是在eden分配对象,gc时大对象很可能会导致动态年龄晋升到老年代
系统负载高?
gc线程等待
中小公司多个java应用部署到一台服务器
gc线程切换执行
设置的gc线程多余cpu核心数
safepoint?
safepoint的执行一共可以分为四个阶段
Spin阶段
等待未在安全点上的用户线程进入安全点
Block阶段
即使进入safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞。
Cleanup阶段
这个阶段是JVM做的一些内部的清理工作
比如,撤销偏向锁
VM Operation阶段
JVM执行的一些全局性工作
例如GC
重点关注RevokeBias或者BulkRevokeBias
并发高点的情况下,线程交替进入,synchronized锁块,导致偏向锁撤销,在安全点执行
-XX:PrintSafepointStatisticsCount=1
关于FullGC
YoungGC前,老年代可用空间小于历次YoungGC后晋升老年代对象的平均大小
YoungGC后,老年代不能容纳晋升对象
MixedGC时,没有空闲的Region可以承载存活的对象
Metaspace可用空间不足
System.gc()
FullGC之于垃圾收集器
SerialOld(老年代,单线程)-- FullGC
ParallelOld(老年代,多线程)-- FullGC
CMS(老年代,多线程)-- 2次FullGC(CMS GC过程中有两次STW)
G1(全堆,多线程)-- FullGC
FullGC过于频繁
主要是看是否有大量短生命周期的对象晋升到老年代
有对象晋升时,对象的年龄分布
FullGC时回收了多少
执行引擎
常用命令
jinfo
jstat
jstack
jmap
问题排查
GC
在jvm里可以设置几个参数,jvm一旦oom,就会导出一份内存快照
分析快照,如jhat,mat -> 大对象、长期占用的对象
常见原因
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Metaspace
java.lang.StackOverflowError: 栈内存溢出
java.lang.OutOfMemoryError: Direct buffer memory
JVM调优
说个案例,详细说明过程:现象 -> 分析(工具) -> 方案 -> 实验 -> 确定
编码规范
JavaWeb
JSP
Servlet
Html
CSS
JavaScript
JQuery
模板引擎
Velocity
Freemarker
Tomcat
重构设计
设计原则
开闭原则总则
依赖倒置原则
接口隔离原则
订单接口类不要包含对用户增删改操作的接口(方法)
单一职责原则
XX接口类的单个接口(方法)不要承载过多的任务
里氏替换原则
最少知道原则
合成复用原则
常用设计模式
创建型模式
单例模式
(C)Singleton
工厂模式
(I)Product
(I)/(C)Creator
结构型模式
代理模式
(I)Subject
(C)RealSubject
(C)Proxy
装饰模式
(I)Component
(C)ConcreteComponent
(I)Decorator
适配器模式
(I)Target
(C)Adapter
(C)Adaptee
行为型模式
策略模式
(C)Context
(I)Strategy
命令模式
(C)Invoker:线程池
一点点变形,这里面既有任务接受者,也有任务
(I)Receiver:执行线程
(I)Command:任务队列
(C)Client
创建线程池
创建并且提交任务
命令开始执行任务(当然线程池这一步是线程run,不给Client控制)
责任链模式
构建责任链
开始过滤
(I)Handler
一个例子
Request:敏感词过滤 -> 登录认证 -> 访问频率 -> 权限控制...
以后可能扩展加解密环节
结合工厂来构建责任链很好使
观察者模式
(I)Observer
一个例子就是Zookeeper
模板方法模式
(C)AbstractClass
(C)ConcreteClass
重构-改善既有代码的设计
开源框架
NET框架
Netty
从NIO到Netty
Channel
Netty Channel 对比 JDK Channel
Netty Channel 包装 JDK Channel + 非阻塞设置
EventLoop与线程模型
NioEventLoop
其实是只有一个线程的线程池
有任务队列
NioEventLoopGroup(线程池)
内部真正的工作线程不随着NioEventLoop创建而创建,而是有任务进来时,惰性创建
任务队列优化
使用JCTools提供的Queue代替JDK的LinkedBlockingQueue
JCTools根据场景对应不用前缀的Queue
猜测,核心点还是volatile+cas自旋来实现Queue
ChannelHandler与ChannelPipeline
Pipeline模式(本质上是责任链,管道+可插拔处理单元=类比JDK Stream)
例如In事件的处理(Http为例)
第一个数据处理单元(Handler):解码器解码,按照响应的协议解析
Http -> Json
第二个数据处理单元(Handler):转换数据结构
Json -> POJO
第三个数据处理单元(Handler):加密字段解密
第四个数据处理单元(Handler):业务逻辑处理器(自定义BusinessHandler)
Netty对Pipeline的优化(处理单元向后传递时跳过没有响应事件的Handler,避免空调用)
每个处理单元的Handler都有一个int类型的MASK字段,
核心工作流程
客户端:bootstrap.connect(..)/服务端:bootstrap.bind(..)
创建Channel对象
初始化Pipeline
将Channel注册到NioEventLoop中
就是将Netty Channel中维护的JDK NIO Channel注册到NioEventLoop中维护的JDK NIO Selector对象中
NioEventLoop的工作原理(包装的Selector,很好理解)
Selector嘛,先来个无限循环
Selector#select还是Selector#selectNow?
看循环内有没有Netty本地工作执行,这很好理解,一个阻塞,一个非阻塞嘛
处理SelectKeys集合
通过Pipeline处理,Socket事件
Mina
HttpClient
HTTPClient调用优化
httpClient复用
连接池化
长连接
MVC框架
Spring
Spring核心
Spring IOC
生命周期
循环依赖
为什么要用三级缓存?
扩展点(常用举例)
@Component + BeanPostProcessor
太多
@Component + FactoryBean
Dubbo客户端启动
@Import + Registrar
AOP
@Import + Selector
TX
@Component + ApplicationListener
Dubbo服务端启动
Spring AOP
AspectJAutoProxyRegistrar
AnnotationAwareAspectJAutoProxyCreator
BeanFactoryAware
setBeanFactory
SmartInstantiationAwareBeanPostProcessor
postProcessBeforeInstantiation
postProcessAfterInitialization
动态代理
Spring如何选择哪种动态代理(别忘了可以强制使用CGLib)
还有哪些动态代理
不同动态代理的区别
JDK Proxy必须有接口
CGLib和javassist都是修改字节码,创建一个新的子类,如出一辙,要强说一点就是javassist的api更丰富
Spring TX
TransactionManagementConfigurationSelector
AutoProxyRegistrar
InfrastructureAdvisorAutoProxyCreator
ProxyTransactionManagementConfiguration
transactionAdvisor
transactionAttributeSource(识别@Transaction,并解析其属性)
transactionInterceptor
txManager
Spring MVC
Spring 源码
为什么读Spring源码?
设计模式的优秀运用
扩展点优秀设计思想
有利于工作中的排错
SpringBoot
服务调用
服务网关
Struts
ORM框架
MyBatis
四层架构
接口层
数据处理层
参数解析,SQL解析,SQL执行,结果处理
Configuration
(Map)mappedStatements
Mapper接口方法和Mapper文件增删改查标签(MappedStatement)的映射
MapperRegistry
(Map)knownMappers
Mapper接口和MapperProxyFactory的映射
。。。
Executor
根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;
为查询创建缓存(一级缓存)
创建Statement对象
ParameterHandler
StatementHandler
预编译Statement对象为PreparedStatement
PreparedStatement执行数据库操作并得到结果集
ResultSetHandler
框架支撑层
数据源
连接池
事务
引导层
核心流程
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream为mybatis-config.xml文件流
SqlSession session = sqlSessionFactory .openSession(true);
XxMapper xxMapper = session.getMapper(XxMapper.class); // 创建代理
创建代理对象
SqlSession#getMapper -> Configuration#getMapper -> MapperRegistry#getMapper -> MapperProxyFactory#newInstance -> Proxy.newProxyInstance(MapperProxy(Java的InvocationHandler))
invoke方法
MappedStatement
xxMapper#增删改查
Spring集成
SqlSessionFactoryBean
sqlSessionFactoryBean.setConfigLocation:设置mybatis.xml配置信息
sqlSessionFactoryBean.setMapperLocations:设置mapper.xml配置信息
sqlSessionFactoryBean.setTypeAliasesPackage:设置实体包配置信息
sqlSessionFactoryBean.setDataSource:设置数据源
Hibernate
RPC框架
如何设计一个RPC框架
服务暴露
服务引用
容错
负载均衡
失败重试
监控
注册中心羊群效应
基于Zookeeper解决方案
Provider:注册到多个注册中心中
<dubbo:service registry=\
Consumer:不同的Consumer从不同的注册中心引用
Consumer1:<dubbo:reference registry=\"xx\" .../>
Consumer2:<dubbo:reference registry=\"yy\" .../>
Consumer3:<dubbo:reference registry=\"zz\" .../>
自研解决方案
Dubbo IOC AOP
Dubbo SPI
Dubbo SPI和JDK SPI的对比
Dubbo是k-v,JDK是列表(会加载无用的接口实现)
@Adaptive增强@SPI,有Adaptive标注在方法上,Dubbo会先为扩展接口生产一个代理,等真正用到的时候才会加载真正的扩展接口的实现
超时时间的设置艺术
服务端设置更合理 -> 超时时间分级设置 -> 超时时间设置多少合适 -> 是否重试 -> 不宜过多重试,失败过多时降级
Dubbo性能调优
设置合理的业务线程池的大小
业务线程的拒绝策略是报错,立即失败
超时重试
阅读Dubbo源码的心得
对spring的扩展点用的很溜
扩展设计:@SPI、@Adaptive
@SPI对比JDK SPI的优势
对Invoker设计(所谓的领域驱动设计)的一点见解
Dubbo二次开发 -> 修改服务引用和调用出错时异步拉取服务配置信息
服务引用时
CustomRegistryFactory implements ZookeeperRegistryFactory
CustomRegistry implements ZookeeperRegistry
服务调用时
CustomFailoverCluster implements FailoverCluster
CustomFailoverClusterInvoker implements FailoverClusterInvoker
注册中心:Eureka -> Zookeeper、Consul、Nacos
配置中心:Config + Bus -> Nacos
负载均衡:Ribbon -> LoadBalancer
LoadBalancer还在萌芽中~
SpringCloud Alibaba的负载均衡集成的是Ribbon
动态代理:Feign -> OpenFeign
Feign(OpenFeign)只不过是用动态代理技术简化了服务调用,内部的负载均衡还是Ribbon
服务网关:Zuul -> Gateway
服务治理:Hystrix -> Resilience4j -> Sentinel
Thrift
SQL数据库
MySQL
存储引擎
锁
聚集索引和非聚集索引
库表字段
库功能要单一(业务分库)
避免一个表中有太多的列(一般不要超过30个)
一定要指定列为NOT NULL
为字段选择合适的数据类型
高效索引
分类(常见)
对比
时间复杂度
是否有序
是否支持范围查询
是否支持部分索引列匹配查找(多列索引)
Hash索引
B+树索引
自适应Hash索引
索引数据结构(B+树索引)
联合索引的底层数据结构
主键索引的key为什么要设置成递增的
页分裂
磁盘碎片化
随机IO
有没有想过B+树索引递增构建的过程 -> 页分裂和标准B+树不同,优化过 -> 索引页分裂优化
主键索引,要存3000W数据,树的高度为多少?select要几次IO?
InnoDB一个节点大小默认是16KB
页,磁盘数据传输以页为单位,一般为4KB,一次IO读一页
为什么索引使用B+树
为什么不用B-树
B-树和B+树的区别
非叶子节点
树高度
IO
叶子节点
全量数据
有序,链表
回溯
为什么不用红黑树?
数据量 -> 树太高
超多IO
聚簇索引和非聚簇索引
数据存储方式
索引创建原则
根据条件统计所有SQL创建最优索引
选择索引选择性高的列作为索引列
优先创建联合索引(多列索引)
排序字段要出现在联合索引里,减少或避免Using filesort
当排序时记录数较多,内存中的排序 buffer满了,只能 Using filesort 进行外部排序
最左前缀原则:选择合适的索引列顺序
一般3个左右联合索引就够了,最好不要超过5个!
互联网的sql都很简单,这些索引基本能覆盖所有的sql
索引失效情况(B+树索引)
为什么会失效 -> 索引数据结构
违反最左原则
like'%xx'
索引列计算或使用函数
索引列类型转换
高到低无效(String列用int查),低到高有效
or,慎用
要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
优化:select * from t where t.a > ? and t.b = ? or t.c = ? order by t.d desc limit 10
not in,not exist
is null
MyISAM
读:共享表锁
写:排他表锁
InnoDB
共享锁(S)
排它锁(X)
意向锁
意在更细粒度(库、表、页、行)上进行加锁
InnoDB意向锁设计比较简单,其意向锁只设计表级别的意向锁
加行锁时,首先会加一个同类型的表级意向锁(S-IS;X-IX)
意向锁由InnoDB自动添加,不能人为显示加意向锁
意向锁不会阻塞除全表扫描以外的任何操作
例如,行锁升级为表锁的情况
行锁
行锁升级(行锁与索引)
Record Lock
Gap Lock
Next-Key Lock
Next-Key Lock退化为Record Lock的情况
隔离级别为RC
参数设置关闭Gap Lock
对唯一索引加锁
表锁
行锁升级
读写与锁
读(select)不加任何锁(MVCC一致性读)
可以手动加锁
select ... in share mode;
加共享锁
select ... for update;
加排他锁
写(insert | update | delete)会自动加排他锁
乐观锁&悲观锁
死锁
锁和事务
锁和事务没有强关系,别绕晕了,每个增删改查都是一个事务,但只有增删改会自动加锁,查是不加锁的
ACID
Atomic
事务里的一堆SQL,要么都成功,要么都失败
Consistency
库存减一个,订单就得多一条
Isolation
不同事务之间相互隔了,互不干扰
Durability
事务成功提交,数据就持久化了
隔离级别
读未提交
读已提交(MVCC)
可以解决脏读问题
可重复度(MVCC)
MVCC可以解决不可重复读问题
可以通过加锁读(锁定读)来解决幻读
select count(*) from tb where id>100 for update;或select count(*) from tb where id>100 in share mode;
由于Next-Key Lock,id>100范围都会被锁住
select count(*) from tb where id>100
串行化
事务和锁
事务和锁没有强关系,别绕晕了,每个增删改查都是一个事务,但只有增删改会自动加锁,查是不加锁的
MVCC
DB_TRX_ID、DB_ROLL_PTR
一致性读(Consistent Read) -> 读快照(undo日志中的版本)
一致性读视图ReadView
活跃事务数组(m_ids)
低水位(数组中的最小事务ID)
高水位(数组中的最大事务ID+1)
版本可见性判断逻辑
版本链(undo日志中的版本记录)
加入版本链时,数据版本为当前已经提交的最新的版本
RR:读取版本链中第一个小于等于当前事务ID的版本;RC:读取版本链中最新的版本
explain
走索引
除非万不得已,SQL都基于单表,别连表
SQL不包含计算,只做存取
计算逻辑交个业务层
各自干擅长的事情
避免大事务
分库分表
拆分信号
单表数据量太大
水平分库
可以分库解决就把库分了
分了库可以解决很多问题
水平分表
分了库单表数据依然很大
单纯的并发量上来了
数据服务器磁盘容量满了
要么好多表
垂直分库?-> 业务拆分
要么大表
典型的列表和详情场景
垂直分表
这样主表字段减少,跟多的行可以缓存在内存中
减少磁盘IO
拆分步骤
根据容量(当前容量和增长量)评估分库分表个数
选key(均匀)
分表规则(hash或range等)
range还是hash
range:扩容简单,场景少
时间:新老数据均匀访问
空间:群体数据均匀,比如地域什么的
hash:扩容复杂,场景多
执行(一般是双写)
双写步骤
配置双写,部署
后台数据迁移任务或者工具
将老数据迁移到新库新表
校验新老数据,以老数据为准
多次往复
配置新库新表,部署
子主题
扩容(一般是双写)
成倍扩容方案
升级从库法(见博客)
迁库扩容方案
预先分库
预先分32个库,放在4台数据库服务器上
迁移分库
数据库服务器并发上来了,新增数据库服务器,直接迁移即可
迁移数据库,DBA有现成的工具
原理就是主从复制
拆分工具
Client端jar包
Proxy中间件
分布式全局ID
snowflake算法
非partition key查询
映射法
基因法
NoSQL法
主从同步
复制策略
半同步复制
一般用这个,防止数据丢失(relay日志得同步刷盘)
全同步复制
并行复制(hash)
减少主从同步延时
并没有想象中的那么有效,28法则,可能热点数据都在某个或某些表
主从延迟
只能缓解无法避免(全同步复制不现实,从库多了太慢)
主从间传递binlog日志
写入relay日志
relay日志重现 -> 数据
集群架构
高可用架构
主备,主从,双主
主从一致性问题
缓存和库一致性问题
一些数据
并发支撑
主从延时
Oracle
JDBC
Druid
HikariCP
ShardingSphere
MyCat
TDDL
NoSQL数据库
测试技能
单元测试
Mock
SpringTest
压力测试
JMeter
0 条评论
回复 删除
下一页