服务端面经_6_28

服务端面经错题集

Java基础

  1. 说出几种运行时异常
     
    NullPointerException - 空指针引用异常
    ClassCastException - 类型强制转换异常。
    IllegalArgumentException - 传递非法参数异常。
    ArithmeticException - 算术运算异常
    ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
    IndexOutOfBoundsException - 下标越界异常
    NegativeArraySizeException - 创建一个大小为负数的数组错误异常
    NumberFormatException - 数字格式异常
    SecurityException - 安全异常
    UnsupportedOperationException - 不支持的操作异常
  2. func(List a)能不能传ArrayList b?为什么? 不能,因为泛型必须一一对应,泛型不存在继承关系,但是list和arraylist之间存在继承关系,参数要求是list,可以传入其实现类。泛型不支持继承关系的原因是,泛型在设计之初就为了避免类型转换的缺陷,规定如果编译器不报错则运行期必然不会发生类型转换问题。数组会发生类型转换问题,如下:
    Integer[] iarr = new Integer[5];
    Number[] narr = iarr;
    narr[0] = 1.5;
    
    原理层面而言,会被擦除成Object,然后对类型进行强转,会被擦除成A,然后被强转。

JVM

内存分布

  1. 1.8后的内存分布情况

    1. 根据JVM官方文档,运行时常量池一定在方法区中。
    2. 根据JEP122,metadata放在metaspace中,字符串常量池和运行时常量池放在堆中。
    3. class对象放在堆中。
  2. 静态方法放在哪里?

    1. https://hxraid.iteye.com/blog/428891

    2. 静态方法放在方法区中。当调用方法时,字节码中是方法表中的序号invokestatic #13,根据序号可以去运行时常量池中确定方法的全限定名(包括类名,方法名,返回值,入参类型),根据全限定名就清楚要去找哪个类,然后进行类加载,从该类的方法区获得方法的直接地址,将直接地址保存到运行时常量池索引为13的常量表中(类加载的静态解析过程),然后根据方法对应的字节码运行方法,当下次再次执行invokestatic #13时,直接找到常量池索引为13的常量表,然后符号引用对应直接引用,直接找到方法字节码即可。以上过程适用于所有静态绑定类型,包括invokestatic和invokespecial。final方法虽然是invokevirtual,但是仍然是静态解析。

    3. 虚方法

      Father father = new Son();
      father.f1();
      0  new hr.test.Son [13] //在堆中开辟一个Son对象的内存空间,并将对象引用压入操作数栈  
      3  dup    
      4  invokespecial #7 [15] // 调用初始化方法来初始化堆中的Son对象   
      7  astore_1 //弹出操作数栈的Son对象引用压入局部变量1中  
      8  aload_1 //取出局部变量1中的对象引用压入操作数栈  
      9  invokevirtual #15 //调用f1()方法  
      12  return  
      

      先调用invokespecial调用实例构造器方法,这是静态解析的,然后用invokevirtual方法调用虚方法,首先根据#15查运行时常量池相对应索引表,然后获得该方法的全限定名,这里的归属类是父类,然后通过class对象查找父类方法表,如果无f1方法则报错,有f1方法则传参为this,并且获得f1方法在父类方法表中索引为n,这个this代指son对象,根据对象去找到son的方法表,找到第n个方法,如果重写则覆盖索引为n的方法,否则不覆盖。

    4. 接口的话,JVM 首先查看常量池,确定方法调用的符号引用(名称、返回值等等),然后利用 this 指向的实例得到该实例的方法表,进而搜索方法表来找到合适的方法地址。

      a.invokestatic:调用静态方法
      b.invokespecial:调用实例构造器方法,私有方法和父类方法。
      c.invokevirtual:调用虚方法。
      d.invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

  3. 基本数据类型存放在哪里? 如果是类的成员变量,那类的实例在堆里,成员变量必然也在堆里。如果是static类型的,那static类型变量均隶属于class对象,因此也在堆里。如果是static final,那么会在准备阶段就初始化完毕放在运行时常量池里,还在堆里。(1.8后运行时常量池可能在堆或者直接内存中,不确定还)。如果在方法中,那就在栈里。

  4. Class.forName(str) 用当前类(caller)的加载器去加载str这个全限定名所代表的的class文件,本质上做的是类加载,如果传参false就是不初始化,默认是true。new关键字做的就是先判断是否类加载,然后再调用构造方法,和class.forName在instance是一样的。Class.forName会完成一系列的类加载动作,classloader.loadClass只是把class文件加载到jvm中。

redis

