解析一些 java 复杂面试题的简单操作

java 虚拟机

什么时候会触发 full gc

System.gc() 方法的调用

老年代空间不足

永生区空间不足(JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中又被习惯称为永生代或者永生区,Permanet Generation 中存放的为一些 class 的信息、常量、静态变量等数据)

GC 时出现 promotion failed 和 concurrent mode failure

统计得到的 Minor GC 晋升到旧生代平均大小大于老年代剩余空间

堆中分配很大的对象

可以作为 root 的对象:

类中的静态变量,当它持有一个指向一个对象的引用时,它就作为 root

活动着的线程,可以作为 root

一个 Java 方法的参数或者该方法中的局部变量,这两种对象可以作为 root

JNI 方法中的局部变量或者参数,这两种对象可以作为 root

例子:下述的 Something 和 Apple 都可以作为 root 对象。

Java 方法的参数和方法中的局部变量,可以作为 root.

新生代转移到老年代的触发条件

长期存活的对象

大对象直接进入老年代

minor gc 后,survivor 仍然放不下

动态年龄判断 ,大于等于某个年龄的对象超过了 survivor 空间一半 ,大于等于某个年龄的对象直接进入老年代

redis

redis 单线程问题

单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。

为什么说 redis 能够快速执行

绝大部分请求是纯粹的内存操作(非常快速)

采用单线程, 避免了不必要的上下文切换和竞争条件

非阻塞 IO - IO 多路复用

redis 的内部实现

内部实现采用 epoll,采用了 epoll+ 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间 这 3 个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说 redis 为特殊的场景选择了合适的技术方案。

Redis 关于线程安全问题

redis 实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个 redis 操作的复合操作来说,依然需要锁,而且有可能是分布式锁。

使用 redis 有哪些好处?

速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1)

支持丰富数据类型,支持 string,list,set,sorted set,hash

支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除

redis 相比 memcached 有哪些优势?

memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型

redis 的速度比 memcached 快很多

redis 可以持久化其数据

数据库

B 树和 B+ 树的区别

B 树,每个节点都存储 key 和 data,所有节点组成这棵树,并且叶子节点指针为 nul,叶子结点不包含任何关键字信息。

B+ 树,所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接,所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而 B 树的非终节点也包含需要查找的有效信息)

为什么说 B+ 比 B 树更适合实际应用中操作系统的文件索引和数据库索引?

B+ 的磁盘读写代价更低 B+ 的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说 IO 读写次数也就降低了。

B+-tree 的查询效率更加稳定 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

java 高并发

JAVA 线程状态转换图示

synchronized 的底层怎么实现

同步代码块(Synchronization)基于进入和退出管程 (Monitor) 对象实现。每个对象有一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:

如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。

如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1.

如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。

被 synchronized 修饰的同步方法并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM 就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

想要了解架构技术知识点的,可以关注我一下,我后续也会整理更多关于分布式架构这一块的知识点分享出来,另外顺便给大家推荐一个交流学习群:697–57–9-75-1,里面会分享一些资深架构师录制的视频录像:有 Spring,MyBatis,Netty 源码分析,高并发、高性能、分布式、微服务架构的原理,JVM 性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多