Java面试题总结(持续更新)

本文深入探讨了Java的内存管理,包括堆、栈、方法区等运行时数据区的作用,以及垃圾回收机制和内存分配策略。接着讲解了SpringBoot的HTTP特性,如HTTP状态码、请求响应过程,并讨论了HTTP连接的建立和关闭。此外,还涵盖了JVM的类加载机制,包括双亲委派模型和不同类型的类加载器。最后,简要介绍了操作系统层面的知识,如线程同步和进程通信方式。
摘要由CSDN通过智能技术生成

JavaSE

Java基础

  1. 解释型语言和编译型语言的区别?Java是解释型语言还是编译型语言?

    编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。

    解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束!

    编译型语言,执行速度快、效率高;依靠编译器、跨平台性差些。

    解释型语言,执行速度慢、效率低;依靠解释器、跨平台性好。

    个人认为,java是解释型的语言,因为虽然java也需要编译,编译成.class文件,但是并不是机器可以识别的语言,而是字节码,最终还是需要 jvm的解释,才能在各个平台执行,这同时也是java跨平台的原因。所以可是说java即是编译型的,也是解释型,但是假如非要归类的话,从概念上的定义,恐怕java应该归到解释型的语言中。

  2. JIT是什么

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lcnK2ri4-1646189574501)(D:\Ego\Java\面试\20191015222132356.png)]

    java编译为字节码文件后,如果jvm发现某段代码为“热点代码”,那么就会用JIT直接编译并进行优化。不是热点代码的话jvm就用解释器去解释。

    热点代码有两类:1.被多次调用的方法 2.被多次循环执行的方法体

    jvm识别热点代码需要进行热点探测,探测算法有两种:

    1. 基于采样

      jvm会周期性对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,它就是热点方法。实现简单高效,但容易收到线程阻塞或其他外因干扰

    2. 基于计数器

      为每个方法或代码块建立计数器,超过一定阈值就认为是热点方法。结果严谨,但实现麻烦

    HotSpot采用第二种,并且有两类计数器:

    1. 方法调用计数器

      调用一次次数加一,超过一定时间(半衰周期)没有调用的话,次数会减半,称为“热度衰减”。

    2. 回边计数器

      统计循环体执行的次数。字节码中遇到控制流向后跳转的指令称为回边,遇到回边就加一

  3. 正则表达式和Java?

    正则表达式就是一种字符串的匹配模式,可以将一些复杂的规则用一个表达式来描述。

    Java的String类提供了支持正则表达式操作的方法:matches() replaceAll() replaceFirst() split()

    此外也可以用Pattern类表示正则表达式对象

  4. Java如何跳出多重嵌套循环?

    在最外层循环前加一个标记,比如a: 然后break a即可跳出,但是不建议这样用,会让代码可读性变差

    java也有关键字goto,但是没有用

  5. int和Integer的区别?

    Java是面向对象语言,所以为了能将基本数据类型当对象操作,Java为他们各自提供了包装类型,Integer就是int的包装类型,从Java5开始引入了自动装箱/拆箱,二者可以相互转换

  6. 如何输出一个某种编码的字符串

    String a = "abcde";
    //将a以UTF-8的编码方式获得字节数组,再以GBK的编码方式编码为b
    String b = new String(a.getBytes("UTF-8"), "GBK");
    System.out.println(b);
    
  7. 请你讲讲数组(Array)和列表(ArrayList)的区别?什么时候应该使用Array而不是ArrayList?

    Array声明的时候就要确定长度,而且长度不可变,只能存储同一数据类型,可以存储基本数据类型

    ArrayList是一个集合,长度可变,可以存放不同数据类型,不可以存放基本数据类型(可以存他们的包装类)。

    当能确定数据类型和个数时可以用Array

  8. 什么是自动拆装箱?

    Java八种基本数据类型都有各自对应的包装类,基本类型和对应的包装类在很多场景下可以自动转换,不需要手动去转换。

    实现原理:

    • 自动装箱:基本类型–>包装类 。调用 包装类.valueOf() , 比如Integer.valueOf(1)
    • 自动拆箱:包装类–>基本类型。调用xxxValue,比如有一个Integer对象a,则为a.intValue()

    哪些地方自动拆装箱:

    1. 向集合里存基本数据类型,会自动装箱
    2. 包装类型和基本类型比较大小时,包装类型会自动拆箱
    3. 包装类型四则运算,会自动拆箱
    4. 三目运算会自动拆箱,所以如果值为null,会报空指针异常
    5. 函数参数与返回值
  9. 为什么会出现4.0-3.6=0.40000001?

    计算机计算十进制时要先转换成二进制,而二进制无法精确表示十进制小数,所以会出现误差

    可以用BigDecimal来解决这个问题。

  10. 十进制的数在内存中是怎么存的?

补码的形式

  1. Java8新特性?

    1. 对HashMap的数据结构进行优化

      HashMap1.8以前是数组+链表,1.8以后是数组+链表/红黑树

    2. Lambda表达式,本质是一段匿名内部类

    3. 函数时接口,给Lambda提供更好的支持

    4. Stream API:创建Stream,中间操作,终止操作

    5. Optional类:用来解决空指针异常

    6. 接口中可以定义m默认实现方法和静态方法

    7. 日期API

  2. Object若不重写hashCode()的话,hashCode()如何计算出来的?

    Object的hashCode是本地方法,用c/c++实现的,直接返回该对象的内存地址

  3. 请你解释为什么重写equals还要重写hashcode?

    1. 提升效率

      向HashMap中put时,首先会计算对象的哈希码,然后看看对应的位置上有没有元素,如果没有的话就可以直接插入了,就不需要从头到尾和每个元素都进行equals判断了。

    2. 还有为了保证HashMap和HashSet中的去重性。因为equals相同的对象,hashcode必须相同,如果只重写equals的话,两个属性相同的对象按照规则hash值也一样,但是没有hashcode的话会调用Object默认的的hashcode,这样值就不相同了。比如两个相同的属性的对象,hashcode值不同,HashMap认为是两个不同的对象,都会存进去,但是我们想要的结果是只存一个,这就违反了HashMap的唯一性了。

关键字

  1. 请你谈谈关于Synchronized和lock

    synchronizedLock
    Java关键字一个接口
    线程执行完或发生异常会释放锁,不会出现死锁需要在finally中手动释放锁,不然容易造成死锁
    不可以中断等待锁的线程可以中断等待锁的线程
    不可以判断锁的状态可以判断锁的状态
    大量线程竞争时性能低大量线程竞争时性能高
  2. 请你介绍一下volatile?

    volatile可以用来保证可见性和有序性。

    先说下内存模型:计算机执行程序时,每条指令都是在CPU上执行,那就涉及到了数据的读写。CPU的运行速度很快,而向主存读写数据的速度很慢,所以就在两者之间加了高速缓存。先把主存的数据复制一份到高速缓存中,然后CPU就可以从高速缓存中进行读写,最后高速缓存再将值刷新到主存中。那么多线程就会出现问题,比如主存中有一个数为0,现在有两个线程想对它加一,那么结果应该为2。但是可能第一个线程计算为1之后还没来得及写入主存,第二个线程就进行运算,也就是读取的也是0,那么最后结果为1。所以就提供了两种办法:

    1. 在总线上加LOCK#锁,那么对于某个变量只能有一个线程访问,但是效率低下。
    2. 缓存一致性协议。当一个CPU写数据时,如果其他CPU中也有这个变量的副本,会让其他CPU重新读取。

    这就是可见性问题。

    有序性问题就是说,JVM在执行时会对程序执行顺序进行优化,比如int a = 1, int b = 2。在实际执行时可能会将他们的顺序交换,但不会造成影响,因为这两个变量没有依赖关系,这就是指令重排序。单线程下是不会造成问题,但是多线程就可能出现问题。比如第一个线程先定义了某个变量,然后定义了一个标志位为true,第二个线程中假设标志位为true的话就使用第一个变量,那么重排序后,第一个线程可能设置标志位为true提前了,第二个线程认为可以使用了,但其实变量还没定义,那么就会出错。Java内存模型具备一定的有序性,即happens-before原则

    在Java中,Java模型为了获得更好的性能,允许处理器使用高速缓存,也允许编译器进行指令重排序,所以也会出现这两个问题。

    那么volatile修饰的变量有两个作用:

    1. 某个线程修改了这个变量后,其他线程立即可见。
    2. 禁止指令重排序。

    但是volatile不可以保证原子性,比如要对a = 0 自增,开启两个线程,每个线程循环100次a++,那么结果可能是小于200的。因为a++不是原子操作,它分为三个步骤,先读取a,再加一,再写会,那么第一个线程第一次加的时候,可能刚读取完,然后被阻塞了,第二个线程再读的时候还是原来的值,加完之后写了回去,这时第一个线程阻塞完毕继续再原来的基础上加1,然后写回,那么两次操作其实只加了1。

    volatile底层原理是,在生成汇编代码时会多出一个lock前缀指令,这个指令相当于一个内存屏障,它提供了3个功能:

    1. 保证重排序时,不会把它后面的指令排序到它前面,也不会把前面的指令排到后面
    2. 强制对缓存的修改操作立即写入主存
    3. 如果是写操作,其他CPU对应的缓存行无效