底层数据结构

  1. redis的intset和ziplist是如何压缩的。
    1. intset默认是2字节一个数,如果插入大于65535会变成每个都是4字节,。。。8字节,以此来压缩空间。
    2. ziplist。因为数组向后遍历是移动一个数组元素大小的距离,每个元素大小固定,但是ziplist中每个节点大小可以是一样,因此每个节点都需要保存必要的偏移量。

Linux

指令

  1. du和df的区别:du->disk uasge,显示每个文件和目录的磁盘使用空间。df->disk free,能看到已经被删除的文件,指令等。
  2. fork:产生一个和父进程完全相同的子进程,但是子进程在之后会被exec系统调用。出于效率考虑,linux引入了写时复制技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。也就是说,在fork的时候,父子进程指向同一个物理内存区域,只有当父进程的某段内存内容变化时,才单独分配物理空间。

海量数据

  1. 海量数据找中位数。类似快排的切分法,以某数为中心分割为两个文件,再把文件较大的那个继续分割,直到只有一个数字。
  2. int 4字节整数的海量数据,如何给出一个数,判断是否在这堆数据里? 方法1:8台机器,每台机器2GB装载进内存,然后创建set,在8台机器上查询。 方法2:使用bitmap,申请Byte[] bitmap = new Byte[229],即一共有232位,然后每个位表示一个数的有无。IO流读数据写bitmap,然后来的数据只要判断bitmap相应的位数是0还是1即可。 方法3: 通过数据结构压缩的方式,先将40亿数据外部排序,然后比如123467这样的数据压缩成(1,4),(6,2)之类的数据结构,可以压缩连续的数据。
  3. TOP K问题,海量数据找前K个。 先用hash算法分配到多个文件中,对每个文件流读取进行小顶堆,然后把多个文件最后的TOP K合并,再重新建一个小顶堆。

数据结构与算法

算法

  1. 时间复杂度和空间复杂度。 时间复杂度表示算法运行的时间,空间复杂度表示算法运行占用的内存,一般用大O表示法 ,但是这个表示法并不是直接表示时间或者时间的数量级,他代表的是渐进复杂度,物理意义为算法随问题规模增大时间的增量。比如O(N^3)就表示增量为3次方级别。

数据结构

  1. 平衡二叉树查找复杂度
  2. 二叉树节点间的最大距离 >https://blog.csdn.net/liuyi1207164339/article/details/50898902

数据库

