Java经典面试题最全汇总208道(六)

Java
269
0
0
2023-07-23
标签   Java面试
目录
  • 前言 
  • 181、什么是类加载器,类加载器有哪些?
  • 182、说一下类加载的执行过程?
  • 183、JVM的类加载机制是什么?
  • 184、什么是双亲委派模型?
  • 185、怎么判断对象是否可以被回收?
  • 186、说一下 jvm 有哪些垃圾回收算法?
  • 187、说一下 jvm 有哪些垃圾回收器?
  • 188、JVM栈堆概念,何时销毁对象
  • 189、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
  • 190、详细介绍一下 CMS 垃圾回收器?
  • 191、简述分代垃圾回收器是怎么工作的?
  • 192、Redis是什么?
  • 193、Redis都有哪些使用场景?
  • 194、Redis有哪些功能?
  • 195、Redis支持的数据类型有哪些?
  • 196、Redis取值存值问题
  • 197、Redis为什么是单线程的?
  • 198、Redis真的是单线程的吗?
  • 199、Redis持久化有几种方式?
  • 200、Redis和 memecache 有什么区别?
  • 201、Redis支持的 java 客户端都有哪些?
  • 202、jedis 和 redisson 有哪些区别?
  • 203、什么是缓存穿透?怎么解决?
  • 204、怎么保证缓存和数据库数据的一致性?
  • 205、Redis,什么是缓存穿透?怎么解决?
  • 206、Redis怎么实现分布式锁?
  • 207、Redis分布式锁有什么缺陷?
  • 208、Redis如何做内存优化?

前言 

短时间提升自己最快的手段就是背面试题,最近总结了Java常用的面试题,分享给大家,希望大家都能圆梦大厂,加油,我命由我不由天。

181、什么是类加载器,类加载器有哪些?

1、什么是类加载器?

类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。

2、类加载器有哪些?

JVM有三种类加载器:

(1)启动类加载器

该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader。

(2)扩展类加载器

它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录。

(3)应用程序类加载器

它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path所指定的目录中加载类,它是自定义的类加载器的父加载器。

182、说一下类加载的执行过程?

当程序主动使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接、初始化3个步骤对该类进行类加载。

1、加载

加载指的是将类的class文件读入到内存中,并为之创建一个java.lang.Class对象。

类的加载由类加载器完成,类加载器由JVM提供,开发者也可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器可以从不同来源加载类的二进制数据,通常有如下几种来源:

从本地文件系统加载从jar包加载通过网络加载把一个Java源文件动态编译,并执行加载

2、连接

当类被加载之后,系统为之生成一个对应的Class对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE中。

类连接又可分为三个阶段:

(1)验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

(2)准备

为类的静态变量分配内存,并设置默认初始值。

(3)解析

将类的二进制数据中的符号引用替换成直接引用。

3、初始化

为类的静态变量赋予初始值。

183、JVM的类加载机制是什么?

JVM类加载机制主要有三种:

1、全盘负责

类加载器加载某个class时,该class所依赖的和引用其它的class也由该类加载器载入。

2、双亲委派

先让父加载器加载该class,父加载器无法加载时才考虑自己加载。

3、缓存机制

缓存机制保证所有加载过的class都会被缓存,当程序中需要某个class时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成class对象,存入缓存区中。

这就是为什么修改了class后,必须重启JVM,程序所做的修改才会生效的原因。

184、什么是双亲委派模型?

如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行

如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器

如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。

双亲委派模式的优势:

避免重复加载;

考虑到安全因素,java核心api中定义类型不会被随意替换

假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动加载器,而启动加载器在核心Java API中发现同名的类,发现该类已经被加载,就不会重新加载网络传递的Integer类,而直接返回已加载过的Integer.class,这样可以防止核心API库被随意篡改。

185、怎么判断对象是否可以被回收?

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)本地方法栈引用的对象

186、说一下 jvm 有哪些垃圾回收算法?

1、对象是否已死算法

引用计数器算法可达性分析算法

2、GC算法

(1)标记清除算法

如果对象被标记后进行清除,会带来一个新的问题--内存碎片化。

如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

(2)复制算法(Java堆中新生代的垃圾回收算法)

  • 先标记待回收内存和不用回收内存;
  • 将不用回收的内存复制到新的内存区域;
  • 就的内存区域就可以被全部回收了,而新的内存区域也是连续的;
  • 缺点是损失部分系统内存,因为腾出部分内存进行复制。