面向对象

  1. 重载和重写的区别?

    两个都是实现多态的方式。重载是编译时多态,重写是运行时多态。重载发生在同一个类中,要求方法名一样,参数列表不一样,对返回值没有要求;重写发生在子类与父类中,子类重写父类的方法要求方法名,参数列表一样,返回值一样或者为父类返回值的字类,访问修饰符不能小于父类,不能抛出新的异常或更宽泛的异常

  2. 面向对象的六原则一法则?

    1. 单一职责原则:一个类只做它该干的事情,也就是实现高内聚
    2. 开闭原则:一个软件实体应该对扩展开放,对修改闭合。这样增加新功能时只用派生一些新类,而不用修改原来的代码
    3. 依赖倒转原则:尽可能使用抽象类型而不是具体类型
    4. 里氏替换原则:任何时候都可以用子类替换掉父类。如果这样做出现了问题那继承一定是错误的
    5. 接口隔离原则:接口要小而专,不能大而全
    6. 合成聚合服用原则:优先使用合成和聚合关系复用代码
    7. 迪米特法则:一个对象尽可能对其他对象了解的少,也就是做到低耦合
  3. 在try块中可以抛出异常吗?

    可以,比如IO流读取File的时候,外层try catch包含创建流的代码,内层try catch来操作流,这样如果流创建失败直接抛异常,就不用关闭流了。

  4. 抽象类和接口的区别?

    • 语法上:

      1. 抽象类可以有构造方法,接口不能有
      2. 抽象类可以包含非抽象的普通方法,接口不可以
      3. 抽象类可以有成员变量,接口不可以
      4. 一个类可以实现多个接口,但只能继承一个抽象类
    • 应用上:

      接口是横向的,抽象类是纵向的,接口约定了一个共同的行为,而抽象类是把一些子类的共性抽取出来,可以帮他们完成一部分方法的实现。所以需要横向扩展就用接口,纵向扩展就用抽象类。

      举个例子,比如某个项目的所有Servlet都需要进行权限判断,记录日志、异常等操作,就可以定义一个抽象类,定义一个抽象方法,里面写具体的业务逻辑,然后定义一个非抽象的方法,去完成权限判断,记录日志的操作,然后去调用这个抽象方法,那么子类去继承他的时候只需要重写业务逻辑的抽象方法就可以了,别的操作就自动帮他完成了。

  5. 请说明一下final, finally, finalize的区别?

    1. final:

      修饰属性表示这个属性不可变,是一个常量。

      修饰方法表示这个方法不可以被重写。

      修饰类表示这个类不可以被继承。

    2. finally:

      用于异常处理,无论是否抛出异常,finally中的代码一定会执行,所以一般用于资源的关闭。

    3. finalize:

      是Object类中的一个方法,当垃圾收集器回收时,被回收的对象会调用此方法,以供其他资源回收

  6. 请说明面向对象的特征有哪些方面?

    1. 封装

      就是要做到高内聚低耦合,把一个对象的属性和行为都封装到一个类中,把成员变量定义为私有。

    2. 继承

      就是把父类的属性和方法继承过来,然后添加一些自己需要的新的东西,做到了可重用性和扩展性

    3. 多态

      就是父类引用指向子类对象。分为编译时多态和运行时多态,重载实现了编译时多态,重写实现了运行时多态

  7. 请说明Comparable和Comparator接口的作用以及它们的区别?

    两个接口都是来定义排序规则的。

    • Comparable:

      相当于内部比较器,比如对集合进行排序一般会用到Collections.sort()方法,但是集合中的这个类必须实现Comparable接口,并重写他的compareTo方法,才可以直接用Collections进行排序

    • Comparator:

      相当于外部比较器,如果一个类我们没办法对他进行扩展,也就是无法继承Comparable,那就可以使用Comparator,在用Collections.sort()方法时,里面传入集合以及一个匿名内部类,这个内部类去实现Comparator接口,并重写compare()方法。比如String类型中,默认的比较规则是按照字典序排序,如果我们想忽略大小写进行排序的话,就可以使用Comparator

  8. 请你讲讲什么是泛型?

    泛型就是参数化类型,就是将操作的数据类型指定为一个参数,可以在类、接口、方法中使用。

    泛型提高了代码的重用率,编译时可以检查类型安全,消除了强制转换,减少了出错的机会。

  9. 请解释一下extends 和super 泛型限定符?

    extends用来定义上界,super定义下界。

    • 上界的list只能get,不能add。

      因为extends表示它和它的子类,但是具体add哪一个子类是不确定的,所以干脆就不让它add

      但是get的话,无论是哪一个子类,都可以向上转型成上界

    • 下界的list只能add,不能get。

      super表示它和它的父类,父类那么多不知道add哪个,所以只能add他和他的子类,虽然不知道add哪个,但是都可以向上转型成下界。

      而get的话,那么多父类是不能向下转型的,除非用Object来接收。

    所以如果想取数据就使用extends,想存数据就用super

  10. 请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?

static表明让成员变量或成员方法所属于一个类,而不是对象,被static修饰的成员变量和方法可以直接通过类名.的方式被访问,独立于对象。

static还可以当作静态代码块,在类第一次被加载的时候执行,只会被执行一次,一般用作初始化,提高性能。

Java不可以覆盖private或static修饰的方法。private只用本类能访问到,子类是访问不到的,更别说覆盖了。

而static修饰的静态方法,跟任何实例无关,在编译时就绑定了,但覆盖是在运行时动态绑定,所以概念上不适用。

  1. 请列举你所知道的Object类的方法并简要说明

    1. hashCode():本地方法,用来获取对象的哈希值,用来确定对象的存储位置
    2. equals():用于确定两个对象是否相同
    3. clone():用来创建并返回当前对象的一个拷贝
    4. toString():返回对象的字符串表示形式
    5. getClass():本地方法,返回运行时的Class对象,被final修饰,不能被重写
    6. notify():本地方法,final修饰不能被重写,唤醒一个在此对象监视器上等待的线程
    7. notifyAll():同notify(),只是唤醒的是所有线程
    8. 三个wait():本地方法,final修饰不能被重写,用来暂停线程的执行。没参数的会一直等待下去,一个参数的传入等待时间,两个参数的传入等待时间和额外时间
    9. finalize():垃圾收集器回收时对象调用此方法进行资源的回收
  2. 类和对象的区别?

    类是一个抽象的概念,是对具有共同特征的实体的集合。

    对象是类的实例,是真实的个体,和真实世界一一对应的。

  3. String为什么不可变?为什么要这样设计?

    因为String的本质是char数组,而char数组是被final修饰的,所以不可以变。

    为什么要这样设计:

    1. 字符串常量池的需要

      Java堆内存中有一块区域是字符串常量池。当创建一个String对象时,如果常量池已经有这个字符串了,那就不会再创建,而是直接将引用指向它。如果有好几个引用都指向了一个常量池中的对象,这个时候如果有一个引用想要改变它的话,那所有的引用指向的就都被改变了,显然不合理。

    2. 效率

      String中有hash来保存它的哈希码,String设置为不可变的话,每次使用的时候就不需要重复计算哈希码,提高了效率

    3. 安全

      Java很多类都使用了String,比如网络连接,反射等等,如果String是可变的话h会引起安全隐患

集合

  1. List、Map、Set三个接口,存取元素时,各有什么特点?

    List和Set都是单列集合,Map是双列集合。

    List有先后顺序,Set和Map是无序的。

    • List:

      存的时候调用add()方法。

      取的时候用get()或者用Iterator接口遍历

    • Set:

      不能有重复的元素。存的时候调用add()方法,返回值为boolean,如果为true说明集合中没有这个元素,成功存进去,如果为false说明已经存在了。

      取的时候用Iterator接口遍历

    • Map:

      以键值对形式存储,不能有重复的键。存的时候调用put()方法

      取的时候可以调用get(),根据key来找相应的value;也可以通过keySet()获取所有key的集合;也可以通过values()获取所有value的集合;也可以通过entrySet()获取所有键值对的集合

    Set和Map都有哈希存储的版本和排序树存储的版本,哈希存储版本存取非常高效,而排序树版本可以自定义排序规则

  2. ArrayList、LinkedList、Vector的区别和实现原理

    • 存储结构

      ArrayList和Vector基于数组实现,LinkedList基于双向链表实现。

    • 线程安全

      ArrayList和LinkedList不是线程安全的,可以用Collections中的静态方法synchronizedList()把他们变成线程安全的。Vector是线程安全的,大部分方法都包含synchronized,所以效率比较低,已经被遗弃了。

    • 扩容机制

      ArrayList如果元素个数超过数组长度,会产生一个新的数组,容量是原来的1.5倍,然后将原来的数据复制过来,再加上新的数据。

    • 效率

      ArrayList和Vector查询效率是O(1),平均增删的效率是O(n)

      LinkedList平均查询效率是O(n),增删效率是O(1)

  3. 请判断List、Set、Map是否继承自Collection接口?

    List和Set继承自Collection,Map不是

  4. 请讲讲你所知道的常用集合类以及主要方法

    List、Set、Map

    List:具体实现有ArrayList和LinkedList,常用方法有get(),add(),remove(),contains(),size()

    Set:具体实现有HashSet和TreeSet,常用方法有add(),remove(),contains(), size()

    Map:具体实现有HashMap和TreeMap,常用方法有get(),put(),remove(),containsKey(),containsValue(),keySet(),values(),entrySet()

  5. Collection和Collections的区别?

    Collection是集合类的父接口,实现有List和Set

    Collections是集合类的一个帮助类,提供了一系列静态方法来操作集合类,主要方法有:

    1. sort:排序方法,默认是升序,也可以实现Comparator接口来自定义排序规则
    2. reverse:翻转集合中元素的顺序
    3. shuffle:对集合随机排序
    4. copy:将第二个参数集合中的元素复制到第一个参数集合中
    5. synchronizedList/synchronizedSet/synchronizedMap:将集合变为线程安全
  6. 请说说快速失败(fail-fast)和安全失败(fail-safe)的区别?

    快速失败:当用迭代器遍历一个集合时,如果这个集合被修改,那么就会抛出异常,所以不能在多线程下并发修改。原理:迭代时会用一个modeCount变量,如果集合遍历时被修改,那么modeCount的值就会改变,迭代器每次调用hasNext和next前都会判断modeCount和expectedmodeCount是否一样,如果不一样就会抛出异常。

    安全失败:迭代器遍历集合时,集合被修改也不会抛出异常。原理:迭代前会对原有集合进行一次拷贝,迭代器对拷贝的集合进行迭代,所以就算原有集合被修改,拷贝的集合也没有被修改,也就不会抛出异常

  7. 请你说说Iterator和ListIterator的区别

    两者都是迭代器。

    Iterator:可以应用于List和Set,只能向后遍历,而且只能获取和删除。

    ListIterator:

    只能应用于List,是对Iterator的一个增强。

    不仅可以向后遍历,也可以向前遍历,即多了hasPrevious()和previous()。

    可以获取当前索引的位置,即nextIndex()和previousIndex()。

    可以对当前对象进行修改,即set()

    可以插入对象,即add()

  8. 请你说明一下ConcurrentHashMap的原理?

    1.7:使用了分段锁Segment,Segment就类似于HashMap,也就是相当于一个二级哈希表。在put时,先根据key找到对应的Segment,然后在对应的Segment里面尝试获取锁,获取到锁之后就进行插入,和HashMap类似,最后再释放锁。在get时就很简单,因为它的value被volatile修饰了,所以保证了可见性,这样就不需要加锁了,效率就得到了提高。

    1.8:数据结构和1.8的HashMap类似,不使用Segment,而是采用了CAS和synchronized来实现并发安全的。put时,先尝试用CAS写入,如果失败就通过自旋保证成功。如果hashcode==-1就需要进行扩容。如果都不满足的话,就用synchronized进行写入。最后判断是否要转成红黑树。

  9. 请说明ArrayList是否会越界?

    会发生。因为ArrayList是线程不安全的,所以多线程情况下可能会发生越界。

  10. HashMap的容量为什么是2的n次幂?

