丰巢面试经验

有时候觉得自己很厉害,遇到什么问题都能解决,即使不是最优解。但有时候又觉得自己很不行。这一次面试其实都是问的基础,但是有些东西真的是没有记住。看过的东西没怎么用就忘记了。

标红的是没回答上来的题目。

问题1、JVM内存管理分哪几块?哪些是线程共享哪些是线程私有的?

答:堆(存放对象和数组)和方法区(存放JVM加载类信息、常量、静态变量、即时编译器编译后的代码等)线程共享。JVM栈(存放局部变量表)、本地方法栈(JVM栈是为JVM执行Java方法服务,本地方法栈是为JVM使用的Native方法服务)、程序计数器(线程所执行字节码行号指示器)线程私有。

问题2、那堆里怎么分代的?垃圾回收算法是什么?

答:新生代分Eden空间、From Survivor空间、To Survivor空间,比例8:1:1,垃圾回收采用标记复制算法。老年代采用标记清除算法。

问题3、怎么判断对象是否可回收?

答:

1)可达性分析算法:

通过判断对象的引用链是否可达来决定对象是否可以被回收,可以作为GC Root的对象:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表);
  • 方法区中的常量引用对象;
  • 方法区中类静态属性引用对象;
  • 本地方法栈中JNI(Native方法)的引用对象;
  • 活跃线程中的引用对象;

2)引用计数算法:

判断对象的引用数量,通过判断对象的引用数量来决定对象是否可以被回收;每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1;任何引用计数为0的对象实例可以被当作垃圾收集;

优点:执行效率高,程序执行受影响较小;缺点:无法检测出循环引用的情况,导致内存泄漏;

问题4、垃圾回收器有哪些?比如CMS,你了解多少?

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

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

  • 整堆回收器:G1

其中相互连线的垃圾回收器,表示可以相互搭配使用。

新生代(Young Generation)

程序中的大部分对象都符合“朝生夕死”的特性,所以绝大数新创建的对象都会存放在新生代,除非是大对象会直接进入老生代。新生代采用的是复制算法,这样可以更高效的回收内存空间。

新生代垃圾回收的执行过程:

1、Eden 区 + From Survivor 区存活着的对象复制到 To Survivor 区;

2、清空 Eden 和 From Survivor 分区;

3、From Survivor 和 To Survivor 分区交换(From 变 To,To 变 From)。

老生代(Tenured Generation)

老生代垃圾回收的频率比新生代低,存放的主要对象是:

1、新生代对象经过 N 次 GC 晋升到老年代。

可以通过设置 -XX:MaxTenuringThreshold=5 来设置,默认值是 15 次。

2、大对象直接存储到老生代。

所谓的“大对象”指的是需要连续存储空间的对象,比如:数组。

当大对象在新生代存储不下的时候,就需要分配担保机制,把当前新生代的所有对象复制到老年代中,因为分配担保机制需要涉及大量的复制,会导致性能问题,所有最好的方案是直接把大对象存储到老生代中。

通过参数 -xx:PretrnureSizeThreshold 来设定大对象的值。

注意:该参数只有 Serial 和 ParNew 垃圾回收器有效。

Serial

Serial 最早的垃圾回收器,JDK 1.3.1 之前新生代唯一的垃圾回收器,使用的是单线程串行回收方式,在单 CPU 环境下性能较好,因为单线程执行不存在线程切换。

线程类型: 单线程

使用算法: 复制算法

指定收集器: -XX:+UseSerialGC

Serial Old

Serial 收集器的老年代版本,同样也是单线程的。它有一个实用的用途作为CMS收集器的备选预案,后面介绍CMS的时候会详细介绍。

线程类型: 单线程

使用算法: 标记-整理

指定收集器: -XX:+UseSerialGC

ParNew

ParNew 其实就是 Serial 的多线程版本,可以和 Serial 共用很多控制参数,比如:-XX:SurvivorRatio , ParNew 可以和 CMS 配合使用。

640?wx_fmt=png

线程类型: 多线程

使用算法: 复制

指定收集器: -XX:+UseParNewGC

Parallel Scavenge

Parallel 和 ParNew 收集器类似,也是多线程的,但 Parallel 是吞吐量优先的收集器,GC停顿时间的缩短是以吞吐量为代价的,比如收集 100MB 的内存,需要 10S 的时间,CMS 则会缩短为 7S 收集 50 MB 的内存,这样停顿的时间确实缩少了,但收集的频率变大了,吞吐量就变小了。