(3)标记压缩算法(Java堆中老年代的垃圾回收算法)

对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。

标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。

老年代的垃圾回收算法称为“Major GC”。

187、说一下 jvm 有哪些垃圾回收器?

说一下 jvm 有哪些垃圾回收器?

188、JVM栈堆概念,何时销毁对象

类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java自动垃圾回收,也可以用System.gc()开启回收器,但是回收器不一定会马上回收。

  • 静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
  • 实例变量在类实例化对象时创建,在对象销毁的时候销毁;
  • 局部变量在局部范围内使用时创建,跳出局部范围时销毁

189、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

新生代回收器:Serial、ParNew、Parallel Scavenge

老年代回收器:Serial Old、Parallel Old、CMS

新生代回收器一般采用的是复制算法,复制算法效率较高,但是浪费内存;

老生代回收器一般采用标记清楚算法,比如最常用的CMS;

190、详细介绍一下 CMS 垃圾回收器?

CMS 垃圾回收器是Concurrent Mark Sweep,是一种同步的标记-清除,CMS分为四个阶段:

  • 初始标记,标记一下GC Root能直接关联到的对象,会触发“Stop The World”;
  • 并发标记,通过GC Roots Tracing判断对象是否在使用中;
  • 重新标记,标记期间产生对象的再次判断,执行时间较短,会触发“Stop The World”;
  • 并发清除,清除对象,可以和用户线程并发进行;

191、简述分代垃圾回收器是怎么工作的?

分代回收器分为新生代和老年代,新生代大概占1/3,老年代大概占2/3;

新生代包括Eden、From Survivor、To Survivor;

Eden区和两个survivor区的 的空间比例 为8:1:1 ;

垃圾回收器的执行流程:

  1. 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  2. 清空 Eden + From Survivor 分区,From Survivor 和 To Survivor 分区交换;每次交换后存活的对象年龄+1,到达15,升级为老年代,大对象会直接进入老年代;
  3. 老年代中当空间到达一定占比,会触发全局回收,老年代一般采取标记-清除算法;

192、Redis是什么?

Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sorted set --有序集合)和hash。

这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

在此基础上,Redis支持各种不同方式的排序。

为了保证效率,数据都是缓存在内存中,Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

193、Redis都有哪些使用场景?

Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作通过list取最新的N条数据模拟类似于token这种需要设置过期时间的场景发布订阅消息系统定时器、计数器

194、Redis有哪些功能?

1、基于本机内存的缓存

当调用api访问数据库时,假如此过程需要2秒,如果每次请求都要访问数据库,那将对服务器造成巨大的压力,如果将此sql的查询结果存到Redis中,再次请求时,直接从Redis中取得,而不是访问数据库,效率将得到巨大的提升,Redis可以定时去更新数据(比如1分钟)。

2、如果电脑重启,写入内存的数据是不是就失效了呢,这时Redis还提供了持久化的功能。

3、哨兵(Sentinel)和复制

Sentinel可以管理多个Redis服务器,它提供了监控、提醒以及自动的故障转移功能;

复制则是让Redis服务器可以配备备份的服务器;

Redis也是通过这两个功能保证Redis的高可用;

4、集群(Cluster)

单台服务器资源总是有上限的,CPU和IO资源可以通过主从复制,进行读写分离,把一部分CPU和IO的压力转移到从服务器上,但是内存资源怎么办,主从模式只是数据的备份,并不能扩充内存;

现在我们可以横向扩展,让每台服务器只负责一部分任务,然后将这些服务器构成一个整体,对外界来说,这一组服务器就像是集群一样。

195、Redis支持的数据类型有哪些?

字符串hashlistsetzset

196、Redis取值存值问题

1、先把Redis的连接池拿出来

JedisPool pool = new JedisPool(new JedisPoolConfig(),".0.0.1");
 
Jedis jedis = pool.getResource();

2、存取值

jedis.set("key","value");
jedis.get("key");
jedis.del("key");
//给一个key叠加value
jedis.append("key","value");//此时key的值就是value + value2;
//同时给多个key进行赋值:
jedis.mset("key","value1","key2","value2");

3、对map进行操作

