抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(三)

举报
aoho 发表于 2021/05/16 20:31:58 2021/05/16
【摘要】 继续上一篇详细分析面试题答案以及复习参考和整理的面试资料。 jvm参数 为什么要配置?堆空间主要组成部分:新生代(new generation),新生代又划分为3部分:edenFrom Survivor(s0区域)To Survivor(s1区域)其中s0和s1区域大小相等老年代(tenured generation)new出来的对象都会存放在堆内存中。新生代和老年代的存在主要用于垃圾回收机...

继续上一篇详细分析面试题答案以及复习参考和整理的面试资料。

jvm参数 为什么要配置?

堆空间主要组成部分:

  • 新生代(new generation),新生代又划分为3部分:
    • eden
    • From Survivor(s0区域)
    • To Survivor(s1区域)
      其中s0和s1区域大小相等
  • 老年代(tenured generation)

new出来的对象都会存放在堆内存中。新生代和老年代的存在主要用于垃圾回收机制,其中主要针对的是新生代,因为对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。

在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:

  • GC的时间足够的小
  • GC的次数足够的少
  • 发生Full GC(新生代和老年代)的周期足够的长

要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

  • 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
  • 年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调节二者的比例为1:3或者1:4,当然也可以设置新生代的大小,原则上为堆空间的1/3或者1/4。
8G内存的机器 java进程最大配置多少?

增大堆内存(-Xms,-Xmx)会减少可创建的线程数量,增大线程栈内存(-Xss,32位系统中此参数值最小为60K)也会减少可创建的线程数量。

操作系统限制,系统最大可开线程数,主要受以下几个参数影响:

  • /proc/sys/kernel/thread-max:系统可以生成的最大线程数量
  • /proc/sys/kernel/pid_max:并不是threads-max,就能创建越多的线程,发现线程数量在达到一定数量以后不再增长。创建的线程数还受到系统可创建的最大pid数影响,默认值为32768。
  • max_user_process: 64位Linux系统,这个参数还是会限制线程数量,可通过ulimit –a查看。
  • /proc/sys/vm/max_map_count:包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。
平衡树的种类

平衡树是一颗二叉搜索树,并且它的深度保持相对稳定,也就是不会退化成链的树。平衡树可以说是区间操作的数据结构中效率高的一种,它最大的用处自然是维护区间了。细分为:splay、有旋/无旋treap、AVL树、替罪羊树、二叉查找树(SBT)树等。

跳表和平衡树区别

Redis sortedset 使用的是跳表。跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。

二叉树可以用来实现字典和有序表等抽象数据结构。在元素随机插入的场景,二叉树可以很好应对。然而,在有序插入的情况下,二叉树就退化了(链表),性能非常差。如果有办法对待插入元素进行随机排列,二叉树大概率可以运行良好。大部分情况下,插入是在线进行的,因此随机排列并不具有可行性。平衡树在操作时对树结构进行调整以满足平衡条件,因此获得理想性能。

跳表是一种概率性可行的平衡二叉树替代数据结构。跳表通过一个随机数生成器实现平衡。虽然跳表最坏情况下(worst-case)性能也很差,但是没有任何输入序列必然会导致最坏情况发生(这点类似划分元素(pivot point)随机选定的快排)。跳表极度不平衡发生的概率非常低(一个包含250个元素的字典,一次查找需要花3倍期望时间的概率小于百万分之一)。跳表平衡概率跟随机插入的二叉树差不多,好处是插入顺序不要求随机。

实现概率性平衡比严格控制平衡要简单得多。对很多应用来说,跳表用起来比平衡树更自然,而且算法更简单。跳表算法简单性意味着更容易实现,而且与平衡树和自适应树相比有常数倍数的性能提升。跳表在空间上也比较高效。平均每个元素只需要额外耗费个2指针(甚至可以配置得更低),并不需要在每个节点上都存与平衡和优先级相关的数据。

volatile,内存重排序到底怎么避免的?

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。这是就需要内存屏障来保证可见性了。

内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

  • 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
  • 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

一个线程在内存中如何存储

从线程和进程的角度来说,进程是资源分配的最小单位,线程是独立调度的最小单位。