为了散列更均匀,减小哈希碰撞。因为在计算key对应的下标时,计算方法是(n-1)&hash值,这其实就相当于取模的位运算形式,如果容量是2的n次幂的话,减去1之后就是低位全是1的形式,这样和hash进行与运算时会大大减小hash冲突。

  1. 如果一直在list的尾部添加元素,用哪种方式的效率高?

    在尾部添加元素的时间复杂度都是O(1),但是ArrayList需要扩容,而LinkedList需要new结点,所以在千万级别一下的时候,扩容的优势还不明显,LinkedList效率更高,但在千万级别以上ArrayList效率更高

  2. 请你解释一下hashMap具体如何实现的?

    jdk1.7以前是通过数组+链表实现的,jdk1.8改为了数组+链表/红黑树。使用时,先初始化HashMap,可以指定容量大小n,它会自动调用tabSizefor()函数来得到第一个大于等于n且为2的整数次幂的数,如果不指定容量的话,就默认为16。然后插入时,会先根据key计算得到hash值,然后通过hash值得到索引。拿到索引后先判断那个位置上有没有结点,如果没有的话直接插入,如果有的话,判断和头节点的key是否相等,如果相等的话直接覆盖,如果不相等的话,如果头节点是红黑树结点,就按照红黑树结点的查找方式遍历,如果是链表结点的话,按链表结点方式遍历,如果在遍历中找到key相同的结点就覆盖,如果没有的话,就插入到尾部,链表的话插入完判断结点是否超过8个而且数组长度超过64,如果超过的话就转为红黑树。最后判断是否需要扩容。

  3. HashMap扩容机制

    首先判断老表的容量是否超过上限,如果超过上限的话,将扩容阈值修改为Integer最大值,如果没超过的话,将容量和阈值都扩大为2倍,并指向新数组,然后遍历老数组,如果当前索引只有一个结点,则重新计算索引位置然后放到新数组;如果当前索引不止一个结点,则计算e.hash&oldCap,如果为0,则直接放到原索引位置,如果为1,则放到原索引+oldCap的位置。因为计算索引时,用的是(n-1)&hash,那么扩容为2倍之后,n-1和原来相比,就是在高位多出来一个1,低位还是一样的,所以只用判断这一位即可

多线程

  1. 如何实现线程安全?

    线程安全主要体现在原子性、可见性、有序性

    实现原子性:使用原子类、synchronized、lock

    实现可见性:volatile、synchronized、lock

    实现有序性:volatile、happens-before原则

  2. 线程的基本状态以及状态之间的关系?

    基本状态:新建、就绪、运行、阻塞、死亡

    1. 当线程被new出来后处于新建状态
    2. 线程调用start处于就绪状态
    3. 就绪的线程得到CPU的执行权后就进入运行状态
    4. 当运行态的线程因为某些原因放弃CPU,比如调用wait(),sleep(),就进入阻塞态。
    5. 休眠结束或被唤醒之后重新进入就绪态
    6. 执行完run方法或遇到了未捕获的异常就会进入死亡态
  3. 什么是守护线程?

    线程分为用户线程和守护线程,守护线程就是在后台提供一种通用服务的线程,比如垃圾回收器。当所有用户线程结束后,守护线程就会结束,因为没有存在的意义了。可以用过thread.setDaemon(true)来设置一个线程为守护线程,必须在start之前设置。守护线程中产生的新线程也是守护线程。守护线程中不可以有读写操作和计算逻辑,因为守护线程随时都有可能停止。

  4. 什么是线程池?

    就是用来管理线程,创建销毁线程的一个容器,可以避免频繁创建销毁线程而带来的资源消耗,使用线程池可以提高效率,方便管理。

  5. 几种常见的线程池?

    1、newSingleThreadExecutor

    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    2、newFixedThreadPool

    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    3、newCachedThreadPool

    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    4、newScheduledThreadPool

    创建一个定长线程池,支持定时及周期性任务执行。

  6. 线程池常用参数?

    corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
    maximumPoolSize:线程池允许的最大线程数量
    keepAliveTime:非核心线程的最大存活时间
    unit:存活时间的单位
    workQueue:工作队列,保存等待执行的任务
    threadFactory:创建线程的工厂类
    handler:拒绝策略

  7. 拒绝策略?

    当工作队列满了,线程池中的线程也达到了最大数量,就会拒绝到来的任务,拒绝策略有四种:

    AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。

    DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。

    CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。

    DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

    默认是AbortPolicy。

  8. 线程池的状态和状态之间的关系?

    5种状态: running、shutdown、stop、tidying、terminated

    1. 线程池被创建后处于running,可以接收任务和处理任务
    2. running时调用shutdown()方法进入shutdown,不接受新任务但可以处理已排队的任务
    3. 调用shutdownNow()方法进入stop,不接受新任务也不能处理已排队的任务,并且中断正在处理的任务
    4. shutdown下,任务队列为空且没有执行中的任务,会转为tidying;stop中执行任务为空也会转为tidying
    5. tidying执行完terminated()会转为terminated
  9. 如何停止线程池?

    1. shutdown()
    2. shutdownNow()
    3. shutdown() + awaitTermination()
  10. Java锁?

并发编程中,为了避免共享数据产生数据不一致的问题,使用锁来锁定代码块、方法。

锁分为:悲观锁、乐观锁、公平锁、非公平锁、独占锁、共享锁、自旋锁、可重入锁、偏向锁/轻量级锁/重量级锁

  1. 什么是死锁?原因和必要条件?怎么解决?

    死锁就是多个进程因为争夺资源而陷入的一种僵局,如果无外力作用就会一直保持下去。

    原因:竞争资源和推进顺序非法

    必要条件:

    1. 互斥条件:也就是竞争共享资源
    2. 请求和保持条件:进程因请求资源而阻塞时,不会释放已有的资源
    3. 不剥夺条件:进程已有的资源使用完之前不可以被剥夺,只能使用完自己释放
    4. 环路等待条件:死锁发生时,必然有一个进程资源环型链

    怎么解决:

    1. 预防死锁:

      破坏4个必要条件之一即可:

      1. 第一个条件不能破坏,因为加锁本来就是为了保证互斥
      2. 请求和保持:一次性分配资源
      3. 不可剥夺:如果获得一部分资源,别的资源获取不到时,释放自己已有的资源
      4. 环路等待:按顺序申请资源
    2. 避免死锁:银行家算法

    3. 检测死锁

    4. 解除死锁

  2. 什么是活锁和饥饿?

    活锁跟死锁相反,死锁是因为争夺资源而陷入僵局,活锁是都可以获取到资源,但是都相互谦让陷入的一种僵局,活锁有可能自行解开。比如让线程休眠一段时间,让别的线程先获取到资源。

    饥饿是一个线程一直获取不到资源,资源一直被别的线程获取,导致他可能永远等待。可能的原因是这个线程的优先级低,所以一直被高优先级的线程争夺到资源。

  3. 无锁技术

    1. 原子类
    2. ThreadLocal
    3. copy-on-write
    4. concurrent开头的并发工具类
  4. 什么是happens-before?

    JVM会对代码进行优化,进行指令重排序,happens-before就保证了重排序后依然可以正确执行代码。他的原则是如果一个操作happens-before另一个操作,那么第一个操作的结果对第二天操作可见。

    有6条规则:

    1. 先写的代码先执行
    2. 释放锁先于获取锁
    3. volatile修饰的变量,写操作先于读操作
    4. 线程的start()先于线程的其他操作
    5. 传递规则
    6. 被调用join()的线程的操作先于join()的返回
  5. sleep()和wait()的区别?

    sleep是Thread类的静态方法,wait是Object类的成员方法

    sleep可以在任何地方使用,wait只能在同步代码块或同步方法中使用

    sleep释放CPU资源,但不释放锁,休眠时间到后继续执行,wait释放锁,进入等待队列,被唤醒后才有机会获取锁

  6. notify和notifyAll的区别?

    一个线程中调用了一个对象的wait方法,该线程就会释放该对象的锁,进入该对象的等待池,等待池中的线程不会去竞争该对象的资源。一个对象还有一个锁池,只有在锁池中的线程才会去竞争该对象的锁。notify就是随机唤醒等待池中的一个线程进入锁池,notifyAll将等待池中所有的线程唤醒进入锁池

  7. 线程池中submit()和execute()方法有什么区别?

    submit参数为runnable或callable,execute参数为runnable

    submit有返回值,execute没有返回值

  8. ThreadLocal有什么作用?有哪些使用场景?

    ThreadLocal是本地线程存储,为每个线程创建一个该变量的副本,可以做到数据间的隔离。

    JDBC连接connection就使用到了,为每个线程创建一个自己的连接,这样就保证了他们是在各自的连接上进行数据库操作,不会出现A线程关了连接,而B线程还在用的情况

  9. 如何保证多个线程同时启动?

    使用countDownLatch。run方法中调用countDownLatch的await(),就会将线程阻塞在此处。然后调用start。

    在需要同时启动的地方调用countDownLatch的countDown()

  10. 说说对于sychronized同步锁的理解

    每个java对象都有一个内置锁,当线程运行到非静态的synchronized方法上时,就会自动获取该实例对象的锁,此时别的线程无法获得它的锁,当方法执行完毕后,会释放锁。