MySQL

  1. 外键删除策略: 一般删除:先删除子表数据,再删除主表。级联删除:不仅删除操作行记录,还删除操作行涉及外键所在的所有记录行。还有办法,采用逻辑删除的方式,标记删除,然后定期进行历史数据归档。

    • 表B通过a列与表A建立关系,则称A为主表,B为从表,a列为外键。删除时,如果要删除A中数据,但B中有关联A的,那外键约束将禁止删除。

    学生表(学号,姓名,性别,班级) 
    其中每个学生的学号是唯一的,学号就是一个主键 
    课程表(课程编号,课程名,学分) 
    其中课程编号是唯一的,课程编号就是一个主键 
    成绩表(学号,课程号,成绩) 
    成绩表中单一一个属性无法唯一标识一条记录,学号和课程号的组合才可以唯一标识一条记录,所以 学号和课程号的属性组是一个主键

    成绩表中的学号不是成绩表的主键,但它和学生表中的学号相对应,并且学生表中的学号是学生表的主键,则称成绩表中的学号是学生表的外键 其中,学生表和课程表都是主表,成绩表为从表

  2. MVVC更详细的讲解。

  3. 建立索引的准则

    • 只对查询,分组,排序用到的字段加索引。
    • 索引应该加在基数比较大的,即该字段的可能性非常多,像性别就不适合作为索引,即区分度要大
    • varchar作为索引要注意取长度,还要注意隐式转换,1默认是数字而不是字符
    • 建立组合索引,有利于减少回表次数,但是最左边要是区分度最高的
    • 最好用自增主键,或者主键具有某种增长特性,否则会频繁页分裂,开销很大
    • 避免重复索引,冗余索引
  4. mysql有哪些索引? ,从物理存储角度而言: 聚簇索引和非聚簇索引。 从数据结构而言: B+树索引,Hash索引,全文索引。 从逻辑角度而言: 主键索引,普通索引,唯一索引,联合索引,(覆盖索引)

  5. SQL语句的执行过程: mysql内部分为server和存储引擎。

    • server用于处理客户端发来的请求,这里并没有采用IO复用,而是使用连接池的方式。Server中先通过连接器获得sql语句,然后查询缓存中是否有结果(要开启缓存),有则返回无则进一步进行分析,将sql语句解析成一个解析树,同时还要验证解析数是否合法,然后交给优化器。优化器对sql语句进行优化,之后交给执行引擎,最后写入数据库/从数据库读出。

    深入理解preparedStatement:结论1.需要手动开启数据库服务器预编译功能,此时JDBC发送预编译语句sql,之后只发送参数。2.PreparedStatement类在填serXxx(1,xxx)时自己做了转义,是java层面的事儿。3.当开启cachePrepStmts时,客户端会以sql语句作为键,预编译完成后的对象PrepareStatement作为值,保存在Map中,以便下次可以重复利用和缓存,这也是java层面的事儿。这里的缓存是指,同样的sql(内有?),交给两个preparedStatement处理时,如果开启缓存只向数据库发送一次预编译命令,否则发送两次。4.第3点的缓存是针对连接的,每个连接有自己的preparedStatement缓存。

    https://blog.csdn.net/marvel__dead/article/details/69486947

    https://blog.csdn.net/alex_xfboy/article/details/83901351

  6. 为什么用B+树而不用其他? 针对AVL和红黑树,他们要解决了1.搜索2.平衡,红黑树相对AVL树有更快速的调节特性,但是数据库查询最关键的是降低IO次数,即降低树高,因此B树是最合适的。但是B树问题是每个节点都有数据,搜索时间不稳定,并且一次IO载入内存的数据相对B+树少,因此采用B+树。因为B+树相邻区域数据有关,因此局部相关性很好。而且B+树所有数据都在叶子上,可以用双链表串联,便于区间搜索。B+树的最大深度为log(m/2)N,最小深度为logmN,m取大一点就可以深度降低。

  7. 事务如何回滚,有什么日志? 核心是redolog和undolog,redo实现持久化,undo实现原子性,当故障恢复时,先通过redo对commmit的事务进行redo(因为持久化在redo中不一定持久化在datafile中),对未commit的事务在redo中进行undo回滚,回滚到哪里呢?就要查undolog。逻辑是其实不用redo,undo也可以实现持久化,比如不用undo和redo,要求每个事务执行都在内存中执行,commit之前必须持久化,那么即是故障,因为内存不会持久化,因此实现了回滚。但是每次都要写入硬盘,而且是随机写入,效率太低,因此必然出现redo,实现异步IO,以物理页为单位进行顺序写入。那如果没有undo呢? 假设只有redo,其他数据改变都在内存中完成,断电自动回滚。我觉得用undo主要还是为了实现MVCC的多版本并发控制,所以没有采取断电的方式,而且断电总觉得有点不稳。MVCC中,每个行记录进行写时复制,同一条行记录会有不同版本,这些版本通过事务唯一ID进行编号,并且通过undolog进行维护,每个版本之间用链表串联起来,指针式rollback_ptr,在MVCC中,insert的记录一定是没有历史纪录的,而upadate/delete的记录有历史版本,因为必须是持有这个记录的所有事务都提交才能删除undolog。redolog必须在刷盘之后才能删除,而undolog在事务提交之后就可以删除了。一个事务修改多条记录会产生多个undo log(undo针对行记录,而redo针对物理页),多个undo log都要写入磁盘效率太低,因此redo把undolog也当做一种数据,写入redo中,在redolog重做恢复的时候也恢复undo log。

计算机网络

TCP/IP

  1. 拥塞控制算法。 1.慢启动:接收到一个ack就++,每经过1个RTT(网络往返时间)就*2。2.拥塞避免:当到达阈值时线性增长3.拥塞控制:一旦发生超时重传就拥塞阈值设定为当前的一半,并开始慢启动,若发生超高速重传事件(TCP Reno版本后),快重传:就将拥塞窗口cwnd设置为当前的一半,拥塞阈值设定为当前cwnd,然后进入快恢复 4快恢复:cwnd拥塞窗口设置为门限值 + 3 * MSS,重传指定数据包,收到ack后退出快恢复,将cwnd设置为门限值,进入线性增加。
  2. OSI有哪几层,各层的协议。 物理层 数据链路层(以太网协议,ARP) 网络层(OSPF,RIP,BGP,ICMP) 传输层(TCP,UDP,QUIC) 会话层(SSL,TSL) 表现层 应用层(HTTP,FTP,SMTP,TELNET,SND)
  3. 服务器出现大量TIME_WAIT是为什么?HTTP关闭连接方是服务器,服务器连接多个客户端因此会产生多个TIME_WAIT,如果网络状况不好导致服务器关闭响应(FIN_WAIT2)状态下发送的ACK消息丢失,客户端会重复FIN,使得产生大量TIME_WAIT。解决方案 开启服务器的TIMEWAIT重用和快速回收,快速回收对NAT有影响,不建议使用。短连接变长连接,减少断开连接的频率。TIMT_WAIT时间修改的短一点。大量TIME_WAIT是小号文件描述符,而不是端口 另一个相关的问题出现大量CLOSE_WAIT,说明是被关闭方在释放资源之后没有close,导致一直在close_wait状态。
  4. TCP连接最多可以有多少个。 1.客户端而言取决于端口号,浏览器发送HTTP请求时会随机选取一个空闲端口号,本机最大端口号为65536,但是前1024个被预留出来,因此只有65536-1024个。2.服务器端的话,没有最大限制,因为一个端口可以建立无数个IP,完全取决于CPU内存之类的硬件。
  5. TCP状态机
  6. 四次挥手中TIME_WAIT的作用1,确认被关闭方已经确实关闭,如果ack丢失,在2MSL时间内,被关闭防一定会重发FIN 2.2MSL的时间可以让本次连接所产生的所有报文段都从网络上消失。
  7. IP\UDP\TCP校验和算法和区别? 校验和算法:1.所有需要校验的数据16bit分成一组,对所有组按位求和,如果进位则加到最低位,将求得的和取反即是校验和。 IP只计算IP首部,TCP计算ip伪首部(源IP地址4,目的IP地址4,协议号2,TCP包长2,共14字节),TCP首部和TCP数据,UDP计算IP伪首部,UDP报头,UDP数据。

    伪首部是一个虚拟的数据结构,实际在物理上并不存在,只是在计算校验和的时候必须用上,以确定发送目的地IP是正确的