同一个进程中的多个线程之间可以并发执行,他们共享进程资源。
线程不拥有资源,线程可以访问隶属进程的资源,进程有自己的独立空间地址,线程没有自己的独立空间地址,但是线程有自己的堆栈和局部变量。

线程的栈、程序计数器、本地方法区也是存放在进程的地址空间上,只是这些栈、程序计数器、本地方法区都只能有某个特定的线程去访问、其他的线程访问不到。如果使用C/C++语言的话,数组越界后,很容易就访问到其他线程的栈了,以致有可能导致其他线程的异常。

线程池,堵塞队列为什么要用堵塞?

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)。

中间件

Tomcat框架的 selevet

Tomcat是目前市场上主流Web服务器之一,是用Java语言开发的项目。Tomcat支持Servlet和JSP的规范,它由一组嵌套的层次和组件组成。所有组件都实现lifecycle生命周期方法,里面包含了init、start、stop、destroy等方法,用来控制生命周期。

Servlet是用Java编写的Server端程序,它与协议和平台无关。Servlet运行于Java-enabled Web Server中。Java Servlet可以动态地扩展Server的能力,并采用请求-响应模式提供Web服务。最早支持Servlet技术的是JavaSoft的Java Web Server。此后,一些其它的基于Java的Web Server开始支持标准的Servlet API。Servlet的主要功能在于交互式地浏览和修改数据,生成动态Web内容。

Tomcat服务器本质是通过ServerSocket与客户端进行通信,要进行通信首先就要进行TCP连接,Tomcat有两个核心组件,Connecter和Container,Connecter将在某个指定的端口上侦听客户请求,接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,Request包含了用户的请求信息,Response负责记录了服务器的答复内容。然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给Container处理。

Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心。

当Connector处理完后会调用Container的invoke()方法,你可以想象Container容器里有一条管道,管道上有很多阀门,每个阀门都会根据request进行一些操作,request和response请求会依次经过这些阀门,而Servlet就是该管道的最后一道阀门,之前的阀门就是filter。

Tomcat容器也分有上下层级关系如下图,Tomcat的四层容器不都是必须的,一般简单的容器只有Context和Wrapper两层,Contenxt负责管理多个Wrapper,负责将映射转发到对应Wrapper,当然期间还要经过filter过滤。Wrapper是最低层的容器,它只包裹着一个Servlet,Wrapper负责加载并管理调用Servlet服务。

mysql B+ B- B区别?

B树,即二叉搜索树,有如下特点:

  • 所有非叶子节点至多拥有两个儿子(Leaf和Right)
  • 左右结点存储一个关键字
  • 非叶子节点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树

B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;如果B树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变B树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销。

B-树,是一种多路搜索树(并不是二叉的):

*B树和B-树是同一种树,只不过英语中B-tree被中国人翻译成了B-树,让人以为B树和B-树是两种树,实际上,两者就是同一种树。此处单从算法的角度进行了划分,区别于 B+ 树,可以参见:*https://en.wikipedia.org/wiki/Binary_search_tree。

  • 关键字集合分布在整颗树中
  • 任何一个关键字出现且只出现在一个结点中
  • 搜素有可能在非叶子节点结束
  • 其搜索性能等价于在关键字全集内做一次二分查找
  • 自动层次控制

由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率。所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题,由于 M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占 M/2 的结点,删除结点时,需将两个不足M/2的兄弟节点合并。B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果
命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。

B+树,B+树是B-树的变体,也是一种多路搜索树,其定义基本与B-树同,除了:

  • 非叶子结点的子树指针与关键字个数相同;
  • 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
  • 为所有叶子结点增加一个链指针;
  • 所有关键字都在叶子结点出现

B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;更适合文件索引系统。

mysql 隔离级别?

数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务拥有四个重要的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。

SQL 标准定义的四种隔离级别:

  • READ UNCOMMITTED(读未提交):该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
  • READ COMMITTED(读提交):
    一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
  • REPEATABLE READ(可重复读):
    该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制来避免幻读。
  • SERIALIZABLE(序列化):
    在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

MVCC如何保证可重复读?