网络编程

  1. BIO、NIO、AIO有什么区别?

    BIO:同步阻塞,线程发起IO操作,不管内核是否准备好IO操作,线程都会一直阻塞直到完成。适用于连接数少的架构。

    NIO:同步非阻塞,发送的请求会注册到多路复用器上,多路复用器轮询到连接有IO请求时才会开启一个线程。适用于连接数多且连接比较短的架构。

    AIO:异步非阻塞,线程发起IO请求立即返回,内核做完IO操作后才会通知线程。适用于连接数多且连接比较长的架构。

  2. 如何读取文件a.txt中第10个字节?

    FileInputStream fis = new FileInputStream("a.txt");
    fis.skip(9);
    int b = fis.read();
    
  3. 节点流和处理流区别?

    节点流:可以从某个节点读数据或写数据的流

    处理流:对已有的流进行封装,提供更丰富的处理,构造方法必须是其他流的对象,比如BufferedReader

  4. 缓冲流的优缺点?

    不带缓存的流读一个字节或字符就写一个字节或字符,缓冲流读取到字节或字符先放入缓冲区,当缓冲区满了再一次性写出去。

    优点:减少了写的次数,提高效率

    缺点:接受端无法即时接收到数据

Spring

  1. spring的好处

    1. 方便解耦和开发,将对象的创建和依赖关系交给spring来处理
    2. 支持AOP编程,可以方便的实现权限拦截和监控等功能
    3. 声明式事务,通过配置就可以完成对事务的支持
    4. 方便集成别的框架
  2. 什么是IOC?

    IOC就是控制反转,是一种设计思想,把对象的创建交给spring来处理,可以解耦合,提高程序的复用性

  3. 什么是AOP?

    AOP就是面向切面编程,可以将一些与业务无关,但是被业务共同调用的逻辑封装起来,增加代码的复用性,降低模块间的耦合

  4. spring注入方式

    接口注入,setter注入,构造器注入

  5. spring的bean是线程安全吗?

    spring不能保证bean是线程安全的,因为默认bean是单例的,这样就可能存在竞争,就会造成线程不安全。

    可以用ThreadLocal或锁来解决这个问题。

  6. spring自动装配bean有哪些?

    XML方式:

    1. no:不进行自动装配
    2. byName:根据名字自动装配
    3. byType:根据类型自动装配
    4. constructor:根据构造器自动装配

    注解方式:

    1. @Autowired:通过类型自动装配,想要通过名字装配可以再加一个@Qualify注解来指定名称
    2. @Resource:先通过名称装配,找不到就通过类型装配
  7. Bean的生命周期

    分为4个阶段:实例化,属性赋值,初始化,销毁。

    实例化的时候,可以通过InstantiationAwareBeanPostProcessor接口来进行扩展,可以在实例化之前调用它的方法来替换原本的bean作为代理,这也是AOP能实现的关键点。可以在实例化之后,属性赋值之前调用它的方法来阻断属性填充。

    初始化阶段,可以调用BeanPostProcessor接口,一系列Aware接口,InitializingBean接口,来进行扩展。BeanPostProcessor接口作用在初始化的前后,Aware接口可以拿到Spring的一些资源,比如BeanName,BeanFactory,ApplicationContext,InitializingBean接口可以自定义一些初始化操作。初始化时调用init-method指定的方法。

    销毁阶段,可以通过DisposableBean接口来进行扩展,最后调用destroy-method指定的方法进行销毁

  8. BeanFactory 和 FactoryBean 的区别

    BeanFactory 是一个工厂类,用来管理Bean的,最核心的功能就是加载Bean,也就是getBean()方法。

    FactoryBean 是一个特殊的Bean,实现该接口的类可以实现它的getObject方法来自定义创建Bean实例

  9. BeanFactory 和 ApplicationContext 的区别

    BeanFactory 是一个基础的IOC容器,提供了完整的IOC功能。

    ApplicationContext是BeanFactory的一个子接口,是一个高级的IOC容器,提供了更多的功能。

    二者加载Bean的时机也不同。BeanFactory 是延迟加载,调用getBean()时才加载,而ApplicationContext在启动后就预加载所有单实例的Bean,到时候可以直接拿来使用

  10. Spring 的 AOP 有哪几种创建代理的方式?

有JDK动态代理和Cglib代理。

JDK动态代理只能代理实现了接口的类,因为通过JDK动态代理生成的类已经实现了Proxy类,所以不能继承别的类了。

而Cglib代理没有这个限制,但是他不可以代理被final修饰的类或方法,因为他的本质是通过继承实现的

  1. Spring是如何解决的循环依赖?

    Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。

  2. 为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?

    如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

  3. Spring 的事务传播行为有哪些?

    1. REQUIRED:Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。
    2. REQUIRES_NEW:每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。
    3. SUPPORTS:如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。
    4. MANDATORY:上下文中必须要存在事务,否则就会抛出异常。
    5. NOT_SUPPORTED :如果上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
    6. NEVER:上下文中不能存在事务,否则就会抛出异常。
    7. NESTED:嵌套事务。如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

SpringMVC

  1. 什么是SpringMVC ?简单介绍下你对springMVC的理解?

    SpringMVC 是Spring框架的一部分,把model,view,controller层分离,把复杂的web应用分成逻辑清晰的几部分,可以做到解耦,简化开发,方便开发人员之间的配合

  2. SpringMVC的流程?

    1. 用户发送请求给DispatcherServlet
    2. DispatcherServlet调用HandlerMapping,请求获取Handler
    3. HandlerMapping根据url找到对应handler,返回给DispatcherServlet
    4. DispatcherServlet调用HandlerAdapter,请求执行Handler
    5. HandlerAdapter执行Handler,Handler执行完将ModelAndView返回给HandlerAdapter
    6. HandlerAdapter将ModelAndView返回给DispatcherServlet
    7. DispatcherServlet将ModelAndView传给ViewResolver进行视图解析
    8. ViewResolver解析完将View返回给DispatcherServlet
    9. DispatcherServlet对View渲染,返回给用户
  3. 如何解决POST请求中文乱码问题,GET的又如何处理呢?

    POST:在web.xml里配置CharacterEncodingFilter 过滤器

    GET:两种方法

    1. 可以将tomcat的编码修改,与工程保持一致
    2. 可以对参数进行重新编码
  4. @RequestMapping的作用是什么?

    用来标识HTTP请求地址与Controller之间的映射关系。

    可以加在类上,也可以加在方法上,完整的路径是类上的@RequestMapping的value加上方法上的RequestMapping的value

  5. SpringMvc的控制器是不是单例模式?如果是,有什么问题?怎么解决?

    是,在多线程访问时会出现线程不安全的问题。

    解决方法:控制器里对可变状态量使用ThreadLocal,为每个线程生成一个副本,互不影响

SpringBoot

  1. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

    启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

    @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

    @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

    @ComponentScan:Spring组件扫描。

  2. SpringBoot自动装配原理

    Springboot通过启动类的@SpringBootApplication注解来实现自动装配。该注解是一个组合注解。包含三个注解

    1. @SpringBootConfiguration:用来进行spring的配置,该注解有@Configuration注解,@Configuration注解有@Component,所以说明他也是spring的一个组件
    2. @EnableAutoConfiguration:用来开启自动配置的注解。该注解主要由@AutoConfigurationPackage和@Import注解组成。@AutoConfigurationPackage注解也使用@Import注解,里面传入了Registrar.class,这个类里面有一个方法可以获得扫描包的路径。@Import注解中传入了一个组件选择器,里面有一个方法可以将需要导入的组件的全类名返回,这些组件就会被添加到容器中
    3. @ComponentScan:用类扫描spring组件的注解

Redis

  1. Redis支持哪些数据类型?

    • string:字符串,最简单的数据结构,一般用在计数场景,比如访客量,点赞量等
    • hash:哈希,类似于hashmap,适合用于存储对象
    • list:链表,是一种双向链表,可以用在消息队列
    • set:集合,类似hashset,适合用于存储不能重复的数据
    • sorted set:有序集合,相比set增加了一个权重参数score,根据score进行排序,可以用在直播间的礼物排行榜
  2. redis优缺点

    • 优点:
      1. 性能极高,每秒可以进行十万次读写
      2. 支持数据的持久化,对数据的更新采用copy-on-write,可以异步的保存在磁盘上
      3. 支持丰富的数据类型
      4. 原子性
      5. 丰富的特性,比如可以设置过期时间
      6. 支持数据的备份,快速的主从复制
      7. 节点集群,很容易将数据分布到多个Redis实例中
    • 缺点:数据库受物理内存的限制,不能用作海量数据的高性能读写
  3. Redis是单线程还是多线程?

    redis4.0以前是完全单线程。

    4.0时,引入了多线程,但额外的线程只用于后台处理,比如删除对象。而核心流程还是单线程,比如接受命令,解析命令,执行命令,返回结果等

    6.0时,多线程主要用于网络IO,也就是接收命令和返回结果,而执行命令还是单线程,所以不需要考虑并发问题

  4. redis为什么是单线程?

    在redis6.0以前,redis核心线程是单线程。因为redis是基于内存的,所以redis的瓶颈不是CPU,而是内存的大小,所以如果使用多线程的话会变得复杂,而且上下文切换和加锁会带来额外的性能消耗。

  5. 为什么redis是单线程还是很快?

    1. 基于内存操作,内存的读写速度非常快
    2. 单线程就不需要上下文切换和加锁,节省了这部分的性能消耗
    3. 使用了IO多路复用机制
    4. 对数据结构进行了优化
  6. redis使用场景

    1. 缓存
    2. 分布式锁
    3. 消息队列
    4. 访客统计
    5. 排行榜
  7. 设置过期时间有什么用?

    1. 为了防止出现OOM,因为内存是有限的
    2. 满足业务场景的需要,比如验证码可能只在1分钟有效,再比如登陆某个APP,几天内免登录
  8. 如何判断是否过期?

    用到了过期字典,就类似于哈希表,key指向的是redis中的某个key,value是long long类型的整数,保存了过期时间

  9. 过期数据的删除策略?

    1. 惰性删除:只会在取出key的时候进行删除,对CPU比较友好,但是容易造成太多过期的key没有被删除
    2. 定期删除:定期抽取一批key进行过期删除操作,对内存比较友好

    redis两种都采用了。

  10. redis内存淘汰机制(MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据)?