Map<String,String> user = new HashMap();
user.put("key","value1");
user.put("key","value2");
user.put("key","value3");
//存入
jedis.hmset("user",user);
//取出user中key 
List<String> nameMap = jedis.hmget("user","key");
//删除其中一个键值
jedis.hdel("user","key");
//是否存在一个键
jedis.exists("user");
//取出所有的Map中的值:
Iterator<String> iter = jedis.hkeys("user").iterator();
while(iter.next()){
    jedis.hmget("user",iter.next());
}

197、Redis为什么是单线程的?

  • 代码更清晰,处理逻辑更简单;
  • 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
  • 不存在多线程切换而消耗CPU;
  • 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善

198、Redis真的是单线程的吗?

Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;

redis内部使用了基于epoll的多路服用,也可以多部署几个redis服务器解决单线程的问题;

redis主要的性能瓶颈是内存和网络;

内存好说,加内存条就行了,而网络才是大 麻烦,所以redis6内存好说,加内存条就行了;

而网络才是大 麻烦,所以redis6.0引入了多线程的概念,

redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。

199、Redis持久化有几种方式?

redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。

RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;

AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。

其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。

如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。

200、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不支持。

201、Redis支持的 java 客户端都有哪些?

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。

202、jedis 和 redisson 有哪些区别?

Jedis 和 Redisson 都是Java中对Redis操作的封装。

Jedis 只是简单的封装了 Redis 的API库,可以看作是Redis客户端,它的方法和Redis 的命令很类似。

Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更加大。

但Jedis相比于Redisson 更原生一些,更灵活。

203、什么是缓存穿透?怎么解决?

1、缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对用的value,就应该去后端系统查找(比如DB数据库)。

一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。

这就叫做缓存穿透。

2、怎么解决?

对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert之后清理缓存。

对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该Bitmap过滤。

3、缓存雪崩

当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。

4、如何解决?

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。

比如

  • 对某个key只允许一个线程查询数据和写缓存,其它线程等待;
  • 做二级缓存;
  • 不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀;

204、怎么保证缓存和数据库数据的一致性?

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();
    redisUtils.del(key);
}

这么做,可以将1秒内所造成的缓存脏数据,再次删除。这个时间设定可根据俄业务场景进行一个调节。

4、数据库读写分离的场景

两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

  1. 请求A进行写操作,删除缓存
  2. 请求A将数据写入数据库了
  3. 请求B查询缓存发现,缓存没有值
  4. 请求B去从库查询这时,还没有完成主从同步,因此查询到的是旧值
  5. 请求B将旧值写入缓存
  6. 数据库完成主从同步,从库变为新值

依旧采用延时双删策略解决此问题。

205、Redis,什么是缓存穿透?怎么解决?

1、缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对用的value,就应该去后端系统查找(比如DB数据库)。

一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。

这就叫做缓存穿透。

2、怎么解决?

对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert之后清理缓存。

对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该Bitmap过滤。

3、缓存雪崩

当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。

4、如何解决?

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。

比如

  1. 对某个key只允许一个线程查询数据和写缓存,其它线程等待;
  2. 做二级缓存;
  3. 不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀;

206、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,, 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();
        testLock();
    }
}

备注:可以通过lua脚本,保证分布式锁的原子性。

207、Redis分布式锁有什么缺陷?

Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。

Redis容易产生的几个问题:

  1. 锁未被释放
  2. B锁被A锁释放了
  3. 数据库事务超时
  4. 锁过期了,业务还没执行完
  5. Redis主从复制的问题

208、Redis如何做内存优化?

1、缩短键值的长度

  1. 缩短值的长度才是关键,如果值是一个大的业务对象,可以将对象序列化成二进制数组;
  2. 首先应该在业务上进行精简,去掉不必要的属性,避免存储一些没用的数据;
  3. 其次是序列化的工具选择上,应该选择更高效的序列化工具来降低字节数组大小;
  4. 以Java为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等

2、共享对象池

对象共享池指Redis内部维护[0-9999]的整数对象池

。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。

所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。

除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。

因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

3、字符串优化

4、编码优化

5、控制key的数量

系列文章:

Java经典面试题最全汇总208道(一)

Java经典面试题最全汇总208道(二)

Java经典面试题最全汇总208道(三)

Java经典面试题最全汇总208道(四)

Java经典面试题最全汇总208道(五)

Java经典面试题最全汇总208道(六)