MySQL的innodb引擎是如何实现MVCC的。innodb会为每一行添加两个字段,分别表示该行创建的版本和删除的版本,填入的是事务的版本号,这个版本号随着事务的创建不断递增。在repeated read的隔离级别下,具体各种数据库操作的实现:

事务开始,第一次不加锁SELECT时,InnoDB从全局事务链表中,筛选所有活动事务(事务trx_id严格递增),生成当前一致性视图。

根据当前一致性视图高低水位,计算事务可见性。

根据可见事务redo log,逆向算出历史版本。SELECT快照读,读之前版本数据。SELECT FOR UPDATE 或 UPDATE 当前读,加行锁读当前值,不会创建一致性视图,有其它事务更新时,等待其它事务提交。(2PL更新时加写锁,事务提交时才会释放)

间隙锁怎么使用?

InnoDB 行级锁是通过给索引上的索引项加锁来实现的,InnoDB行级锁只有通过索引条件检索数据,才使用行级锁;否则,InnoDB使用表锁。

在不通过索引(主键)条件查询的时候,InnoDB是表锁而不是行锁。也就是说,在没有使用索引的情况下,使用的就是表锁。

间隙锁可以理解为是对于一定范围内的数据进行锁定,如果说这个区间没有这条数据的话也是会锁住的;主要是解决幻读的问题,如果没有添加间隙锁。如果其他事务中添加 id 在 1 到 100 之间的某条记录,此时会发生幻读;另一方面,视为了满足其恢复和赋值的需求(幻读的概念在上面有提到)。

默认情况下,innodb_locks_unsafe_for_binlog是0(禁用),这意味着启用了间隙锁定:InnoDB使用下一个键锁进行搜索和索引扫描。若要启用该变量,请将其设置为1。这将导致禁用间隙锁定:InnoDB只使用索引记录锁进行搜索和索引扫描。

innodb自动使用间隙锁的条件:

  • 必须在RR级别下
  • 检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)

间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:

  • 防止间隙内有新数据被插入
  • 防止已存在的数据,更新成间隙内的数据(例如防止 number=3 的记录通过update变成 number=5)
mysql hash索引使用场景?

hash索引的特点:

  • hash索引是基于hash表实现的,只有查询条件精确匹配hash索引中的所有列的时候,才能用到hash索引。
  • 对于hash索引中的所有列,存储引擎都会为每一行计算一个hash码,hash索引中存储的就是hash码。
  • hash索引包括键值、hash码和指针 。

在MySQL的存储引擎中,MyISAM 不支持哈希索引,而 InnoDB 中的hash索引是存储引擎根据B-Tree索引自建的。因为hash索引本身只需要存储对应的hash值,所以索引的结构十分紧凑,这也让hash索引查找的速度非常快。然而,哈希索引也有限制,如下:

  • 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行(即不能使用哈希索引来做覆盖索引扫描),不过,访问内存中的行的速度很快(因为memory引擎的数据都保存在内存里),所以大部分情况下这一点对性能的影响并不明显。
  • 哈希索引数据并不是按照索引列的值顺序存储的,所以也就无法用于排序
    哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引的全部列值内容来计算哈希值的。如:数据列(a,b)上建立哈希索引,如果只查询数据列a,则无法使用该索引。
  • 哈希索引只支持等值比较查询,如:=,in(),<=>(注意,<>和<=>是不同的操作),不支持任何范围查询(必须给定具体的where条件值来计算hash值,所以不支持范围查询)。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  • 如果哈希冲突很多的话,一些索引维护操作的代价也很高,如:如果在某个选择性很低的列上建立哈希索引(即很多重复值的列),那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应的引用,冲突越多,代价越大。
redis 为什么快?

完全基于内存,绝大多数请求是纯粹的内存操作,非常快速。数据存储在内存中,类似于HashMap,具备较快的查找和操作的时间复杂度O(1)。

数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用考虑各种锁的问题,不存在加锁释放锁(上下文的切换),没有因为可能出现死锁而导致的性能消耗。
使用多路I/O复用模型,非阻塞IO。

使用底层模型不同,它们之间底层实现方式以及与客户端之间的通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求(用户态和内核态之间的切换)。

未完待续

本来本文的标题是 抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(下),然题目比较多,限于篇幅,下篇我们继续。

推荐阅读

面试合集

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。