有6种淘汰机制:

  1. 从已设置过期时间的数据集里面挑选最近最久未使用(LRU)的进行淘汰
  2. 从已设置过期时间的数据集里面挑选即将过期进行淘汰
  3. 从已设置过期时间的数据集里面任意选择数据进行淘汰
  4. 当内存不足时,挑选最久未使用(LRU)的key进行淘汰
  5. 任意选择数据进行淘汰
  6. 内存不足时,写入数据直接报错,不进行淘汰

4.0之后又新增了两种

  1. 从已设置过期时间的数据集里面挑选最少使用(LFU)的进行淘汰

  2. 当内存不足时,挑选最少使用(LFU)的key进行淘汰

  3. redis持久化机制?

    1. 快照持久化RDB

      通过创建快照获得某个时间点上的数据的副本,然后对快照进行备份,可以复制到其他服务器上,也可以留在原地以便重启服务器的时候使用

    2. 只追加文件AOF

      实时性更好,默认是不开启的。开启后,每执行一条更改数据的命令,都会将该命令写入缓存中,然后再根据配置文件中的配置来决定何时将其同步到硬盘AOF文件中。

      有三种AOF持久化方式:

      1. 每次修改都会写入,这样会严重影响性能
      2. 每秒同步一次
      3. 让操作系统决定何时同步

      为了兼顾数据和写入性能,可以采用第二种,几乎对redis性能没影响,而且即使出现系统崩溃,也只会丢失一秒的数据

  4. bigkey是什么?有什么危害?怎么发现?

    当一个key对应的value占用空间比较大时,这个key就被称为bigkey。比如string类型的value超过10kb,复合类型的value包含的元素超过5000个。

    bigkey会占用更多的内存空间,对性能有很大影响。

    如何发现:

    1. 使用自带的bigkeys参数来找
    2. 分析RDB文件。这个方法的前提是采用的RDB持久化
  5. 什么是缓存穿透?怎么解决?

    大量请求的key不存在缓存中,就会导致请求直接全都到了数据库上,就会给数据库带来很大的压力。

    解决方案:

    1. 缓存无效key。当请求的key在缓存和数据库中都查不到时,就把它写到一个缓存中,如果这个key再次请求就可以直接拒绝掉。但是这种方法只能用于key变换不频繁的情况,如果一个黑客换不同的key的话,这种方案就不行了。
    2. 布隆过滤器。将所有可能请求的值都放到布隆过滤器中,如果请求的值在布隆过滤器中,才会走下面的流程,不存在的话直接返回错误信息。
  6. 什么是缓存雪崩?怎么解决?

    缓存同一时间大面积失效,导致大量的请求都落到了数据库上,造成很大的压力。

    有两种情况,一种是redis宕机了,导致请求都走到服务器上。一种是大量的数据在同一时刻都过期了,导致请求走到了服务器上。

    解决方案:

    1. 对于前者,可以用redis集群,避免单机出现问题导致整个缓存服务都没法用。也可以限流,避免同时处理大量请求。
    2. 对于后者,可以设置不同的失效时间,比如给他们的失效时间加上随机值,这样就不会同一时刻大量失效了。

MySQL

  1. MyISAM与InnoDB的区别

    InnoDB支持事务,MyISAM不支持

    InnoDB支持行级锁,MyISAM支持表级锁

    InnoDB支持MVCC,MyISAM不支持

    InnoDB支持外键,MyISAM不支持

    InnoDB不保存总行数,MyISAM保存总行数,所以MyISAM查询总行数速度快

  2. int(10)中10指什么?

    指最大显示宽度为10,如果不足10位前面用0补齐

  3. MySQL如何获取当前日期?

    SELECT CURRENT_DATE()

  4. 说一说MySQL中的锁机制?

    锁是用来保证并发访问时数据一致性的机制。

    按粒度分:

    • 表级锁。粒度最大的一种锁。对整个表进行加锁,开销小,加锁快,不会出现死锁,但发生锁冲突的概率高,并发低。
    • 行级锁。粒度最小的一种锁。只对操作的行进行加锁,开销大,加锁慢,会出现死锁,但发生锁冲突的概率小,并发高。
    • 页级锁。介于前两者之间。

    按操作分:

    • 共享锁:一个事务对一个数据加上之后,别的事务也可以对他加共享锁,但是他们都只能读不能写。
    • 排它锁:一个事务对一个数据加上之后,这个事务可以读可以写,别的事务既不能对他加共享锁,也不能加排它锁。
  5. MySQL中DATETIME和TIMESTAMP的区别?

    1. DATETIME范围是1001-9999年,TIMESTAMP范围是1970-2038年
    2. DATETIME时间与时区无关,TIMESTAMP与时区有关
    3. DATETIME存储空间为8字节,TIMESTAMP为4字节
    4. DATETIME默认为null,TIMESTAMP默认为当前时间
  6. MySQL事务隔离级别,解决了什么问题?

    1. 读未提交
    2. 读已提交:解决了脏读
    3. 可重复读:解决了不可重复读
    4. 串行化:解决了幻读
  7. 可重复读如何实现的?/如何理解MVCC?

    使用了MVCC机制。MVCC就是多版本并发控制,为了解决发生读写冲突时加锁造成的额外性能消耗。

    实现原理:

    数据表中每一个字段后面有3个隐藏属性,一个是主键ID,是自增的,如果已经指定了主键那么就没有这个属性了。第二是最近一次修改这个数据的事务的ID,第三个是回滚指针,指向这条记录的上一个版本。

    当事务进行快照读的时候,会生成一个ReadView,这个ReadView就是用来判断当前事务能读哪一个版本的。ReadView里面有一个记录生成ReadView时活跃的事务列表,有一个低水位,记录了列表里最小的ID,有一个高水位,记录了下一个要生成的事务ID,也就是已生成的事务ID + 1。如果当前事务的ID小于低水位,那么当前事务就可以看到记录。如果当前事务的ID大于等于高水位,那么就表示这个事务是在ReadView之后才生成的,就不可以读到。如果在低水位和高水位之间,那么就判断他是不是在活跃事务列表里,如果在的话,就代表还没提交,所以不可见,如果不在,就说明已经提交了,就可以读到。

    在上面的阶段中,如果不可见的话,就会根据回滚指针查找上一个版本,然后继续进行判断,直到找到可见的版本。

    RC每个快照读都会生成新的ReadView,而RR在同一事务中只会在第一次快照读的时候生成ReadView,以后获取的都是同一个ReadView

  8. 如何解决幻读?

    对于快照读的幻读问题,因为快照读是从ReadView里面获取数据的,所以自然不会看到新插入的行,所以MVCC就已经解决了快照读的幻读。

    而对于当前读,MVCC是无法解决的。可以使用gap锁或next-key锁来解决,这样可以锁住行与行之间的间隙,别的事务就无法插入数据了。

  9. 什么是索引?

    索引是对列进行排序的一种结构,可以提高检索的速度。

  10. 常见的索引结构都有哪些?

hash:底层就是哈希表,进行查找时,根据key计算hashcode,然后找到对应的数据行地址,然后根据地址拿到数据

B树:是一种多路搜索树,每个结点记录了key,key对应的value的地址,指向下一层节点的指针。

