Java基础
- String,StringBuffer和StringBuilder的关系,区别’
- 都是final类,都不允许被继承;
- String类长度是不可变的,StringBuffer和StringBuilder类长度是可以改变的;
- StringBuffer类是线程安全的,StringBuilder不是线程安全的;
String类型和StringBuffer类型的主要性能区别:String是不可变的对象,因此每次在对String类进行改变的时候都会生成一个新的string对象,然后将指针指向新的string对象,所以经常要改变字符串长度的话不要使用string,因为每次生成对象都会对系统性能产生影响,特别是当内存中引用的对象多了以后,JVM的GC就会开始工作,性能就会降低;
使用StringBuffer类时,每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用,所以多数情况下推荐使用StringBuffer,特别是字符串对象经常要改变的情况;
StringBuilder是5.0新增的,此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
基本原则:如果要操作少量的数据,用String ;单线程操作大量数据,用StringBuilder ;多线程操作大量数据,用StringBuffer。
Redis
穿透,击穿,雪崩
1.缓存穿透问题:
这个问题一般出现在有人恶意攻击服务器的时候发生,利用请求来查询一个不存在的数据,导致mysql查询不到数据,也不会写入缓存,导致大量请求都去查询数据库,当查询请求达到一定数量后服务器就会宕机
解决办法:
方案一:缓存空数据,当查询请求查询数据库返回为空的时候,将这个请求的结果设置为null存入缓存。当再次收到同样的请求时会从缓存中进行处理。
此方案优缺点:消耗内存:当大量请求都是null的时候会非常占用内存。同时还有数据不一致的问题,当某条已经缓存为null的数据在后面被赋予实际值的时候,缓存中存储的值还是null,就会导致数据不一致性问题。
方案二:布隆过滤器
布隆过滤器主要进行一个请求过滤的功能,在请求发送之前先对值进性一个判断,判断这个值存不存在判断通过再去查询数据库或者缓存,相反就拦截请求。
2.缓存击穿问题:
给某一个key设置了过期时间,当key过期的时候,恰好这个时候这个key有大量的并发请求发送过来,这些请求可能会瞬间把服务器压垮
解决方案:
方案一:互斥锁
当缓存失效时,不立即去load db,先使用如 Redis 的setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
原理:当第一个线程进行查询时未命中redis中数据就会给redis加一个互斥锁,当线程从db中获取到数据,在将数据写入缓存,
未释放锁期间有其他线程进行访问rediis会持续等待线程一释放锁在进行查询。
特点:强一致,性能差
方案二:逻辑过期
实现原理:
在设置key的时候,设置一个过期时间字段一款存入缓存中,不给当前key设置过期时间
当查询的时候,从redis中取出数据后判断时间是否过期
如果过期则开通另一个线程进行数据同步,则当前的线程继续正常返回原来数据
数据同步线程未结束期间任何线程访问的数据都是之前未更新的旧数据
特点:高可用,性能优。不能保证数据绝对一致
3.缓存雪崩
指在同一时间大量的缓存同时失效或者Redis服务宕机,导致大量的请求到达数据库,带来的巨大压力
解决方案
方案一:给不同的key的TTL添加随机值
方案二:利用Redis的集群提高服务的可用性(哨兵模式,集群模式)
方案三:给缓存业务添加降级限流策略(ngxin或者spring cloud gateway)
方案四:给业务添加多级缓存(Guava或Caffeine)
解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,
比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
打油诗
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底
Redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
此问题分为两个大方向,具体回答需要结合实际业务
一致性要求高:
双写一致性:当修改数据库数据的同时也需要更新缓存的数据,缓存和数据库要保持一致
- 读操作:缓存命中,直接返回数据,未命中缓存则查询数据库,将查询结果写入缓存设置超时时间,然后返回结果
- 写:延迟双删
引发问题:1.先删数据库还是先修改缓存?
答:两种方式在多线程情况下都会出现脏写问题
2.为什么要删除两次缓存?
答:避免脏数据
3.为什么要延迟删除?
答:一般情况下数据库都是主从模式,所以需要延迟时间将数据同步到从节点
延迟双删总结:极大控制脏数据风险,但也只是控制了一部分。因为延迟时间问题不好确定所以也有脏数据风险,做不到强一致性!
1.先删缓存:
正常情况:
线程一删除缓存——>更新数据库
线程二查询缓存未命中——>查询数据库——>将最新数据存入缓存
异常情况:
线程一删除缓存——>线程二查询缓存未命中查询数据库——>线程二将查询结果写入缓存,此时线程二运行结束——>此时线程一进行更新数据库
最终导致脏数据现象
2.先修改数据库
异常情况:
正常情况:
分布式锁:等当前线程读写成功后,其他线程才能进行操作
最终方案:们采用redisson实现的读写锁,在读的时候添加共享锁,
可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,
这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。
这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
相关问题:
- 那这个排他锁是如何保证读写、读读互斥的呢?
答:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
- 你听说过延时双删吗?为什么不用它呢?
答:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,
最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,
并不能保证强一致性,所以没有采用它。
允许延迟一致:
异步通知:确保数据的最终一致性
基于Mq实现:
基于canal实现:
相关问题:
redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
答:
嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)
我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。
canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,
然后在通过canal的客户端获取到数据,更新缓存即可。
Redis持久化问题
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB:
RDB全称 RedisDatabase Backup file(Redis数据备份文件),也叫做Redis数据快照。简单来说就是把所有数据都记录到磁盘中。dangRedis实例故障重启后,从磁盘读取快照文件,恢复数据
Redis命令:
Save //由Redis主进程来执行RDB,会阻塞所有命令
bgsave //开启子进程执行RED,避免主进程受到影响
Redis内部有触发RDB机制,可以在redis.conf文件下找到
save 900 1 //900秒内有一个key被修改就执行bgsave
save 300 10//300秒内有十个key被修改就执行bgsave
save 60 1000//60秒内有一万个key被修改就执行bgsave
RDB执行原理:
bgsave在开始时会fork主进程得到子进程(复制页表,页表:记录虚拟地址与物理地址的映射关系),子进程和主进程共享主进程的内存数据。完成fork后读取内存数据写入RDB文件
AOF:
全称Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看作命令日志文件。
AOF默认为关闭,需要手动配置redis.conf文件来开启AOF
是否开启AOF命令默认是0
appendonly yes
AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录频率也可以通过redis.conf来配置
表示每执行一次,立即记录到AOF文件
appendfsync always 写命令执行完放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 | 刷盘时机 | 优点 | 缺点 |
Alway | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响较大 |
everyec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
AOF因为是记录命令,导致AOF回避RDB大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。
解决办法:通过bgrewriteaof命令,可以让AOF文件执行重写功能,用最少命令达到相同效果。
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
AOF文件比上次 增长超过多少百分比触发重写 auto-aof-rewite-percentage 100 AOF文件体积最小达到多大以上触发重写 auto-aof-rewrite-min-size 64mb
RDB与AOF整体对比
RDB | AOF | |
持久化方式 | 定时对整个内存做快照 | 记录每一次执行命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会压缩,文件体积小 | 记录命令,文件体积很大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量cpu和内存消耗 | 低,主要是磁盘IO资源但是AOF 重写时会占用大量xpu和内存资源 |
使用场景 | 可以容忍分钟的数据丢失,追求更快启动速度 | 对数据安全性要求较高常见 |
常见问题
1.面试官问 你们项目中怎莫用的持久化?
答:数据要求比较高的时候会结合两者使用。
2.redis做为缓存,数据的持久化是怎么做的?
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
3.这两种持久化方式有什么区别呢?
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
4.这两种方式,哪种恢复的比较快呢?
RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,
我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,
我们当时设置的就是每秒批量写入一次命令
5.允许主线程同时对数据进行修改,避免影响其他业务。的请求吗?
答:在RDB生成期间,Redis可以同时处理写可以,Redis使用操作系统的多进程写时复制技术来实现快照持久化,保证数据一致性。
Redis在持久化时,调用glibc的函数fork产生一个子进程。
快照持久化都交给子进程来处理,父进程继续处理客户端请求。 当主线程执行写的命令修改数据时,这个数据就会复制一份副本,bgsave子进程读取这个副本数据写到RDB文件。
保证了快照的完整性
Redis的数据淘汰策略
当Redis中的内存不够用时,此时向Redis中添加新的Key,那么Redis会按照某一种规则将内存中的数据删除掉,这些数据的删除规则称之为内存的淘汰策略。
- noeviction:不淘汰任何key,但是内存满时不允许写入数据,默认就是这种策略
- volatile-ttl:对设置了TTL的key,比较的剩余TTL值,TTl值越小越先被淘汰
- allkeys-random:对全体Key,随机淘汰
- volatile-random:对设置了TTL的Key,随机淘汰
- allkeys-lru:对全体KEy,基于LRU算法进行淘汰
- volatile-lru:对设置了TTL的Key,基于LRU算法进行淘汰
- allkeys-lfu:对全体Key,基于LFU算法进行淘汰
- volatile-lfu:对设置了TTL的KEy,基于LFU算法进行淘汰
LRU(Least Recently Used)最近最少使用。用当前时间减去最后一次访问时间,得到值越大越先被淘汰。
LFU(Least Frequently Used)最少频率使用会统计每个Key的使用频率,值越小越先被淘汰
淘汰策略-使用建议
优先使用allkeys-lru策略。充分利用LRU算法的优势,把最近常访问的数据留在缓存中。如果有明显的冷热区分,建议使用
问题:为什么不选LFU呢它也是按照频率淘汰
答:在实际运营中,访问频率并不一定准确反应数据的热度。
比如某个数据在某个时间段内被访问了很多次,但是在其他时间段内很少被访问。这个时候仅仅按照数据访问频率来判断数据的热度,就可能会出现误判。
如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用allkey-random,随机选择淘汰
如果业务中有指定的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
如果业务中有短期的高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。
常问面试题:
问:Redis的数据过期策略有哪些 ?
答:嗯~,在redis中提供了两种数据过期删除策略
第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key
定期清理的两种模式:
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
- FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。
问:Redis的数据淘汰策略有哪些 ?
答:嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错是可以在redis的配置文件中进行设置的,
里面有两个非常重要的概念,一个是LRU,另外一个是LFULRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中
问:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
答:可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据
问:Redis的内存用完了会发生什么?
答:嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。
分布式锁
Redis实现分布式锁主要利用Redis的Setnx命令。setnx是SET if not exits(如果不存在,则SET)的简写
获取锁:
添加锁,NX是互斥,EX是设置超时时间/
SET lock value NX EX 10
释放锁:
释放锁,删除即可/
DEl key
分布式锁执行流程
Redis实现分布式锁如何合理的控制锁的有效时长?
1.根据业务执行时间预估
业务执行时间不好评估,可用性太差
2.给锁续期
基于Redisson实现
Redisson实现的分布式锁-执行流程
当线程一获得锁的时候会去操作redis,同时加锁成功后还会添加一个看门狗机制,它将每隔(releaseTimr/3)的时间做一次锁的续期。当线程释放锁的时候会去通知看门口不再续期
release Time:锁的过期时间
当线程一获得锁后有其他线程来获取锁会失败,但是失败后不会终止线程而是进行while循环不断尝试获取锁,其中会设置一个阈值不会一直循环获取
public void redisLock() throws InterruptedException{
//获取(重入锁),执行名称
Rlock lock =redissonClient.getLock("heimalock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
//boolean islock=lock.tryLock(10,30,TimeUnit.SECONDS);
boolean isLock=lock.tryLock(10,TimeUnit.SECONDS);
//判断是否获取成功
if(isLock){
try{
System.out.printIn("执行业务");
} finally{
//释放锁
lock.unlock();
}
}
}
//加锁,设置过期时间等操作都属于lua脚本完成
redisson实现分布式锁-可重入
public void add1(){
RLock lock = redissonClient.getLock(“heimalock");
boolean isLock = lock.tryLock();
//执行业务
add2();
//释放锁
lock.unlock();
}
public void add2()
RLock lock = redissonClient.getLock(“heimalock");
boolean isLock = lock.tryLock();
//执行业务
//释旅锁
lock.unlock();
- 当add1执行时会初始化Key,
- Value里的 field和value field存储线程id value存储重入次数
- Key记录锁名称
- redisson实现的分布式锁-主从一致性
redisson实现的分布式锁能主从一致性的问题
1.正常情况主节点写入将数据同步到子节点
2.主节点没来得及同步数据挂掉了
3.从从节点选取一个成为主节点,新的线程会直接访问新的主节点会尝试获取锁
4.因为之前数据还没同步就挂了,所以新的请求也能加锁成功,这个时候就形成了两个线程同时持有一把锁
这个时候就丧失了锁的基本特性,互斥性。如果是业务还在执行就会出现脏数据!
RedLock(红锁):
不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1)避免在一个redis实例上加锁
AP和CP思想
redis主要思想AP思想
优先保证:高可用性,我们可以保证最终一致
CP思想的zookeeper
保证数据的强一致性
总结:
问题:
1.Redis分布式锁如何实现 ?
答:嗯,在redis中提供了一个命令setnx(SET if not exists)由于redis的单线程的,
用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的
2.如何控制Redis实现分布式锁有效时长呢?
答:redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,
在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,
如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了还有一个好处就是,
在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,
它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
3.redisson实现的分布式锁是可重入的吗?
答:是可以重入的。这样做是为了避免死锁的产生。
这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。
在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
4.:redisson实现的分布式锁能解决主从一致性的问题吗?
答:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,
此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,
会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,
应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。
这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,
所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
5.如果业务非要保证数据的强一致性,这个该怎么解决呢?
答:redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,
建议使用zookeeper实现的分布式锁,它是可以保证强一致性的
Redis集群
全量同步步骤:
总结三大步骤:
1.从节点发起请求给主节点master,主节点判断是不是第一次请求,如果不是就进行全量同步
2.主节点master会执行bgsave去生成一个RDB文件发送给从节点去执行
3.主节点在记录RDB期间接收的其他命令会保存在repl_baklog日志文件中,再把日志发送给从节点执行
增量同步步骤
1.从节点发送同步请求,主节点master判断是不是第一次请求,并且获取到从节点slave发送的offset值
2.主节点master从日志文件中获取offset之后的数据同步到了从节点
面试问题:
1.Redis集群有哪些方案, 知道嘛 ?
答:在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群
2.那你来介绍一下主从同步?
答:单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。
一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中
3.能说一下,主从同步数据的流程?
答:主从同步分为了两个阶段,一个是全量同步,一个是增量同步
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,
那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,
这样就保持了一致当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,
最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,
这个就是全量同步增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,
主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
Redis哨兵
哨兵作用:
1.监控:sentinel会不断检查您的master和slave是否按预期工作
2.自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后将一个slave提升为主
3.通知:Sentinel充当Redis客户端的服务器发现来源,当集群发生故障转移时,会将最新的信息推送给Redis的客户端
服务状态监控:
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线: 若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高
redis集群脑裂
异常场景:
由于网络原因主节点master与子节处于不同的网络分区,哨兵检测不到主节点,会从子节点提升一个变为主节点。但是客户端依然可以链接之前的主节点并往里面写入数据
当网络恢复后旧的主节点会成为新的主节点的子节点,旧的主机点会清空自身内容同步新的主节点内容,
这个时候之前脑裂过程中客户端写入旧的主节点的数据就会丢失
这个就是由于脑裂产生的数据丢失问题了。
解决办法
redis中有两个配置参数:
min-replicas-to-write 1 表示主节点最少的salve节点为1个;
min-replicas-max-lag 5表示数据复制和同步的延迟不能超过5秒
面试题:
1.你们使用redis是单点还是集群,哪种集群?
答:我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。
尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
2.redis集群脑裂,该怎么解决呢?
答:嗯! 这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是redis的哨兵模式集群的
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。
关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
Redis分片集群
概念:
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征
- 集群中有多个master,每个master保存不同数据
- 客户端请求可以访问集群任意节点,最终会被转发到正确节点
- master之间通过ping彼此检查健康状态
- 每个master都可以有多个slave节点
分片集群结构-数据读写
Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384个哈希槽,每key通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。
面试问题:
redis的分片集群有什么作用
答:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,
就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点
2.Redis分片集群中数据是怎么存储和读取的?
答:在redis集群中是这样的Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,
key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑是一样的
Redis快的原因
1、完全基于内存的,C语言编写
2、采用单线程,避免不必要的上下文切换可竞争条件
3、使用多路I/O复用模型,非阻塞IO
例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
能解释一下I/O多路复用模型?
答:I/O多路复用是指利用单个线程来同时监控多个Socket,并在某个Socket可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,他会在通知用户Socket就绪的同时,把已经就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器,命令回复处理器,命令请求处理器:
在REdis6.0后,为了提升更好的性能,在命令回复器使用了多线程来处理回复事件,在命令请求处理器中,将命令转换使用了多线程,增加命令转换速度,在命令执行的时候依然是单线程。
Mysql
索引
索引(index)是帮助MySQL高效获取数据结(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
什么是索引?
- 索引(index)是帮助MySQL高效获取数据的数据结构(有序)
- 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
- 通过索引列对数据进行排序,降低数据排序成本,降低了cpu的消耗
Mysql中,如何定位慢查询?
答:我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题
如果项目中没有运维的监控系统,其实在Mysql中也提供了慢日志查询的功能,可以在Mysql的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置是两秒多点,只要SQL执行时间超过2秒就会记录到日志文件中,我们就可以在日志中找到执行比较慢的SQL了。
那这个SQL语句执行很慢, 如何分析呢?
如果一条SQL执行很慢的话,我们通常会使用mysql自动执行计划explain来去查看这条sql的执行情况,比如我们在这里通过Key和Key_len检查是否命中了索引,如果本身已经添加了索引,也可以检查索引是否失效的情况,
第二个,我们可以通过type字段来查看是否有进一步的提升空间,是否存在全盘扫描或全索引扫描,
第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或者修改返回字段来修复
索引的底层数据结构了解过吗?
MySQL的InnoDB引擎采用的B+树的数据结构来存储索引的
阶数更多,路径更短
B树与B+树对比
磁盘读写代价B+树更低,非叶子节点只存储指针,叶子节点存储数据
B+树更便于扫库和区间查询,叶子节点是一个双向链表
1:磁盘读写代价B+树更低
B+树,非叶子节点不存储任何数据只存储指针,相当于导航所以他的效率很高,
B树非叶子节点存储了子节点的数据的数据,所以相比B+树
B+树便于扫库和区间查询
B+树叶子节点之间使用双向指针进行连接的在进行范围查找的时候会更加方便,
什么是聚簇索引什么是非聚簇索引
聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了正行数据,有且只有一个,一般情况下主键在作为聚簇索引的id
非聚簇索引是数据与索引分开存储,B+书的叶子节点保存对应的主键,可以有多个。我们单独创造的索引一般都是非聚簇索引
什么是回表查询
通过二级索引找到对应的主键值,到聚集索引中查找整行的数据,这个过程就是回表。
什么是覆盖索引
覆盖索引是指Select查询语句使用了索引,返回的列必须在索引中全部能找到,例如我们使用id查找,他会直接走聚集索引查询一次扫描,直接返回数据。
如果按二级索引查找数据的时候,返回列中没有创建索引,可能会触发回表查询,尽量避免使用Select*,尽量再返回列中都包含添加索引的字段
总的来说覆盖索引就是一次查询就能够返回所需的全部数据
而需要回表的都不是覆盖索引。
MySQL超大分页怎末处理
超大分页一般出现在数据比较大的时候ih,我们使用Limit分页查询,并且需要对数据进行排序,这个时候效率很低,我们可以采用覆盖索引和子查询来解决
先用分页查询数据的ID字段,确定了ID之后,再用子查询来过滤,只查询这个ID列表中数据就可以了。
因为查询id的时候,走的是覆盖索引,所以效率会很高
索引创建原则有哪些?
这个有很多,比如我们的表超过了10万以上,我们才会创建索引,并且添加索引字段是查询比较频繁的字段,一般也是作为查询条件,排序字段或分组的字段这些。
还有就是在创建索引的时候都是会使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话我们也会把它放在组合索引后面的字段
如果一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要加索引,这个索引数量也要控制,索引增加会导致sql的增改速度变慢
什么情况下索引会失效
违反最左前缀法则
范围查询右边的列,不能使用索引
不要再索引列上进行运算操作,索引将失效
字符串不加单引号,造成索引失效(类型转换)
以%开头的Like模糊查询
SQL优化经验
避免使用select *
避免索引失效
尽量使用union all 代替union会多一次过滤,效率会变低
避免在where子句中进行表达式操作
join优化能用innerjoin就不使用left join right join ,如必须使用一定要以小表为驱动,内连接会对两个表进行优化,优先把小的表放到外面,大的表放到里面 左连接和右链接不会
优化需要考虑:建表的时候,使用索引,sql语句的编写,主从复制,读写分离,数据量比较大的话就考虑分库分表。
事物的特性:
事物是一组操作的集合,把多个操作作为一个整体提交或回滚,同时成功,同时失败
ACID:
原子性:事物是不可分割的最小单元,要么全部成功要么全部失败
一致性:事物完成时,必须保证所有数据保持一致状态
隔离性:数据库系统提供的隔离机制,保证事物在不受外部并发操作影响的独立环境下运行
持久性:一旦提交或回滚,他对数据库的改变就是永久的
事物并发问题:脏读幻读,不可重复读
脏读:一个事务读取到另一个事务还没有提交的数据
不可重读读:一个事务读取同一个记录,但两次读取的数据不一致
幻读:一个事物按照条件查询数据时,没有对应数据,但是再插入时,又发现数据已经存在
解决方案:
未提交读,读已提交,可重复读,串行化
undo log 和redo log的区别
redo log:记录数据也的物理变化,服务宕机可以用来同步数据
undo log :记录的逻辑日志,当时事物回滚时,通过逆操作回复原来数据
redo log:保证了事物持久性,undo log保证了事务的原子性和一致性事物隔离性是由锁和mvcc
mvcc
事物的隔离性是如何保证的呢?
事物的隔离性是由mvcc实现的,其中mvcc的意思是多版本并发控制。维护一个数据的多个版本,是的读写操作没有冲突,他的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView视图
隐藏字段是指:在mySQL中给 每个表都设置了隐藏字段,有一个是trx_id(事物id),记录每一次操作的事物ID,是自增的:另一个字段是roll_pointer(回滚指针),指上一个版本的事物版本记录地址
undo log主要是记录回滚日志,存储老版本数据,在内部会形成一个版本连,在多个事务并行操作其某一行记录,记录不同事物修改数据的版本,通过roll_pointer指针形成一个链表
readView解决的是一个事物查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事物id判断该访问哪个版本的数据,不同隔离级别快照读是不一样的,最终访问的结果不一样,如果是rc隔离级别,每一次执行快照读时生成ReadView,如果时rr隔离级别仅在事务中第一次执行快照时生成ReadView,后续复用。
Mysql主从同步原理
MySQL主从复制的核心就是二进制日志(DDL(数据定义语言)语句和DML(数据操作语言)语句),他的步骤是这样的:
第一:主库在事务提交时,会把数据变更记录在二进制日志文件BInLog中
第二:从库读取主库的二进制文件BinLog,写入到从库的中继日志RelayLog
第三:从库重做中继日志中的时间,将改变反应他自己的数据
MySQL分库分表
微服务开发,每个微服务对应一个数据库,是根据业务进行拆分,这个其实就是垂直拆分。
水平分库?
一开始是单库,后来随着业务逐渐发展,业务上来的很迅速,其中表已经存放1000万的数据,我们做了很多优化也不好使,性能依然很慢,所以当时就是用了水平分库
我们一开始先做了3台服务器对应了3个数据库,由于库多了,需要分片,我们当时采用的mycat来作为数据库的中间件。数据都是按照id(自增)取模的方式来存取的。当然一开始的时候,那些旧数据,我们做了一些清洗的工作,我们也是按照id取模规则分别存储到了各个数据库中,好处就是可以让各个数据库分摊存储和读取的压力,解决了我们当时性能的问题
框架
spring框架中的单例ben是线程安全的吗?
不是线程安全的,打不过多用户同时请求一个服务时,容器会给每一个请求配发一个线程,这时多个线程会并发执行请求对应的业务逻辑(成员方法)如果该处理逻辑中有对线程单列状态的修改(体现为改单列的成员属性),必须考虑线程同步问题。
Spring框架并没有对单列ben进行任何多线程的封装处理。关于单列的线程安全和并发问题需要开发者自行去搞,
比如:我们通常在项目中使用的Spring ben都是不可可变的状态(比如Service类和DAO类)所以在某种程度上说SPring的单列ben是线程安全的
如果你的ben有多种状态的话,最浅显的解决办法就是将多态ben的作用由Singleton变更为
prototype
什么是AOP
AOp是面向切面编程,在Spring中作用于将那些与业务无关的,但却多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合度,一般可以作为公共日志保存,事务处理等。
Spring管理的事务,底层就是用的Aop
有没有使用过AOP
我们当时在后台管理系统中,就是使用了ioc和aop来记录了系统的操作日志
主要思路是这样的,使用AOP中的环绕通知+切点表达式,这个表达式主要就是找到记录日志的方法,然后通过环绕通知+节点表达式,这个表达式就是要找到记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息,注解,请求方式等,获取到这些参数后,保存到数据库。
常见AOP使用场景
记录操作日志
缓存处理
Spring中内置事物处理
Spring 实现的事物本质就是AOP完成的,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法后根据执行情况提交或者回滚事物。
Spring中事物失效场景
在这个项目中之前遇到过,我想想
- 第一个,如果在方法上异常捕获处理的话,自己手动处理了异常,没有抛出,就导致了事物失效,所以一般处理了异常之后,别忘了抛出去就行了,
- 还有一个如果方法抛出检查异常,如果报错也会导致事物失效,最后在Spring事物的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事物
- 还有一个没有public修饰方法,也会导致事物失效。
Spring的ben生命周期
这个好像步骤挺多的,我之前简单看过一些源码。我就大致说一下吧
首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息,这里面就封装了bean的所有信息,比如,类的全路径,是否延迟加载,是否是单例等等这些信息z
在创建bean的时候,第一步是调用构造函数实例化bean
第二步是吧bean的依赖注入,像平时开发用的@Autowire都是这一步完成的
第三步是处理Aware接口,如果某一个bean实现了Awarei接口就会重写方法执行
第四步是bean的后置处理BeanPostProcessor,这个是前置处理器
第五步是初始化方法,比如实现了接口InitializingBean或者自定义方法init-method标签或@PostContruct
第六步是执行了bean的后置处理器BeanPostProceessor,主要是对Bean进行增强,可能在这里产生代理对象。
最后一步是销毁bean
Spring中的循环引用
循环依赖:循环依赖就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B依赖,B依赖于A。
循环依赖在Spring中是允许存在的,Spring框架依据三级缓存已经解决了大部分的循环依赖
一级缓存:单例池,缓存已经经历了完整的声明周期,已经初始化完成的bean对象
二级缓存:缓存早期的bean对象(生命周期还没走完)
三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
具体流程:
第一步实例A对象,同时创建ObjecFactory对象存入三级缓存singletonFactories
第二,A在初始化的时候需要b对象,这个走b的创建的逻辑
第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories
第四,B需要注入A,通过三级缓存中获取ObiectFactory来生成一个A的对象同时存入二级缓存,这个是有两中情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键
第五,B通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects
第六,回到A对象初始化,因为B对象已经创建完成,则可以注入B,A创建成功存入一次缓存singletonObjects
第七,二级缓存中的临时对象A清除
构造方法出现了循环依赖怎么解决?
由于bean的生命周期中构造函数是第一个执行的,Spring矿建并不能解决构造函数的依赖注入,可以使用@Lazy懒加载,什么时候需要对象在进行Bean对象的创建。
SpringMVC
Spring执行流程?
我知道两种执行流程分别是 视图版本 jsp和接口开发
前五步都一样
- 用户发送请求到前端控制器DispatcherServlet,这是一个调度中心。
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。
- HandlerMapping找到具体处理器(可查找XML配置或者注解配置),生成处理器对象及处理器拦截器,在一起返回DispatcherServlet
- DispatcherServlet调用HandlerAdapter(处理器适配器)
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
jsp:
- Controller执行完成返回ModelAndView对象
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
- ViewReslover解析后返回具体View(视图)。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- DispatcherServlet响应用户。
接口开发:
方法上添加@ResponseBody
通过HttpMessageConverter来返回结果转换为JSON并响应
当然现在的开发,基本都是前后端分离的开发的,并没有视图这些,一般都是handler中使用Response直接结果返回
Springboot自动配置原理
在SpringBoot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
其中 @EnableAutoConfiguration是实现自动化配置的核心注解。
该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的Classpath路径下META-INF/Spring.factories文件中的所配置的类的全类名。
在这些配置类中所定义的Ben会根据条件注解所指定的条件决定是否需要将其导入到Spring容器中。
一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的Class文件,如果有则加载该类,把这个配置类的所有的Bean放入Spring容器中使用。
Spring的常见注解有哪些?
-
- 第一类:声明 Bean,有@Component,@Servive,@Repository,@Controller
- 第二类:依赖注入相关的,有@Autowired,@Qualifier,@Resourse
- 第三类:设置作用域@Scope
- 第四类:Spring配置相关的,比如@Configuration,ComponentScan和@bean
- 第五类:跟aop相关做增强的注解@Aspect,@Before,@After,@Around,@Pointcut
SpringMVC常见的注解有哪些?
-
- @RequestMapping:用于映射请求路径
- @RequestBody:注解实现接收http请求的JSON数据
- @RequesParam:指定请求参数的名称
- @PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;@ResponsonseBody:注解实现Controller方法返回对象转化为JSON对象响应给客户端。@RequesHeader:获取指定请求数据,还有像PostMapping,@GetMapping这些。
Springboot常见注解
-
- 核心注解是@SPringBootApplication,他由几个注解组成:
- @SpringBootConfiguration:组合了-@Configuration注解,实现配置文件的功能;
- @EnableAutoConfiguration:自动配置的功能,也可以关闭某个自动配置选项
- @ConmponentScan:Spring组件扫描
Mybatis执行流程
- 读取MyBatis配置文件:mybatis-config。xml加载运行环境和映射文件
- 构造会话工厂SqlSessionFactory,一个项目只需要一个,单列的,一般由Spring进行管理
- 会话工厂创建SqlSession对象,这里面就包含了执行SQL语句的所有方法
- 操作数据库接口,Executor执行器,同时负责查询缓存的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
Mybatis是否支持延迟加载
延迟加载的意思是:就是在需要用到数据时才进行加载,不需要数据时就不加载数据
Mybatis支持一对一关联对象和一对多关联对象的延迟加载
在Mybatis配置文件中,可以配置是否使用延迟加载
LazyLodingEnabled=true|false
延迟加载在底层主要使用的CGLIB动态代理完成的
第一是,使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper
第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询
第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
Mybatis的一级、二级缓存用过吗?
mybatis的一级缓存是基于PerpetualCache的HashMap本地缓存,存储作用域为Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
二级缓存需要单独开启
二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用PerpetualCache,HashMap存储。
想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行
Mybatis的二级缓存什么时候会清理缓存中的数据?
当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
JVM
java的运行环境
- 一次编写,到处运行
- 自动内存管理,垃圾回收机制
JVM组成
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library (本地接口)
运行流程:
- 类加载器将java代码转换为字节码
- 运行时数据区把java代码转换为字节码
- 执行引擎将字节码翻译为底层系统指令,再交由cpu去执行,此时需要调用其它语言的本地接口来实现整个程序的功能
好的,你能详细说一下 JVM 运行时数据区吗?
- 运行时数据区包含了堆,栈,本地方法,程序计数器这几部分每个功能作用不一样
- 堆解决的是是对象实例存储的问题,垃圾回收管理器的主要区域
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量,静态变量,即时编译器编译后的代码。
- 栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表。操作数栈,动态链接,方法出口等信息。
- 本地方法栈与栈功能相同,本地方法栈 执行的是本地方法,一个java调用非java代码的接口
- 程序计数器(pc寄存器)程序计数器中存放的是当前线程执行的字节码的行数。jvm工作时就是通过改变这个计数器的值来选取下一个需要执行的2字节码指令
什么是程序计数器?
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址
java虚拟机,对于多线程是通过线程轮流切换并且的分配线程执行的执行时间。在任何一个时间带你上,一个处理器只会处理执行一个线程,如果当前执行的这个线程她所分配的执行时间用完了【挂起】。处理器会切换到另一个线程上来进行执行。并且这个线程的执行时间也用完了,接着处理器就会又来执行被挂起的这个线程。这时候县城计数器就起到了关键作用,程序计数器在来回切换的线程中记录他上一次执行的行号,然后接着向下执行。
你能详细介绍java堆吗?
java中的堆是线程共享的区域。主要用来保存对象实例,数组等,当堆中没有没有内存空间可分配给实例,也无法扩展时,则抛出OutOfMemoryError异常
在java8中堆会存在年轻代,老年代
-
- Young区配划分为三个部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另一个留作垃圾收集时复制对象用。在Eden区变满的时候,GC就会将存活的对象移动到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,仍然存活于Survivor的对象将被移动到Tenured区间
- Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定次数后,对象就会被转移到Tenured区
能不能解释一下方法区
与虚拟机栈类似本地方法栈就是为虚拟机执行本地方法时提供服务的。不需要进行GC。本地方法一般由其他语言编写
你听说过直接内存吗?
他又叫作堆外内存,线程共享的区域,在java8之前就有个永久代的概念,实际上指的是HotSpot虚拟机上的永久代,他用永久代实现了JVM规定义的方法区功能主要存储类的信息,常量,静态变量,即时编译器编译后的代码等,这部分由于是在堆中实现的,受GC的管理,不过由于永久代有-XX:MaxPermSize的上限,所以如果大量动态生成类(将类信息放入永久代),很容易造成OOM,有人说可以把永久代设置的足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。
所以在Java8中就把方法去的实现移动到了本地内存中的元空间中,这样方法区就不受JVM的控制了,也就不会进行GC,也因此提升了性能
什么时虚拟机栈
虚拟机栈是表述的是方法执行时的方法模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈帧。保存方法执行时的局部变量,动态连接信息,方法返回地址信息等等。方法开始执行的时候会进栈,方法执行完会出栈【相当于清空了数据】,所以这块区域不需要进行GC
栈堆的区别是什么?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的。
- 两者的异常错误不同,但如果栈内存或者对内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
详细介绍JAVA堆?
线程共享的区域:主要用来保存实例,数组等,内存不够则抛出OutOfMemoryError异常
组成:年轻代+老年代
年轻代被划分为三个部分。Eden区和两个大小严格相同的Survivor区
老年代主要要存生命周期长的对象,一般是一些老的对象
神魔是虚拟机栈?
每个线程运行时所需要的内存,成为虚拟机栈,先进后出
每个帧由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存?
垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配越大越好嘛?
未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少,例如,机器总内存512m,目前能活动的线程数为512个,如果把栈内存改2048k,那么能活动的栈帧就会减半
方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,他是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致占内存溢出?
栈帧过多导致占内存溢出,典型问题:递归调用
栈过大导致内存溢出
类加载器
什么时类加载器,类加载器有哪些?
JVM只会处理二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
常见的类加载器有四个
启动类加载器(BootStrap ClassLoader):其实就是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
扩展类加载器(ExtClassLoader)该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的库类
应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类
自定义的加载类,也就是加载开发者自己编写的Java类。
类加载器执行过程?
加载:查找和导入Cllass文件
验证:保证加载类的准确性
准备:为类变量分配内存并设置类变量初始化操作
解析:把类中的符号引用转换为直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
使用:/jvm开始从入口方法开始执行用户程序代码
卸载:当用户代码只能够完毕后,JVM编开始小会创建的class对象,最后负责运行的JVM也退出内存
双亲委派?
如果一个类收到了类加载请求,他首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都因该传输顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(他的搜索返回中没有找到所需的类)时,子类加载器才会去尝试自己去加载
双亲委派机制作用?
通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载确保唯一性
为了安全,保证库Api不会被修改
垃圾回收
简述Java垃圾回收机制(神魔是GC?为什么要GC)
JVM实践