HTTP

  1. 如何理解HTTP方法中的patch。 这个方法是非幂等的,和put的区别是允许更改资源的一部分。比如user这个资源,PUT要传入user的所有信息,patch只用传递一部分,那为什么patch是非幂等的呢?因为put规范是指定A改成B,而patch可以指定对uesr的age+1,而patch只能要求吧user的age改为一个数。restful是规范,有利于降低沟通成本,设计优秀的url,但是不强制,当你给前端一个patch接口的时候,你是想告诉前端,这个方法是不幂等的
  2. HTTP请求数据太大怎么办? 1.采用post 2,检查服务器(比如tomcat)是否有大小限制,因为理论上post是大小无限制的

并发

  1. synchronized底层原理:HotSpot虚拟机的对象头包括两部分MarkWord和KlassPointer,前者我们很熟悉用来标识不同的锁,后者指向元空间中的metadata。
    monitor是一个对象,每个java对象通过markword指向一个monitor,关联过程可以使monitor和对象一起创建,也可以在线城市图获取对象锁时生成,但当一个monitor被某个线程持有后,就被锁定了。每个monitor都持有一把锁。

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
    }
    

    (重量锁情况下)ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

    当系统调用monitorenter时,当前线程加入entry_list并试图获取当前对象所关联的monitor的持有权,成功则可充所计数器+1,失败则当前线程阻塞。

    在偏向锁情况下,markword会标记偏向进程,如果相同则不需要执行monitor那边的内容,如果是轻量锁,线程尝试以cas+自旋方式将对象头markword替换成指向lock record(线程存储锁记录的区域)的指针,并将lock record的displace markword变成markword原来的值,owner指针指向对象头。释放的时候,再进行一次交换,如果释放失败就采用重量锁的释放方式,即唤醒所有阻塞中的线程。如果自旋过久,也会膨胀为重量锁。

  2. 锁的开销主要有哪些? 在linux操作系统中,一个线程想要获得锁会先尝试CAS,失败后调用系统sys_futex试图获取锁,因此锁开销为CAS+上下文切换(线程抢锁失败会被调度器挂起,此时进行一次上下文切换+一次用户态到内核态的切换,如果是跨处理器的切换开销就会更大),所以最大的开销是上下文切换。

框架

Spring

  1. 单实例无状态:比如DAO类就是无状态单实例,但是user,goods之类的就是有状态多实例。有无状态具体体现为成员变量的值是否可变。有状态的单例((比如servlet是单例的,其成员变量也是单例的,但是可以有状态,此时需要加锁)需要加锁,防止一个线程对其的更改影响到另一个线程,也可以放在Threadlocal中。

操作系统

  1. 进程间通信考虑资源如何跨进程共享,线程间通信考虑如果保证安全。 进程间通信方式:管道(有名在无情缘关系进程间通信,无名在父子间通信),信号量,消息队列,信号,共享内存,socket。线程间通信方式:信号量,锁,wait/notiy,JUC包里的一些东西。
  2. 孤儿进程: 父进程退出,子进程还在运行,但是被进程号为1的init进程收养,负责其状态维护。僵尸进程:子进程退出,但是父进程没有调用wait或waitpid方法,导致子进程描述符仍然保存在系统中。ps aux|grep Z可以找到僵尸进程,用kill 9杀掉父pid可以让僵尸进程彻底被杀掉。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值