B+树:是B树的变种,它的每个非叶子结点记录了key和指向下一层结点的指针, 他把数据的地址都放到了叶子结点,然后通过指针连接起来。MySQL用的就是B+树

  1. 为什么MySQL数据库要用B+树存储索引?而不用红黑树、Hash、B树?

    红黑树:如果在内存中,红黑树的查找效率比B+树高,但是因为涉及到磁盘操作,而红黑树是一个二叉树,所以数据量大的时候层数会很高,从根节点向下查找的过程中,每读一个结点都相当于一次IO操作,所以效率就比不上B+树了。

    Hash:如果只查单个值的话Hash效率是很高的,但是他不支持范围查询,也不支持索引值的排序,也不支持联合索引的最左匹配规则

    B树索引:B树索相比于B+树,在进行范围查询时,需要做局部的中序遍历,可能要跨层访问,跨层访问代表着要进行额外的磁盘I/O操作;另外,B树的非叶子节点存放了数据记录的地址,会导致存放的节点更少,树的层数变高。

  2. MySQL 中的索引叶子节点存放的是什么?

    MyISAM和InnoDB都是采用的B+树作为索引结构,但是叶子节点的存储上有些不同。

    MyISAM:主键索引和辅助索引(普通索引)的叶子节点都是存放 key 和 key 对应数据行的地址。在MyISAM 中,主键索引和辅助索引没有任何区别。

    InnoDB:主键索引存放的是 key 和 key 对应的数据行。辅助索引存放的是 key 和 key 对应的主键值。因此在使用辅助索引时,通常需要检索两次索引,首先检索辅助索引获得主键值,然后用主键值到主键索引中检索获得记录。

  3. 什么是聚簇索引(聚集索引)?

    就是将索引和数据放到了一起,找到了索引也就找到了数据,效率非常高。只有InnoDB的主键索引是聚簇索引,辅助索引和MyISAM的索引都是非聚簇索引。InnoDB中,有且只有一个聚簇索引,一般是主键,如果没有主键的话就则优先选择非空的唯一索引,如果也没有唯一索引,就会创建一个隐藏的row_id作为聚簇索引。

  4. 什么是回表查询?

    对于InnoDB的主键索引来说,只需要走一遍主键索引就可以在叶子节点拿到数据。

    而对于辅助索引来说,叶子节点存的是key和主键值,所以还需要再走一次主键索引,这就是回表查询。

  5. 走普通索引,一定会出现回表查询吗?

    不一定,如果查询的字段全部命中了索引就不需要了,比如有一个 user 表,主键为 id,name 为普通索引,则再执行:select id, name from user where name = ‘joonwhee’ 时,通过name 的索引就能拿到 id 和 name了,因此无需再回表去查数据行了。

  6. 什么是覆盖索引(索引覆盖)吗?

    当索引包含了查询语句中的所有列时,就出发了覆盖索引,不需要进行回表查询,效率很高

  7. 联合索引(复合索引)的底层实现?最佳左前缀原则?

    联合索引底层还是使用B+树索引,并且还是只有一棵树,只是此时的排序会:首先按照第一个索引排序,在第一个索引相同的情况下,再按第二个索引排序,依次类推。

    最左前缀匹配原则就是联合索引是从最左边开始匹配直到遇到范围查询,因为后面的索引都是在前面排好序的基础上进行排序的,如果左边都没排好序,那么右边也是无序的。

  8. union 和 union all 的区别 ?

    union all:对两个结果集直接进行并集操作,记录可能有重复,不会进行排序。

    union:对两个结果集进行并集操作,会进行去重,记录不会重复,按字段的默认规则排序。

    因此,从效率上说,UNION ALL 要比 UNION 更快。

  9. B+树中一个节点到底多大合适?

    1页或页的倍数最为合适。因为如果一个节点的大小小于1页,那么读取这个节点的时候其实也会读出1页,造成资源的浪费。所以为了不造成浪费,所以最后把一个节点的大小控制在1页、2页、3页等倍数页大小最为合适。 MySQL中一个结点大小为1页,默认为16k

  10. 为什么一个节点为1页就够了?

    Innodb中,B+树中的一个节点存储的内容是:

    非叶子节点:key + 指针

    叶子节点:数据行(key 通常是数据的主键)

    对于叶子节点:我们假设1行数据大小为1k(对于普通业务绝对够了),那么1页能存16条数据。

    对于非叶子节点:key 使用 bigint 则为8字节,指针在 MySQL 中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170个。那么一颗高度为3的B+树能存储的数据为:1170 * 1170 * 16 = 21902400(千万级)。

    所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。在查找数据时一次页的查找代表一次IO,所以通过主键索引查询通常只需要1-3次 IO 操作即可查找到数据。千万级别对于一般的业务来说已经足够了,所以一个节点为1页,也就是16k是比较合理的。

  11. 什么是 Buffer Pool?

    Buffer Pool 是 InnoDB 维护的一个缓存区域,用来缓存数据和索引在内存中,主要用来加速数据的读写,如果 Buffer Pool 越大,那么 MySQL 就越像一个内存数据库,默认大小为 128M。

    InnoDB 会将那些热点数据和一些 InnoDB 认为即将访问到的数据存在 Buffer Pool 中,以提升数据的读取性能。

    InnoDB 在修改数据时,如果数据的页在 Buffer Pool 中,则会直接修改 Buffer Pool,此时我们称这个页为脏页,InnoDB 会以一定的频率将脏页刷新到磁盘,这样可以尽量减少磁盘I/O,提升性能。

  12. InnoDB四大特性

    1. 插入缓冲

      对于辅助索引,不是每一次都直接插入到索引页中。而是先判断缓冲池中是否有它,如果有就直接插入,如果没有先放入到一个缓冲对象中,在以一定的频率去插入,这样就提高了辅助索引插入的性能。

    2. 二次写

      如果脏页在向磁盘刷新时,还没刷新完发生了故障,那么就会出现数据不完整的问题。

      二次写有2部分组成,一个是二次写缓冲,一个是共享表。

      每次脏页刷新时:

      1. 先将数据复制到二次写缓冲中
      2. 再将二次写缓冲分两次写到共享空间中
      3. 然后将二次写缓冲写到实际的空间中

      这样就算发生故障,也可以从共享空间进行数据恢复

    3. 自适应哈希索引

      哈希的查找速度非常快,但因为不支持范围查询,所以InnoDB没有采用它。但InnoDB会监控索引的查找,对于某些频繁访问的热点数据,自动为他们建立哈希索引提升速度。

    4. 预读

      InnoDB预计某些page可能很快会用到时,会提前将他们读到缓冲池中。有两种预读:一个是线性预读,一个是随机预读。线性预读是着眼于下一个块,而随机预读是着眼于当前块中剩下的page。

  13. InnoDB 的行锁是怎么实现的?

    通过索引上的索引项来实现的,所以只有通过索引检索数据时才会使用行锁。

    对于主键索引:只需锁住主键索引即可

    对于普通索引:先锁住普通索引,接着锁住主键索引,这是因为一张表的索引可能存在多个,通过主键索引才能确保锁是唯一的,不然如果同时有2个事务对同1条数据的不同索引分别加锁,那就可能存在2个事务同时操作一条数据了。

  14. InnoDB 锁的算法有哪几种?

    Record lock:记录锁,单条索引记录上加锁,锁住的永远是索引,而非记录本身。

    Gap lock:间隙锁,在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。

    Next-key lock:Record lock 和 Gap lock 的结合,即除了锁住记录本身,也锁住索引之间的间隙。

  15. MySQL 如何实现悲观锁和乐观锁?

    乐观锁:更新时带上版本号(cas更新)

    悲观锁:使用共享锁和排它锁,select…lock in share mode,select…for update。

  16. 存储引擎的选择?

    没有特殊情况,使用 InnoDB 即可。如果表中绝大多数都只是读查询,可以考虑 MyISAM。

  17. explain 用过吗,有哪些字段分别是啥意思?

    explain 字段有:

    id:标识符

    select_type:查询的类型

    table:输出结果集的表

    partitions:匹配的分区

    type:表的连接类型

    possible_keys:查询时,可能使用的索引

    key:实际使用的索引

    key_len:使用的索引字段的长度

    ref:列与索引的比较

    rows:估计要检查的行数

    filtered:按表条件过滤的行百分比

    Extra:附加信息

  18. type 中有哪些常见的值?

    按类型排序,从好到坏,常见的有:const > eq_ref > ref > range > index > ALL。

    const:通过主键或唯一键查询,并且结果只有1行(也就是用等号查询)。因为仅有一行,所以优化器的其余部分可以将这一行中的列值视为常量。

    eq_ref:通常出现于两表关联查询时,使用主键或者非空唯一键关联,并且查询条件不是主键或唯一键的等号查询。

    ref:通过普通索引查询,并且使用的等号查询。

    range:索引的范围查找(>=、<、in 等)。

    index:全索引扫描。

    All:全表扫描

  19. explain 主要关注哪些字段?

    主要关注 type、key、row、extra 等字段。主要是看是否使用了索引,是否扫描了过多的行数,是否出现 Using temporary、Using filesort 等一些影响性能的主要指标。

  20. 如何做慢 SQL 优化?

    首先搞明白慢的原因是什么,一般从三个方面来优化

    1. 使用explain看看是否使用了索引,如果可以用索引解决就用索引解决。
    2. 看看是否加载了不需要的列
    3. 看看是否数据量过大,能否拆分

计算机网络

  1. Http和Https的区别?

    http是运行在tcp上的明文传输,https是加了SSL外壳的http,运行在SSL之上,是添加了加密和认证的HTTP。

    • 端口不同:HTTP端口是80,HTTPS端口是443
    • 资源消耗不同:HTTPS需要加密解密,所以会消耗更多的CPU和内存资源
    • 开销:HTTPS需要证书,需要向认证机构购买
  2. 对称加密与非对称加密?

    对称加密就是加密和解密用同一个密钥,所以最大的问题就是如何将密钥安全的传给对方。

    非对称加密就是使用公钥和私钥,发送者使用对方的公钥进行加密,接收者使用私钥进行解密,比对称加密要安全,但也会造成速度慢

  3. 三次挥手过程

    一开始客户端处于关闭状态,服务端处于监听状态。

    1. 客户端发送一个SYN报文,标志位为SYN,并生成一个初始化序列号ISN,此时客户端处于同步已发送状态。
    2. 服务端收到报文后,也会生成一个初始化序列号ISN,同时把客户端的ISN+1作为ACK的值,一起发送给客户端,此时服务端处于同步已回复状态。
    3. 客户端接收到报文后,将服务端的ISN+1作为ACK,然后将自己的序号加1,发送给服务端,此时客户端处于连接状态,服务端接收到报文后也处于连接状态。、

    前两次挥手不可以携带数据,第三次挥手可以携带数据。

  4. 为什么前两次挥手不可以携带数据,第三次挥手可以携带数据?

    因为如果有人要恶意攻击服务器,如果第一次握手可以携带数据的话,他就会在第一次挥手中的报文中加入大量数据,他不用管服务端是否连接,疯狂发送报文,服务端就会花费很多时间和内存来接收这些报文。

    对于第三次连接,他此时已经是连接状态,并且知道服务端的发送和接受能力都是正常的,所以就可以携带数据。

  5. 为什么需要三次握手,两次不行吗?

    • 第一次握手:客户端发送网络包,服务端收到了。

      这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。

    • 第二次握手:服务端发包,客户端收到了。

      这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。

    • 第三次握手:客户端发包,服务端收到了。

      这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

    如果是用两次握手,则会出现下面这种情况:

    如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

  6. ISN(Initial Sequence Number)是固定的吗?

    三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

  7. SYN攻击是什么?
    服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

    常见的防御 SYN 攻击的方法有如下几种:

    缩短超时(SYN Timeout)时间
    增加最大半连接数
    过滤网关防护
    SYN cookies技术

  8. 四次挥手过程

    一开始客户端和服务端都处于连接状态,关闭可以由任意一方发起,假设由客户端发起。

    • 客户端发送一个FIN报文,并生成一个序列号,主动关闭TCP连接,然后客户端处于终止等待1状态。
    • 服务端接收到报文后,将客户端的ISN+1作为ACK,然后发送给客户端,此时服务端处于关闭等待状态,客户端接收到后处于终止等待2状态。此时服务端可以继续发送没发送完的数据给客户端。
    • 当服务端也要断开连接时,也发送一个FIN报文,将客户端的ISN+1作为ACK,发送给客户端,服务端进入最后确认状态。
    • 客户端接收到后,将服务端的ISN+1作为ACK,发送给服务端,然后客户端进入时间等待状态,等经过2SML后才进入关闭状态。服务端接收到报文后就处于关闭状态。
  9. 挥手为什么需要四次?
    因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

  10. 四次挥手释放连接时,等待2MSL的意义?

