1、什么是类加载器,类加载器有哪些?
1、什么是类加载器?
类加载器负责加载所有的类,其为所有被载入内存的类生成一个 java.lang.Class 实例对象。
2、类加载器有哪些?
JVM 有三种类加载器:
(1)启动类加载器
该类没有父加载器,用来加载 Java 的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自 java.lang.classLoader。
(2)扩展类加载器
它的父类为启动类加载器,扩展类加载器是纯 java 类,是 ClassLoader 类的子类,负责加载 JRE 的扩展目录。
(3)应用程序类加载器
它的父类为扩展类加载器,它从环境变量 classpath 或者系统属性 java.lang.path 所指定的目录中加载类,它是自定义的类加载器的父加载器。
2、说一下类加载的执行过程?
当程序主动使用某个类时,如果该类还未被加载到内存中,JVM 会通过加载、连接、初始化 3 个步骤对该类进行类加载。
1、加载
加载指的是将类的 class 文件读入到内存中,并为之创建一个 java.lang.Class 对象。
类的加载由类加载器完成,类加载器由 JVM 提供,开发者也可以通过继承 ClassLoader 基类来创建自己的类加载器。
通过使用不同的类加载器可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载
- 从 jar 包加载
- 通过网络加载
- 把一个 Java 源文件动态编译,并执行加载
2、连接
当类被加载之后,系统为之生成一个对应的 Class 对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到 JRE 中。
类连接又可分为三个阶段:
(1)验证
文件格式验证
元数据验证
字节码验证
符号引用验证
(2)准备
为类的静态变量分配内存,并设置默认初始值。
(3)解析
将类的二进制数据中的符号引用替换成直接引用。
3、初始化
为类的静态变量赋予初始值。
3、JVM 的类加载机制是什么?
JVM 类加载机制主要有三种:
1、全盘负责
类加载器加载某个 class 时,该 class 所依赖的和引用其它的 class 也由该类加载器载入。
2、双亲委派
先让父加载器加载该 class,父加载器无法加载时才考虑自己加载。
3、缓存机制
缓存机制保证所有加载过的 class 都会被缓存,当程序中需要某个 class 时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成 class 对象,存入缓存区中。
这就是为什么修改了 class 后,必须重启 JVM,程序所做的修改才会生效的原因。
4、什么是双亲委派模型?
如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行,如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器,如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。
双亲委派模式的优势:
- 避免重复加载;
- 考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委派模式传递到启动加载器,而启动加载器在核心 Java API 中发现同名的类,发现该类已经被加载,就不会重新加载网络传递的 Integer 类,而直接返回已加载过的 Integer.class,这样可以防止核心 API 库被随意篡改。
5、怎么判断对象是否可以被回收?
1、引用计数算法
(1)判断对象的引用数量
通过判断对象的引用数量来决定对象是否可以被回收;
每个对象实例都有一个引用计数器,被引用 + 1,完成引用 - 1;
任何引用计数为 0 的对象实例可以被当做垃圾回收;
(2)优缺点
优点:执行效率高,程序受影响较小;
缺点:无法检测出循环引用的情况,导致内存泄漏;
2、可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
如果程序无法再引用该对象,那么这个对象肯定可以被回收,这个状态称为不可达。
那么不可达状态如何判断呢?
答案是 GC roots,也就是根对象,如果一个对象无法到达根对象的路径,或者说从根对象无法引用到该对象,该对象就是不可达的。
以下三种对象在 JVM 中被称为 GC roots,来判断一个对象是否可以被回收。
(1)虚拟机栈的栈帧
每个方法在执行的时候,JVM 都会创建一个相应的栈帧(操作数栈、局部变量表、运行时常量池的引用),当方法执行完,该栈帧就从栈中弹出,这样一来,方法中临时创建的独享就不存在了,或者说没有任何 GC roots 指向这些临时对象,这些对象在下一次 GC 的时候便会被回收。
(2)方法区中的静态属性
静态属性数据类属性,不属于任何实例,因此该属性自然会作为 GC roots。这要这个 class 在,该引用指向的对象就一直存在,class 也由被回收的时候。
class 何时会被回收?
- 堆中不存在该类的任何实例
- 加载该类的 classLoader 已经被回收
- 该类的 java.lang.class 对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息
(3)本地方法栈引用的对象
6、说一下 jvm 有哪些垃圾回收算法?
1、对象是否已死算法
- 引用计数器算法
- 可达性分析算法
2、GC 算法
(1)标记清除算法
如果对象被标记后进行清除,会带来一个新的问题 – 内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。
(2)复制算法(Java 堆中新生代的垃圾回收算法)
- 先标记待回收内存和不用回收内存;
- 将不用回收的内存复制到新的内存区域;
- 就的内存区域就可以被全部回收了,而新的内存区域也是连续的;
缺点是损失部分系统内存,因为腾出部分内存进行复制。
(3)标记压缩算法(Java 堆中老年代的垃圾回收算法)
对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。
标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。
老年代的垃圾回收算法称为 “Major GC”。
7、说一下 jvm 有哪些垃圾回收器?
说一下 jvm 有哪些垃圾回收器?
8、JVM 栈堆概念,何时销毁对象
- 类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java 自动垃圾回收,也可以用 System.gc() 开启回收器,但是回收器不一定会马上回收。
- 静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
- 实例变量在类实例化对象时创建,在对象销毁的时候销毁;
- 局部变量在局部范围内使用时创建,跳出局部范围时销毁;
9、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
新生代回收器一般采用的是复制算法,复制算法效率较高,但是浪费内存;
老生代回收器一般采用标记清楚算法,比如最常用的 CMS;
10、详细介绍一下 CMS 垃圾回收器?
CMS 垃圾回收器是 Concurrent Mark Sweep,是一种同步的标记 - 清除,CMS 分为四个阶段:
初始标记,标记一下 GC Root 能直接关联到的对象,会触发 “Stop The World”;
并发标记,通过 GC Roots Tracing 判断对象是否在使用中;
重新标记,标记期间产生对象的再次判断,执行时间较短,会触发 “Stop The World”;
并发清除,清除对象,可以和用户线程并发进行;
11、简述分代垃圾回收器是怎么工作的?
分代回收器分为新生代和老年代,新生代大概占 1/3,老年代大概占 2/3;
新生代包括 Eden、From Survivor、To Survivor;
Eden 区和两个 survivor 区的 的空间比例 为 8:1:1 ;
垃圾回收器的执行流程:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden + From Survivor 分区,From Survivor 和 To Survivor 分区交换;
- 每次交换后存活的对象年龄 + 1,到达 15,升级为老年代,大对象会直接进入老年代;
- 老年代中当空间到达一定占比,会触发全局回收,老年代一般采取标记 - 清除算法;
12、Redis 是什么?
Redis 是一个 key-value 存储系统,它支持存储的 value 类型相对更多,包括 string、list、set、zset(sorted set – 有序集合)和 hash。这些数据结构都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave(主从)同步。
13、Redis 都有哪些使用场景?
- Redis 是基于内存的 nosql 数据库,可以通过新建线程的形式进行持久化,不影响 Redis 单线程的读写操作
- 通过 list 取最新的 N 条数据
- 模拟类似于 token 这种需要设置过期时间的场景
- 发布订阅消息系统
- 定时器、计数器
14、Redis 有哪些功能?
1、基于本机内存的缓存
当调用 api 访问数据库时,假如此过程需要 2 秒,如果每次请求都要访问数据库,那将对服务器造成巨大的压力,如果将此 sql 的查询结果存到 Redis 中,再次请求时,直接从 Redis 中取得,而不是访问数据库,效率将得到巨大的提升,Redis 可以定时去更新数据(比如 1 分钟)。
2、如果电脑重启,写入内存的数据是不是就失效了呢,这时 Redis 还提供了持久化的功能。
3、哨兵(Sentinel)和复制
Sentinel 可以管理多个 Redis 服务器,它提供了监控、提醒以及自动的故障转移功能;
复制则是让 Redis 服务器可以配备备份的服务器;
Redis 也是通过这两个功能保证 Redis 的高可用;
4、集群(Cluster)
单台服务器资源总是有上限的,CPU 和 IO 资源可以通过主从复制,进行读写分离,把一部分 CPU 和 IO 的压力转移到从服务器上,但是内存资源怎么办,主从模式只是数据的备份,并不能扩充内存;
现在我们可以横向扩展,让每台服务器只负责一部分任务,然后将这些服务器构成一个整体,对外界来说,这一组服务器就像是集群一样。
15、Redis 支持的数据类型有哪些?
- 字符串
- hash
- list
- set
- zset
16、Redis 取值存值问题
1、先把 Redis 的连接池拿出来
JedisPool pool = new JedisPool(new JedisPoolConfig(),"127.0.0.1");Jedis jedis = pool.getResource();
2、存取值
jedis.set("key","value");
jedis.get("key");
jedis.del("key");
//给一个key叠加value
jedis.append("key","value2");//此时key的值就是value + value2;
//同时给多个key进行赋值:
jedis.mset("key1","value1","key2","value2");
3、对 map 进行操作
Map<String,String> user = new HashMap();
user.put("key1","value1");
user.put("key2","value2");
user.put("key3","value3");
//存入
jedis.hmset("user",user);
//取出user中key1
List<String> nameMap = jedis.hmget("user","key1");
//删除其中一个键值
jedis.hdel("user","key2");
//是否存在一个键
jedis.exists("user");
//取出所有的Map中的值:
Iterator<String> iter = jedis.hkeys("user").iterator();
while(iter.next()){jedis.hmget("user",iter.next());
}
17、Redis 为什么是单线程的?
- 代码更清晰,处理逻辑更简单;
- 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
- 不存在多线程切换而消耗 CPU;
- 无法发挥多核 CPU 的优势,但可以采用多开几个 Redis 实例来完善;
18、Redis 真的是单线程的吗?
Redis6.0 之前是单线程的,Redis6.0 之后开始支持多线程;
redis 内部使用了基于 epoll 的多路服用,也可以多部署几个 redis 服务器解决单线程的问题;
redis 主要的性能瓶颈是内存和网络;
内存好说,加内存条就行了,而网络才是大麻烦,所以 redis6 内存好说,加内存条就行了;
而网络才是大麻烦,所以 redis6.0 引入了多线程的概念,
redis6.0 在网络 IO 处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。
19、Redis 持久化有几种方式?
redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上;
AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库,就像 memcache 一样。
20、Redis 和 memecache 有什么区别?
1、Redis 相比 memecache,拥有更多的数据结构和支持更丰富的数据操作。
(1)Redis 支持 key-value,常用的数据类型主要有 String、Hash、List、Set、Sorted Set。
(2)memecache 只支持 key-value。
2、内存使用率对比,Redis 采用 hash 结构来做 key-value 存储,由于其组合式的压缩,其内存利用率会高于 memecache。
3、性能对比:Redis 只使用单核,memecache 使用多核。
4、Redis 支持磁盘持久化,memecache 不支持。
Redis 可以将一些很久没用到的 value 通过 swap 方法交换到磁盘。
5、Redis 支持分布式集群,memecache 不支持。
21、Redis 支持的 java 客户端都有哪些?
Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。
22、jedis 和 redisson 有哪些区别?
Jedis 和 Redisson 都是 Java 中对 Redis 操作的封装。Jedis 只是简单的封装了 Redis 的 API 库,可以看作是 Redis 客户端,它的方法和 Redis 的命令很类似。Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于 Jedis 更加大。但 Jedis 相比于 Redisson 更原生一些,更灵活。
23、什么是缓存穿透?怎么解决?
1、缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对用的 value,就应该去后端系统查找(比如 DB 数据库)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
2、怎么解决?
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 之后清理缓存。
对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 Bitmap 过滤。
3、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。
4、如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待;
- 做二级缓存;
- 不同的 key,设置不同的过期时间,让缓存失效的时间尽量均匀;
24、怎么保证缓存和数据库数据的一致性?
1、淘汰缓存
数据如果为较为复杂的数据时,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。
2、选择先淘汰缓存,再更新数据库
假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。
假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。
3、延时双删策略
如下场景:同时有一个请求 A 进行更新操作,另一个请求 B 进行查询操作。
- 请求 A 进行写操作,删除缓存
- 请求 B 查询发现缓存不存在
- 请求 B 去数据库查询得到旧值
- 请求 B 将旧值写入缓存
- 请求 A 将新值写入数据库
次数便出现了数据不一致问题。采用延时双删策略得以解决。
public void write(String key,Object data){redisUtils.del(key);db.update(data);Thread.Sleep(100);redisUtils.del(key);
}
这么做,可以将 1 秒内所造成的缓存脏数据,再次删除。这个时间设定可根据俄业务场景进行一个调节。
4、数据库读写分离的场景
两个请求,一个请求 A 进行更新操作,另一个请求 B 进行查询操作。
- 请求 A 进行写操作,删除缓存
- 请求 A 将数据写入数据库了,
- 请求 B 查询缓存发现,缓存没有值
- 请求 B 去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
- 请求 B 将旧值写入缓存
- 数据库完成主从同步,从库变为新值
依旧采用延时双删策略解决此问题。
25、Redis,什么是缓存穿透?怎么解决?
1、缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对用的 value,就应该去后端系统查找(比如 DB 数据库)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
2、怎么解决?
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 之后清理缓存。
对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 Bitmap 过滤。
3、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。
4、如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待;
- 做二级缓存;
- 不同的 key,设置不同的过期时间,让缓存失效的时间尽量均匀;
26、Redis 怎么实现分布式锁?
使用 Redis 实现分布式锁
redis 命令:set users 10 nx ex 12 原子性命令
//使用uuid,解决锁释放的问题
@GetMapping
public void testLock() throws InterruptedException {String uuid = UUID.randomUUID().toString();Boolean b_lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);if(b_lock){Object value = redisTemplate.opsForValue().get("num");if(StringUtils.isEmpty(value)){return;}int num = Integer.parseInt(value + "");redisTemplate.opsForValue().set("num",++num);Object lockUUID = redisTemplate.opsForValue().get("lock");if(uuid.equals(lockUUID.toString())){redisTemplate.delete("lock");}}else{Thread.sleep(100);testLock();}
}
备注:可以通过 lua 脚本,保证分布式锁的原子性。
27、Redis 分布式锁有什么缺陷?
Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
Redis 容易产生的几个问题:
- 锁未被释放
- B 锁被 A 锁释放了
- 数据库事务超时
- 锁过期了,业务还没执行完
- Redis 主从复制的问题
28、Redis 如何做内存优化?
1、缩短键值的长度
- 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
- 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
- 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
- 以 JAVA 为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo 等
2、共享对象池
对象共享池指 Redis 内部维护 [0-9999] 的整数对象池。创建大量的整数类型 redisObject 存在内存开销,每个 redisObject 内部结构至少占 16 字节,甚至超过了整数自身空间消耗。所以 Redis 内存维护一个 [0-9999] 的整数对象池,用于节约内存。 除了整数值对象,其他类型如 list,hash,set,zset 内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
3、字符串优化
4、编码优化
5、控制 key 的数量