线程类型: 多线程

使用算法: 复制

指定收集器: -XX:+UseParallelGC

Parallel Old

Parallel Old 是 Parallel 的老生代版本,同样是吞吐量优先的收集器。

线程类型: 多线程

使用算法: 标记-整理

指定收集器: -XX:+UseParallelOldGC

CMS

CMS(Concurrent Mark Sweep)一种以获得最短停顿时间为目标的收集器,非常适用B/S系统。

使用 Serial Old 整理内存。

CMS 运行过程:

640?wx_fmt=png

1、初始标记

标记 GC Roots 直接关联的对象,需要 Stop The World 。

2、并发标记

从 GC Roots 开始对堆进行可达性分析,找出活对象。

3、重新标记

重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要 Stop The World 。

4、并发清除

除垃圾对象。

CMS 缺点:

1、对 CPU 资源要求敏感。

CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程。

2、CMS无法清除浮动垃圾。

浮动垃圾指的是CMS清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除。

3、CMS 垃圾回收会产生大量空间碎片。

CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。

注意:CMS 收集器中,当老生代中的内存使用超过一定的比例时,系统将会进行垃圾回收;当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时采用 Serial Old 算法进行清除,此时的性能将会降低。

线程类型: 多线程

使用算法: 标记-清除

指定收集器: -XX:+UseConcMarkSweepGC

G1

G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

640?wx_fmt=png

G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),所以 G1 GC 值得深入掌握。

G1 运行过程:

1、初始标记

标记 GC Roots 直接关联的对象,需要 Stop The World 。

2、并发标记

从 GC Roots 开始对堆进行可达性分析,找出活对象。

3、重新标记

重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要 Stop The World 。

4、筛选回收

首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段可以与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的。

线程类型: 多线程

使用算法: 复制、标记-整理

指定收集器: -XX:+UseG1GC(JDK 7u4 版本后可用)

问题5、Mysql怎么解决慢查询?Mysql的底层数据结构除了B+树还有什么?B+树特点是什么?

分析慢查询语句:

可以通过Explain关键字(Explain关键字模拟优化器执行SQL查询语句)分析慢查询语句。

举个例子:

SELECT * FROM `user` ORDER BY age LIMIT 0,10 

上面的sql语句先从user表中通过age字段升序排序,然后再通过LIMIT关键字选取偏移量为0的后面的10条数据。

Explain SELECT * FROM `user` where id in (select user_id from `user_role` where role_id in (SELECT id from role where role.name = 'p8'))

这种语句嵌套了几次,通过Explain来分析查询语句。分析含义解释链接参考:[MySQL高级](一) EXPLAIN用法和结果分析_王洪玉的博客-CSDN博客_explain

语句可以优化成SELECT u.`name` FROM `user` AS u JOIN `user_role` AS ur ON u.id = ur.user_id JOIN role as r on r.id = ur.role_id WHERE r.`name`='P8'

mybatis中可以在SqlSessionTemplate类的invoke方法打断点debug看statementMap里要执行的SQL语句。

MyISAM和InnoDB两种存储引擎都采用了B+树

问题6、Volatile原语、Atomic原子类你了解多少?

synchronized是通过加锁来实现线程的安全性,而volatile的主要作用是在多处理器开发中保证共享变量对于多线程的可见性。可见性的意思是,当一个线程修改一个共享变量时,另外一个线程能读取到修改以后的值。

线程安全问题的本质其实就是 原子性、有序性、可见性。

  • 原子性 和数据库事务中的原子性一样,满足原子性特性的操作是不可中断的,要么全部执行成功要么全部执行失败
  • 有序性 编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的,重排序可能会导致多线程程序出现内存可见性问题
  • 可见性 多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值

原子性、有序性、可见性这些问题,我们可以认为是基于多核心CPU架构下的存在的问题。因为在单核CPU架构下,所有的线程执行都是基于CPU时间片切换,所以不存在并发问题。

硬件层面上,CPU的寄存器——>CPU的高速缓存——>内存——>磁盘。