MSL是最长报文段寿命,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

  1. 客户端发送最后一个ACK报文时可能会丢失,如果服务端一段时间接收不到报文时会重传一个报文,然后客户端接收到后再重新发送一次,然后正常关闭。如果不经过这个等待时间,而是直接关闭的话,如果报文丢失,服务端就无法正常关闭了。

  2. 经过2MSL的等待,本次连接所有的报文都从网络中消失了,下一次连接j就不会出现旧连接的报文段了。

  3. TCP和UDP的区别?

    1. TCP是需要建立连接的,UDP不需要建立连接
    2. TCP是可靠传输,UDP是不可靠传输
    3. TCP是面向字节流的,UDP是面向报文的
    4. UDP的传输速率比TCP快
  4. TCP如何保证可靠?

    1. 超时重传。TCP传输过程中,发送发发出数据后,就会等待接收方发送的报文,如果超过一定时间没有接收到报文,就会进行重发。接受端收到重发的数据后,如果数据不存在,那么说明第一次发送的时候没有接收到数据,那么就会接收数据然后进行ACK应答;如果发现数据已存在,那么说明第一次接收到消息并且应答了,只是发送端没有接收到应答,那么接收端就会直接丢弃,然后发送ACK应答。
    2. 流量控制。如果发送端发送数据过快,那么接受端的缓冲区会很快被填满,那么发送端后面的数据就会丢失。流量控制就是TCP根据接受端的处理能力来决定发送端的发送速度。报文段头部中有一个窗口大小,里面其实就是接收端缓冲区剩余的大小,每次接收端应答时都会将自己的缓冲区剩余大小跟随报文一起发送,然后发送端就可以根据这个来决定自己的发送速度,如果窗口大小为0,就会停止发送,然后定期发送数据段来探测窗口大小。
    3. 拥塞控制。发送方刚开始发送数据时,网络可能很拥堵,如果这时候仍然发送大量数据就会造成丢包,所以TCP会引入慢启动机制,在刚开始时先发送少量数据进行探路,然后根据网络状态来决定传输速率。
    4. 校验和
    5. 序列号和确认号
    6. 三次握手四次挥手
  5. 什么是HTTP?

    就是超文本传输协议,对客户端和服务端数据传输的一种规范。

  6. HTTP由什么组成?

    • 请求报文:
      1. 请求行:请求方法、URL、HTTP版本协议
      2. 请求头
      3. 请求体
    • 响应报文:
      1. 状态行:HTTP版本、状态码
      2. 响应头
      3. 响应体
  7. GET和POST区别?

    1. 后退按钮或刷新的时候,POST会重新提交,GET不会
    2. GET可以被缓存,POST不可以,除非手动设置
    3. GET的数据在URL里,可以被看到,POST在请求体里,地址栏看不到
    4. 受浏览器限制,URL长度受限,所以GET数据大小有限制,POST无限制
    5. GET主要用于获取资源,POST主要用于发送数据修改资源
    6. GET速度比POST快
    7. GET产生一个TCP数据报,请求头和请求体一起发送。TCP产生两个TCP数据报,先发送请求头,服务器响应100后再发送请求体
  8. 聊一聊HTTP的状态码有哪些?
    2XX 成功

    200 OK,表示从客户端发来的请求在服务器端被正确处理

    201 Created 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立
    202 Accepted 请求已接受,但是还没执行,不保证完成请求
    204 No content,表示请求成功,但响应报文不含实体的主体部分
    206 Partial Content,进行范围请求
    3XX 重定向

    301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302 found,临时性重定向,表示资源临时被分配了新的 URL
    303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
    304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
    307 temporary redirect,临时重定向,和302含义相同
    4XX 客户端错误

    400 bad request,请求报文存在语法错误
    401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403 forbidden,表示对请求资源的访问被服务器拒绝
    404 not found,表示在服务器上没有找到请求的资源
    408 Request timeout, 客户端请求超时
    409 Confict, 请求的资源可能引起冲突
    5XX 服务器错误

    500 internal sever error,表示服务器端在执行请求时发生了错误
    501 Not Implemented 请求超出服务器能力范围,例如服务器不支持当前请求所需要的某个功能,或者请求是服务器不支持的某个方法
    503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求
    505 http version not supported 服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本

  9. DNS解析过程

    1. 首先搜索浏览器缓存,如果没有命中就搜索操作系统的缓存,也即是hosts文件。
    2. 如果还没有命中,本地域名服务器就递归去查询自己的DNS缓存,也就是本地域名服务器以DNS客户的向其他的域名服务器去查询,就是代替主机去查询。
    3. 如果没有命中,那本地域名服务器就上级域名服务器进行迭代查询。
      1. 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器
      2. 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址
      3. 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
    4. 本地域名服务器拿到地址后,将地址缓存起来,然后操作系统自己也缓存起来,然后发给浏览器,浏览器也缓存起来
  10. HTTP特点?

    1. 无状态。每次客户端的请求服务端都认为是新请求,两次会话之间没有联系。
    2. 支持任意类型数据的传输。在content-type里设置。
    3. 支持B/S模式。
  11. HTTP长连接和短连接?
    HTTP长连接,指的是复用TCP连接。多个HTTP请求可以复用同一个TCP连接,这就节省了TCP连接建立和断开的消耗。

    HTTP1.0默认使用的是短连接。浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。

    HTTP/1.1起,默认使用长连接。要使用长连接,客户端和服务器的HTTP首部的Connection都要设置为keep-alive,才能支持长连接。

  12. HTTP1.1和 HTTP2.0的区别?
    HTTP2.0相比HTTP1.1支持的特性:

    新的二进制格式:HTTP1.1 基于文本格式传输数据;HTTP2.0采用二进制格式传输数据,解析更高效。

    多路复用:在一个连接里,允许同时发送多个请求或响应,并且这些请求或响应能够并行的传输而不被阻塞,避免 HTTP1.1 出现的”队头堵塞”问题。

    头部压缩,HTTP1.1的header带有大量信息,而且每次都要重复发送;HTTP2.0 把header从数据中分离,并封装成头帧和数据帧,使用特定算法压缩头帧,有效减少头信息大小。并且HTTP2.0在客户端和服务器端记录了之前发送的键值对,对于相同的数据,不会重复发送。比如请求a发送了所有的头信息字段,请求b则只需要发送差异数据,这样可以减少冗余数据,降低开销。

    服务端推送:HTTP2.0允许服务器向客户端推送资源,无需客户端发送请求到服务器获取。

JVM

  1. 说一下 JVM 的主要组成部分及其作用?

    包括两个子系统和两个组件:两个子系统是类加载子系统和执行引擎,两个组件是运行时数据区和本地接口。

    类加载系统:根据给定的全限定类名将class文件加载到运行时数据区的方法区

    执行引擎:用来执行classes中的指令

    本地接口:和其他编程语言交互的接口

    运行时数据区:就是常说的JVM内存

    首先编译器把Java代码编译为字节码,然后类加载器将字节码加载到运行时内存区的方法区,字节码并不能直接交给操作系统去执行,需要通过执行引擎将它翻译为底层系统指令,然后交给CPU执行,这个过程需要调用其他语言的本地接口来完成。

  2. 说一下 JVM 运行时数据区

    运行时数据区分为5个部分,2个线程共享数据区:堆,方法区,3个线程线程隔离数据区:虚拟机栈,本地方法栈,程序计数器

    堆:虚拟机内存区域最大的一块,被所有线程共享,用来存放对象的实例

    方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等

    程序计数器:保存当前线程正在执行的字节码指令地址,分支、循环、跳转、线程恢复都需要这个计数器

    虚拟机栈:用来存储局部变量表、操作数栈、动态链接、方法出口等

    本地方法栈:和虚拟机栈类似,区别是虚拟机栈是服务Java方法的,本地方法栈是服务本地方法的

  3. 深拷贝和浅拷贝?

    浅拷贝就是增加一个指针指向已存在的内存地址。

    深拷贝是开辟一块新的内存存放复制的对象。

    浅拷贝的话如果某个指针指向的对象放生变化或者释放内存,那么它的所有浅拷贝出来的对象也会发生相同的变化

  4. 说一下堆栈的区别?

    1. 堆的物理地址是不连续的,所以性能慢一些;栈的物理地址是连续的,性能快一些。
    2. 堆的内存分配是在运行期确认的;栈的内存分配是在编译器确认的,大小固定。
    3. 堆存放的是对象实例和数组;栈存放的是局部变量,操作数栈,方法出口等。
    4. 堆对于所有线程可见、共享;栈是线程私有的
  5. 对象创建的过程

    1. 虚拟机遇到一条new指令时,先判断常量池是否已经加载相应的类,如果没加载就先加载。
    2. 加载后,为对象分配内存。如果堆的内存是绝对规整的,也就是用过的内存放在一边,没用的放在另一边,就使用指针碰撞的方式分配内存,也就是将中间的指针向空闲内存移动和对象大小相等的距离;如果堆的内存是不规整的,就从空闲列表中分配
    3. 还要处理并发安全问题。有两种方法,一个是同步处理:采用CAS,一个是使用本地线程分配缓冲,就是在堆上为每个线程预先分配一块区域,线程只在自己的区域上分配内存
  6. Java会存在内存泄漏吗?
    内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。

    但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

  7. 什么情况下会发生栈内存溢出?

    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
    如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)

  8. 发生OOM的场景

    1. 堆溢出:不断地创建对象,并且一直没有被回收,到达最大的容量限制后就会溢出。

      解决方案:先判断是否为内存溢出,也就是长生命周期的引用拿着短生命周期的引用,如果是的话将它回收。如果不是内存溢出的话,就检查JVM的参数设置的是否适当。

    2. 栈溢出:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

      如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

    3. 运行时常量池溢出:比如一直调用string的intern方法,将一个字符串添加到常量池中,然后达到最大限制时会发生溢出。解决方案:通过-XX:PermSize和-XX:MaxPermSize 设置方法区的大小,从而间接限制常量池的容量

    4. 方法区溢出:方法区存放了class相关的信息,在经常生成大量class的应用中可能会出现这种溢出。

  9. 简述Java垃圾回收机制
    在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

  10. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
    对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

  1. Java 中都有哪些引用类型?
    强引用:发生 gc 的时候不会被回收。
    软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
    弱引用:有用但不是必须的对象,在下一次GC时会被回收。
    虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

  2. 怎么判断对象是否可以被回收?
    垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

    一般有两种方法来判断:

    引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
    可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

  3. 说一下 JVM 有哪些垃圾回收算法?
    标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
    复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
    标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
    分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

  4. 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
    新生代回收器:Serial、ParNew、Parallel Scavenge
    老年代回收器:Serial Old、Parallel Old、CMS
    整堆回收器:G1
    新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

  5. 说下CMS和G1

    CMS:是老年代的回收器,可以配合新生代的serial和ParNew一起使用。是一种以获得最短回收停顿时间为目的的回收器。采用的是标记清楚算法,容易产生内存碎片

    G1:收集范围是新生代和老年代。可以预测垃圾回收的停顿时间,采用的是标记整理算法,不容易产生内存碎片

  6. 简述分代垃圾回收器是怎么工作的?
    分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

    新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

    把 Eden + From Survivor 存活的对象放入 To Survivor 区;
    清空 Eden 和 From Survivor 分区;
    From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
    每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

    老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

  7. 为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

    如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
    Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
    设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

  8. 简述java内存分配与回收策率以及Minor GC和Major GC

    1. 对象优先分配在Eden区,当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
    2. 大对象,也就是需要大量连续内存的对象直接进入老年代,不然会频繁的在Eden和survior发生复制。
    3. 长期存活的对象进入老年代。

    Minor GC是发生在新生代,非常频繁,速度快;Major GC发生在老年代,速度慢

  9. 简述java类加载机制和原理

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。

    Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

    类装载方式,有两种 :

    1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,

    2.显式装载, 通过class.forname()等方法,显式加载需要的类

    Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

  10. 什么是类加载器,类加载器有哪些?

    实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

    主要有一下四种类加载器:

    • 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
    • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
    • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
    • 自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
  11. 说一下类装载的执行过程?

    类装载分为以下 5 个步骤:

    • 加载:根据查找路径找到相应的 class 文件然后导入;
    • 验证:检查加载的 class 文件的正确性;
    • 准备:给类中的静态变量分配内存空间;
    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
    • 初始化:对静态变量和静态代码块执行初始化工作。
  12. 什么是双亲委派模型?

    当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。 、

    好处:

    • 避免重复加载:因为加载这个类会优先让加载器的父类进行加载,那么如果父类已经加载过了,他就不需要加载了,避免了重复加载。
    • 避免核心类篡改:如果有人想篡改核心类,比如自己写一个String类,有了双亲委派模型的话,会优先让顶级加载器也就是启动类加载器进行加载,那么自己写的String根本就没有机会去加载,因为父类已经加载过了。