原子性问题:在多核CPU架构下,在同一时刻对同一共享变量执行 decl指令(递减指令,相当于i--,它分为三个过程:读->改->写,这个指令涉及到两次内存操作,那么在这种情况下i的结果是无法预测的。

原子性解决办法:1)总线锁  2)缓存锁——MESI缓存一致性协议

  • M(Modify) 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
  • E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
  • S(Shared) 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
  • I(Invalid) 表示缓存已经失效

每个CPU核心不仅仅知道自己的读写操作,也会监听其他Cache的读写操作CPU的读取会遵循几个原则:

  • 如果缓存的状态是I,那么就从内存中读取,否则直接从缓存读取
  • 如果缓存处于M或者E的CPU 嗅探到其他CPU有读的操作,就把自己的缓存写入到主内存,并把自己的状态设置为S
  • 只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为M

MESI优化带来的可见性问题

前面说过MESI协议,也就是缓存一致性协议。这个协议存在一个问题,就是当CPU0修改当前缓存的共享数据时,需要发送一个消息给其他缓存了相同数据的CPU核心,这个消息传递给其他CPU核心以及收到消息完成各自缓存状态的切换这个过程中,CPU会等待所有缓存响应完成,这样会降低处理器的性能。为了解决这个问题,引入了 StoreBufferes存储缓存。

参考:架构师五分钟带你读懂,Volatile的作用及原理_PolarisHuster的博客-CSDN博客

问题7、AQS怎么实现的?

AQS全称AbstractQueuedSynchronizer,抽象的队列式同步器是Java并发包里面一个基础类,我们熟悉的很多并发工具同时通过继承它来实现的,譬如ReentrantLock, Semaphore, CountDownLatch等等。

AQS名为队列同步器,内部就是通过维护了一个FIFO队列来完成获取资源线程的排队工作,即如果同时有多个线程想获取共享资源,等待的线程将会进入此队列。AQS无法确定共享的资源到底是什么,但是可以需要维护共享资源的同步状态。实际上,AQS内部用一个int变量state来代表同步状态, 用volatile修饰来保证线程可见性。

参考链接:[并发]AQS实现原理 - 知乎

问题8、Dubbo框架怎么实现的?

Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合。总体架构如下,

参考链接:Dubbo架构设计及原理详解 - BarryW - 博客园

问题9、ThreadLocal原理?

ThreadLocal实际是Thread.ThreadLocalMap中一条记录的引用。ThreadLocalMap实际上在任何地方都是可以访问到的。所以,它l实际上是在存储全部变量,只不过这个变量比较特殊,每一个实例都和一个线程绑定了。

参考:面试官:听说你看过ThreadLocal源码?我来瞅瞅?

问题10、线程池怎么工作的?

  1. 通过execute方法提交任务时,当线程池中的线程数小于corePoolSize时,新提交的任务将通过创建一个新线程来执行,即使此时线程池中存在空闲线程。
  2. 通过execute方法提交任务时,当线程池中线程数量达到corePoolSize时,新提交的任务将被放入workQueue中,等待线程池中线程调度执行。
  3. 通过execute方法提交任务时,当workQueue已存满,且maximumPoolSize大于corePoolSize时,新提交的任务将通过创建新线程执行。
  4. 当线程池中的线程执行完任务空闲时,会尝试从workQueue中取头结点任务执行。
  5. 通过execute方法提交任务,当线程池中线程数达到maxmumPoolSize,并且workQueue也存满时,新提交的任务由RejectedExecutionHandler执行拒绝操作。
  6. 当线程池中线程数超过corePoolSize,并且未配置allowCoreThreadTimeOut=true,空闲时间超过keepAliveTime的线程会被销毁,保持线程池中线程数为corePoolSize。 
  7. 当设置allowCoreThreadTimeOut=true时,任何空闲时间超过keepAliveTime的线程都会被销毁。 

问题11、Redis怎么实现高性能的读写?

redis是基于内存进行操作的,性能较高。

PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization1)两种模式

  * 部分重同步功能由以下三个部分构成:
    * 主服务的复制偏移量(replication offset)和从服务器的复制偏移量
    * 主服务器的复制积压缓冲区(replication backlog),默认大小为1M
    * 服务器的运行ID(run ID),用于存储服务器标识,如从服务器断线重新连接,取到主服务器的运行ID与重接后的主服务器运行ID进行对比,从而判断是执行部分重同步还是执行完整重同步

问题12、MQ你了解多少?

问题13、JVM层面反射怎么实现的?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值