RabbitMQ

  1. AMQP是什么?

    AMQP是应用层的一个提供统一消息服务的队列协议。分为三层,最高层定义了客户端调用的命令,客户端可以通过他实现自己的业务逻辑。中间层负责客户端和服务端的交互,提供可靠同步机制和错误处理。最底层主要传输二进制数据流。

  2. AMQP几大组件?

    1. 交换机:消息代理服务器把消息路由到队列的组件
    2. 队列:用来存放消息的数据结构,位于硬盘或内存中
    3. 绑定:一套规则,用来告知交换机应该把消息投递给哪个队列
  3. 为什么需要消息队列?

    1. 异步处理
    2. 服务解耦
    3. 流量削峰
  4. 说说Broker服务节点、Queue队列、Exchange交换器?

    • Broker:可以看做RabbitMQ的服务节点。一般请下一个Broker可以看做一个RabbitMQ服务器。
    • Queue:RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
    • Exchange:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。
  5. 消息队列的缺点?

    1. 系统可用性降低:MQ挂掉的话消息就无法正常传递,可能整个系统就会出现问题。
    2. 复杂性提升:要保证消息不被重复消费和丢失
    3. 数据不一致:系统A处理完直接返回成功,用户以为请求成功,但是消息可能到了BCD系统那里,BC处理完了,但是D还没有处理完。
  6. MQ如何保证消息的可靠性?

    1. 生产者丢失数据:两种方案。第一种使用事务,就是发送消息前开启事务,如果MQ没有接收到消息就回滚,收到了就提交,但是这种方式是同步的,比较耗性能。第二种使用confirm机制,就是MQ收到消息后返回给生产者一个ack消息,这种方式是异步的,速度比较快。
    2. MQ自身丢失数据:持久化。首先队列创建的时候要持久化,其次发送消息的时候消息也设为持久化。但是还有一种情况,还没来得及持久化MQ就崩了,那么可以结合confirm机制,在持久化到磁盘后再返回ack消息。
    3. 消费者丢失消息:关闭自动应答,使用deliverCallback手动应答。
  7. 交换器4种类型?

    1. fanout:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。

    2. direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中。

    3. topic:

      匹配规则:

      RoutingKey 为一个 点号’.': 分隔的字符串。比如: java.xiaoka.show

      BindingKey和RoutingKey一样也是点号“.“分隔的字符串。

      BindingKey可使用 * 和 # 用于做模糊匹配,*匹配一个单词,#匹配多个或者0个

    4. headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配。性能差,基本用不到。

  8. 死信队列?导致死信的原因?

    当消息无法被正常消费时,被重新发送到另一个交换机,这个交换机绑定的队列就是私信队列。

    导致死信的原因:

    1. 消息被拒
    2. 队列满了,无法再添加。
    3. 消息TTL过期。

操作系统

  1. 什么是操作系统?

    管理计算机硬件和软件资源的程序。

  2. 用户态和内核态?什么是系统调用?

    用户态运行的进程可以访问用户程序的资源

    内核态的进程可以访问计算机系统的任何资源,没有限制

    系统调用:我们运行的程序基本都是用户态,当我们需要调用系统级别的功能时,就必须通过系统调用来帮助我们去调用。

  3. 进程间的通信方式有哪几种?

    1. 匿名管道:用于父子进程和兄弟进程间的通信
    2. 有名管道:因为匿名管道只能用于有亲缘关系的进程间的通信,为了克服这个出现了有名管道,遵循FIFO原则,可以实现本机任意两个进程间的通信
    3. 信号:用于通知接收进程某个事件已发生
    4. 消息队列:是消息的链表,有特定的格式,存放在内存中。消息队列可以实现随机读取,比FIFO更有优势。消息队列只有在内核重启或者显式删除时,才会被删除掉。
    5. 信号量:是一个计数器,可以实现进程间对共享数据的访问,实现同步
    6. 共享内存:多个进程间可以访问同一块内存,这是方式需要依靠同步操作
    7. 套接字:用于实现客户端和服务端的通信
  4. 线程间的同步方式?

    1. 互斥量。可以保证共享资源不会被多个线程访问,比如Java的synchronized和lock
    2. 信号量。允许同一时刻多个线程去访问共享资源,但是要控制最大访问的数量
    3. 事件:通过通知操作来实现线程同步
  5. 调度算法有哪些?

    1. 先来先服务
    2. 短作业优先
    3. 时间片轮转
    4. 多级反馈队列
    5. 优先级调度
  6. 内存管理是做什么的?

    就是负责内存的分配与回收,还有将逻辑地址转为物理地址

  7. 内存管理有哪几种方式?

    分为连续分配方式和非连续分配方式。

    • 块式管理:是一种连续分配方式,就是将内存分为几个固定大小的块,每个块中只有一个进程,每当程序运行需要的时候就分配给它一块,这样的话如果程序占用的空间很小,就会造成很大一部分浪费,也就是碎片
    • 页式管理:是一种非连续分配方式,将主存分为大小相等且固定的页,通过页表对应逻辑地址和物理地址。
    • 段式管理:是一种非连续分配方式,将主存分为一段一段的,每段都有实际的意义,每段定义了逻辑信息。
    • 段页式管理:结合了页式管理和段式管理,将主存分为若干段,每段又分为若干页

    页是物理单位,段式逻辑单位,分页可以提高内存利用率,分段可以更好的满足用户的需求

  8. 快表和多级页表?

    快表是为了加快虚拟地址向物理地址转换的速度。可以把它理解为一个高速缓存,里面存了页表的一部分内容。

     1. 首先根据虚拟地址中的页号去查询快表
    
    1. 如果在快表中,直接读取它的物理地址
    2. 如果不存在的话,去访问页表,读取到物理地址,并且在快表中存一份。
    3. 当快表满了的时候再按照一定的淘汰策略进行清除。

    多级页表是为了避免把全部页表都放在内存中占用过多空间,特别是有的页表可能根本用不到,多级页表就是用空间换取时间的做法。

  9. CPU 寻址了解吗?为什么需要虚拟地址空间?

    CPU寻址借助了一个叫内存管理单元的硬件,它可以将虚拟地址转换为物理地址,这样才可以访问到真实的物理地址。

    为什么需要虚拟地址空间:如果没有虚拟地址的话,用户可以直接访问物理地址,一个是很容易破坏操作系统,有可能在你无意中就操作了某个字节,造成了操作系统的破坏。另一个就是使多个程序同时运行变得困难,比如微信运行的时候给某个内存地址赋值,然后又运行了个QQ,它同样也给微信的哪个内存地址赋值,这样就把微信覆盖了,微信就没法运行了。所以使用虚拟地址一个是可以更安全,一个是使不同进程使用的虚拟地址彼此隔离。

  10. 虚拟内存是什么?

    在没有虚拟内存的时候,用户被分配到的空间都在内存中,这样的话可能会造成很多问题,比如:

    1. 程序员在编写程序的时候需要时刻考虑有没有超过自己的边界,有没有进入到别人分配的内存中。
    2. 有可能读取到别人的内存,破坏别人的程序
    3. 分配的内存空间太小,可能不够用。

    有了虚拟内存后,它为每个进程都提供了一个连续的空间,让进程产生一种自己在独享主存的错觉,这样的话用户在编写代码时就不用考虑是否越界了。同时,虚拟内存还把内存扩展到了硬盘,提高了分配到的内存空间。

  11. 页面置换算法的作用?常见的页面置换算法有哪些?

    地址映射过程中,如果要访问的页面不在内存中,就会发生缺页中断,就会将它调入进来,如果当前内存没有空闲的页面了,就要将其中一个页面移出去,为即将到来的页面让出空间。

    常见页面置换算法:

    1. 最佳页面置换算法:淘汰以后最长时间内不被使用的页面,因为无法预知,所以无法实现,可以用来衡量别的算法的优劣。
    2. 先进先出
    3. LRU:最近最久未使用的页面被淘汰
    4. LFU:使用最少的页面被淘汰
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值