Bootstrap

自己记录的一些Java后端面试题(上)

一.说明

记录了一些自己的面试问题。。。。找工作真难啊 干这行 太卷了 算法得会把 基础还得非常扎实 源码也得看

目录

二.Java基础部分

java基础

1.String s = "abc"创建几个对象
  • 如果方法区常量池中没有"abc",那么创建两个,否则创建一个‘
2.方法区和永久代的概念?

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

3.java多态的实现原理:

多态的实现原理

  • 分为继承和接口,一个描述的是子类重写父类方法的多态性,一个是接口的实现类实现接口方法的多态性
  • 继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法
  • 接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表
4.java怎么创建一个对象

在这里插入图片描述

  1. 类加载检查:看看有没有加载过。具体:当程序遇到new 关键字时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程,如果已经被加载,那么进行下一步,为对象分配内存空间

  2. 内存分配,有指针碰撞,空闲列表两种方式

  3. 内存分配中线程安全问题:1.CAS+自旋 2.TLAB:预先给线程在Eden区分配一块内存,不够再CAS在这里插入图片描述

  4. 初始化零值

  5. 虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  6. 执行init方法

注意:指针碰撞的碰撞含义,多个线程同时为对象分配内存时会发生指针碰撞

5.对象访问方式:
  1. 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;在这里插入图片描述

  2. 直接指针访问:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

6.Java注解原理:
  1. 如何定义注解:元注解retention,target,inherited,documented等,有自己的成员
  2. @Documented — — 生成说明文档,添加类的解释 ;@Inherited — —允许子类继承父类中的注解; @Target? — —注解用于什么地方;@Retention — —注解运行状态
  3. 如何使用注解:标在目标的类上面
  4. 注解原理:java.lang.reflect.AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息。实际上还是反射
7.工厂模式:
  1. 简单工厂:一堆if-else语句,每次都得根据输入字符串来new不同实例,不属于gof23种设计模式,违反了开闭原则(得自己加if-else)
  2. 工厂方法:每个类class都对应一个工厂,一个工厂只生产一种class
  3. 抽象工厂:一个工厂生产多种产品,比如一个factory生产子弹和枪械两个class,抽象工厂是生产一整套有产品的(至少要生产两个产品),这些产品必须相互是有关系或有依赖的,而工厂方法中的工厂是生产单一产品的工厂。
  4. beanFactory是抽象工厂方法模式的应用
8.线程的五种状态(java定义):
  1. 新建状态(New):创建后尚未启动的状态

  2. 运行状态(Runnable):Runnable包括了操作系统线程状态中的Running和Ready,也就是说处于此状态的线程有可能正在执行,也可能正在等待CPU为它分配执行时间。

  3. 无限期等待(Waiting):处于这种状态的线程不会被分配给CPU时间,它们要等待被其他线程显示唤醒。

  • 没有设置Timeout参数的Object.wait()方法
  • 没有设置Timeout参数的Thread.join()方法
  • LockSupport.park()方法
  1. 期限等待(Timed Waiting):处于这种状态的线程不会被分配CPU时间,不过无须等待被其他线程显示的唤醒,在一定时间内他们会被系统自动的唤醒
  • Thread.sleep()
  • 设置了Timeout参数的Object.wait()方法
  • 设置了Timeout参数的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUnit()方法
  1. 阻塞(Blocked):在线程等待进入同步区域的时候,线程将进入这种状态。“阻塞状态”和“等待状态”的区别是:“阻塞状态”在等待获取一个排它锁,“等待状态”则是在等待一段时间或者一个唤醒动作。

注意:阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。这几种情况都不释放锁

  1. 结束(Terminated):已终止线程的线程状态。
补充:线程相关api区别:
  1. yield():yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行。yield()不会释放锁

  2. LockSupport中的park() 和 unpark()

    总结一下,LockSupport比Object的wait/notify有两大优势:

    ①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。说明:park和wait的区别。wait让线程阻塞前,必须通过synchronized获取锁

    ②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

  3. join():等待该线程终止。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

9.jdk1.8有什么改变?
  1. 速度更快 – 红黑树
  2. 代码更少 – Lambda
  3. 强大的Stream API – Stream
  4. 便于并行 – Parallel
  5. 最大化减少空指针异常 – Optional
10.java的异常:
  1. 分为exception和error
  2. error属于不受检异常,处理不了
  3. exception分为编译时异常:ioexception;运行时异常:空指针,越界等
  4. 运行时异常就是RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。我们的代码不用管。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
  5. 编译时异常比如IOexception,需要我们人为catch或者throws ,编译器也会报错
11.类加载器,自定义类加载器?
  1. 类加载器
  2. 自定义的话就要重写classLoader的findClass方法,可以通过网络,磁盘等方式获取字节码文件
  3. 关于写一个类能否替换解答的误区。双亲委派是可以打破的在这里插入图片描述
  4. 打破双亲委派机制的例子:Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,比如JDBC,JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而驱动的jar包是放在我们应用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概 念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是系统类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可 以通过线程的上下文类加载器来加载。
12.创建对象的五种方式:
  1. new 一个对象
  2. class.newInstance()方法 反射,只能调用无参的构造函数
  3. Constructor.newInstance()方法,够调用有参构造函数和私有构造函数。
  4. Clone()
  5. Unsafe.allocateInstance-没有初始化对象的实例字段
13.jdk1.8默认垃圾回收器:

Parallel Scavenge + Parallel Old

14.CopyOnWriteList写时复制
  1. 原理:通过加锁lock保证新增操作线程同步,每次修改都会拷贝一个新的数组
  2. 思想:写时复制,保存一个快照,只能保证最终一致性
  3. 缺点:占内存
15.String的hashcode实现?
16.使用异常捕获的代码为什么比较耗费性能?
  • 因为构造异常的实例比较耗性能。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。
17.如何查看java字节码:

javap 命令,内置的反编译工具

18.GC相关,Full GC ,minorGc理解

Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;

  1. young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
  2. full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
  3. perm gen永久代,在jdk1.8后被替换为元空间,它们都是方法区的实现
  4. 针对不同的垃圾回收器,其回收策略也是不一样的
  5. old gc 其实只有CMS这个回收器有这个概念
19.Java的永久代和元空间?

从jdk开始,就开始了永久代的转移工作,将譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但是永久在还存在于JDK7中,直到JDK8,永久代才完全消失,转而使用元空间。而元空间是直接存在内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。

  1. JVM的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入Java堆中. 这样可以加载多少类的元数据就不在由MaxPermSize控制, 而由系统的实际可用空间来控制.
  2. 之所以用元空间永久代中存储字符串和数组容易出现性能问题和内存溢出;而且类和方法信息难以确定,要指定永久代大小不容易,太小容易导致永久代溢出,太大容易导致老年代溢出。而且,在永久代进行GC复杂度高,效率低。
  3. 注意:永久代和元空间都是对方法区的实现
20.关于包装类Integer

如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false

Integer a  = 128;
Integer b = 128;
a==b ---> false
21.Java8 Stream原理:

stream底层是如何实现的呢?我们在使用 Stream API时,基本上所有中间操作,都会传入一个Lambda表达式,也就是回调函数。因此,一个完整的操作应该是由<数据来源,操作框架,回调函数>构成的三个元素组成:

  1. Stream API框架里,使用Stage记录每一个操作,Stage之间形成一个双向链表
  2. 怎么记录操作? 多个Stage以双向链表的形式组织在一起,构成整个流水线:每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构,就能记录下对数据源的所有操作。
  3. 如何叠加? 已经在stage中记录了每一步操作,此时并没有执行。但是stage只是保存了当前的操作,并不能确定下一个stage需要何种操作,何种数据,其实JDK为此定义了Sink接口,其中只有begin()、end()、cancellationRequested()、accept()四个接口
  4. 实际上Stream API内部实现的的本质,就是如何重写Sink的这四个接口方法。
  5. 有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}
  6. 如何执行? 有一个wrapSink()的方法,会讲上游到下游所有sink方法封装,然后for循环遍历调用。迭代器是SplitIterator
    在这里插入图片描述
22.序列化和反序列化如何实现?
  • 解释:Java对象序列化是将实现了Serializable接口的对象转换成一个字节序列,能够通过网络传输、文件存储等方式传输 ,传输过程中却不必担心数据在不同机器、不同环境下发生改变,也不必关心字节的顺序或其他任何细节,并能够在以后将这个字节序列完全恢复为原来的对象(恢复这一过程称之为反序列化)。

  • 原因:我们知道,不同进程/程序间进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等,而这些数据都会以二进制序列的形式在网络上传送。这就需要使用Java序列化与反序列化了。发送方需要把这个Java对象转换为字节序列,然后在网络上传输,接收方则需要将字节序列中恢复出Java对象。

  • 序列化ID?序列化运行时会使用一个称为 serialVersionUID 的版本号,并与每个可序列化的类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。理解:相当于快递的打包和拆包,里面的东西要保持一致,不能人为的去改变他,不然就交易不成功。序列化与反序列化也是一样,而版本号的存在就是要是里面内容要是不一致,不然就报错。像一个防伪码一样

  • 实现:OutputStream 的读和写api,封装input / outputstream

23.final关键字

1、修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

2、修饰方法
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。

3、修饰变量
修饰变量是final用得最多的地方,也是本文接下来要重点阐述的内容。

  final成员变量表示常量,只能被赋值一次,赋值后值不再改变。

1. 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。

2. final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。

3. 当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。

24.虚引用的作用?

个人理解:深入理解JAVA虚拟机一书中有这样一句描述:“为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知”。所以虚引用更多的是用于对象回收的监听,能做的功能如下:

1. 重要对象回收监听 进行日志统计
2. 系统gc监听 因为虚引用每次GC都会被回收,那么我们就可以通过虚引用来判断gc的频率,如果频率过大,内存使用可能存在问题,才导致了系统gc频繁调用

25.JVM结构:

jvm
在这里插入图片描述

26.Lambda表达式是怎么实现的?

类似于一个语法糖,在编译成字节码的过程中,多了一个对应 Lambda 表达式的私有静态方法 private static xxx,最后该方法会被调用执行。

27.Object类的常见方法:
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
28.throw和throws的区别?

throw代表动作,表示抛出一个异常的动作;throws代表一种状态,代表方法可能有异常抛出;throw用在方法实现中,而throws用在方法声明中;throw只能用于抛出一种异常,而throws可以抛出多个异常。

29.新生代对象何时进入老年代?
  1. 大对象直接进入老年代
  2. 经过多次minor gc依然在survivor区存活,进入老年代(大龄对象)
  3. 动态年龄判定:从年龄为1的对象开始累加,大于survivor区(这里指from或to)的一半时,记这个年龄和最大年龄阈值的最小值result,大于这个result的对象进入老年代
  4. 空间分配担保,minor gc后,survivor区不足以存放存活对象,通过空间分配担保进入老年代
30.Servlet生命周期

在这里插入图片描述

31.安全的hashmap
  • JAVA中线程安全的map有:Hashtable、synchronizedMap、ConcurrentHashMap
  • synchronizedMap:它其实就是加了一个对象锁,每次操作hashmap都需要先获取这个对象锁,这个对象锁有加了synchronized修饰,锁性能跟hashtable差不多
  • Hashtable:操作都用synchronized修饰
32.分析一个代码执行中jvm有什么变化

Person p=new Person("zhangsan",20);
该句话所做的事情:

  1. 在栈内存中,开辟main函数的空间,建立main函数的变量 p。
  2. 加载类文件:因为new要用到Person.class,所以要先从硬盘中找到Person.class类文件,并加载到内存中。

注意:在Person.class文件加载时,静态方法和非静态方法都会加载到方法区中,只不过要调用到非静态方法时需要先实例化一个对象

  1. 执行类中的静态代码块:如果有的话,对Person.class类进行初始化。
  2. 开辟空间:在堆内存中开辟空间,分配内存地址。
  3. 默认初始化:在堆内存中建立 对象的特有属性,并进行默认初始化。
  4. 显示初始化:对属性进行显示初始化。
  5. 构造代码块:执行类中的构造代码块,对对象进行构造代码块初始化。
  6. 构造函数初始化:对对象进行对应的构造函数初始化。
  7. 将内存地址赋值给栈内存中的变量p。
33.死锁的四个必要条件
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
   当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。 
34.ArrayList的remove方法问题
  1. 多线程下:触发fail-fast机制,抛异常
  2. 单线程下:也会出问题!例子
    在这里插入图片描述
    解释:删除元素“222”,当循环到下标为1的元素的的时候,发现此位置上的元素是“222”,此处元素应该删除,根据上图中的元素移动可知,在删除元素后面的所有元素都要向前移动一个位置,那么移动之后,原来下标为2的元素“222”,此时下标为1,这是在i = 1,时的循环操作,在下一次的循环中,i = 2,此时就遗漏了第二个元素“222”。

总结:for循环正向删除,会遗漏连续重复的元素。

35.为什么有两个survivor区?

因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生


36.什么是堆外内存

堆外内存

好处:

  1. 避免jvm垃圾回收,操作系统直接管理
  2. io不需要flush
37 concurrent

添加链接描述

38 Java排查问题的各种指令
  1. cpu占用:用top命令,看进程/线程谁消耗的多
    在这里插入图片描述

多线程与并发

1.说一下synchronized这个关键字
  • 我这边介绍了,说到了jdk1.6后的优化(锁升级),底层实现(monitor)
  • 底层实现注意点:字节码中包含一个 monitorenter 指令以及多个 monitorexit 指令。这是因为 Java 虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁。
  • 为什么重量级锁耗费性能? Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。也就是互斥锁(mutex)来实现的。这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。
  • 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
  • 轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  • 偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
  • monitor实现原理
  • hotspot虚拟机是用的ObjectMonitor,线程需要和monitor进行关联
2.什么是内存泄漏,如何解决内存泄漏?
  • 内存泄漏就是创建的对象引用没被回收,导致内存凭空丢掉了一部分
  • 解决策略:内存泄漏解决
  • 发生在堆区,正常来说我们是看不见的,要依赖第三方工具
3.如何合理设置线程池参数?:
  • 合理设置线程池参数
  • 分CPU密集型和IO密集型
  • 一般来说,CPU密集型的线程池线程数=CPU核数,不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率
  • IO密集型:最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)] (一般两倍以上,充分利用CPU)
  • 具体情况具体分析
4.volatile底层原理?:

volatile的作用,保证变量的可见性和禁止指令重排序:

volatile

  1. 在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。
  2. volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。1.将当前处理器缓存行数据刷写到系统主内存2.这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。
  3. Java 内存模型在 1.5 版本对 volatile 语义进行了增强,就是通过happens-before原则。
  4. 注意,指令重排序一定要看有没有数据依赖
5.happens-before原则:
  1. 在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。它真正要表达的是:前面一个操作的结果对后续操作是可见的。
  2. 所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句,所以等于是给了编译器优化的约束。
  3. happens-before原则包括volatile原则
  4. 一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作。因为会有指令重排序
  5. 实现:通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。
  6. happens-before原则只会禁止那些会出现问题的操作,而不会禁止那些不会出问题的重排序操作。
6.AQS:

AQS框架介绍

  • 模版方法,实现tryAcquire tryRelease / tryAcquireForShare tryReleaseForShare 其中一组或二者都有
  • volatile修饰的state变量表示资源,通过CAS自旋修改其值
  • 双向链表的队列,FIFIO存储线程
  • park()/unpark() 来唤醒
7.sleep()和wait()方法
  1. 二者都能让线程进入到暂停状态
  2. 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
  3. wait()是Object类的方法,sleep是Thread类的方法
  4. wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒
  5. wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放
8.为什么调用start方法而不是直接run()

调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

9.ThreadLocal原理
  1. 为什么看起来每个线程都是独立的呢,明明只声明了一个threadLocal变量?
  2. 因为每个thread内部都有一个叫做threadLocals的map,这个map叫做ThreadLoaclMap,它的key是ThreadLocal,val是Object
  3. 调用ThreadLoacl的get时,先要通过当前Thread获取到ThreadLocalMap,然后再调用这个Map的getEntry方法,传入的是ThreadLocal对象,然后拿到这个entry,取到的是每个线程独有的数据;set的时候也一样
  4. threadLoalMap
10.ThreadLocal的内存泄漏问题:

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

解决方案:

  1. 解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

在这里插入图片描述

11.什么是管程?
  1. 管程是一种概念,任何语言都可以通用。
  2. 在java中,每个加锁的对象都绑定着一个管程(监视器)
  3. 线程访问加锁对象,就是去拥有一个监视器的过程。如一个病人去门诊室看医生,医生是共享资源,门锁锁定医生,病人去看医生,就是访问医生这个共享资源,门诊室其实是监视器(管程)。
  4. 所有线程访问共享资源,都需要先拥有监视器。就像所有病人看病都需要先拥有进入门诊室的资格。
  5. 监视器至少有两个等待队列。一个是进入监视器的等待队列一个是条件变量对应的等待队列。后者可以有多个。就像一个病人进入门诊室诊断后,需要去验血,那么它需要去抽血室排队等待。另外一个病人心脏不舒服,需要去拍胸片,去拍摄室等待。
  6. 监视器要求的条件满足后,位于条件变量下等待的线程需要重新在门诊室门外排队,等待进入监视器。就像抽血的那位,抽完后,拿到了化验单,然后,重新回到门诊室等待,然后进入看病,然后退出,医生通知下一位进入。
  7. Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程利用OOP的封装特性解决了信号量在工程实践上的复杂性问题,因此java采用管理机制。
  8. jvm -> c++(管程) -> 字节码(monitorenter / monitorexit) -> 操作系统(mutex)

总结起来就是,管程就是一个对象监视器。任何线程想要访问该资源,就要排队进入监控范围。进入之后,接受检查,不符合条件,则要继续等待,直到被通知,然后继续进入监视器。

12.通过读写锁来实现缓存:
class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();

    private ReadWriteLock rwLock = new ReentrantReadWriteLock();


    public void put(String key, Object value) {
        rwLock.writeLock().lock(); //写锁加锁
        map.put(key, value);
        rwLock.writeLock().unlock();
    }

    public Object get(String key) {
        Object result = null;
        rwLock.readLock().lock();
        result = map.get(key);
        rwLock.readLock().unlock();
        return result;
    }
}

public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(num + "");
            }, String.valueOf(i)).start();
        }

    }


}
13.JMM内存模型
  • 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
  • 实际上本质是处理器速度过快,所以在多处理器环境下引入了高速缓存,先把运行需要的数据存到缓存,然后计算,最后返回内存。
14.单例模式中双检锁为什么要加volatile??
  1. singleton = new Singleton()这句初始化实际上包括三部分
memory = alloct();  //:1:分配对象的内存空间
ctorinstance(memory);  //2: 初始化对象
instace = memory;  //3:设置instace指向刚分配的内存地址

  1. 由于指令重排序,可能还没有初始化就设置了地址,这样不对
15.指令重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果;

  • 存在数据依赖关系的不允许重排序

重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

16.Happens-before原则:
  1. 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!

  2. 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

  3. volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

  4. 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

  5. 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

  7. 传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。

  8. 对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

17.Executors类提供的三种线程池解释
  1. Executors.newFixedThreadPool();

这个线程池有固定的工作线程数目,看构造器发现其核心线程数和最大线程数一样,伸缩时间为0,workQueue是链表形式的

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
  1. Executors.newSingleThreadExecutor();

一池一线程,核心和最大线程数都是1,也用的是链表工作队列

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
  1. Executors.newCachedThreadPool();

工作线程数目根据任务多少进行累加

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
18.线程之间共享的资源?

一.同一进程中的线程共享的资源

  1. 进程代码段

  2. 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)

  3. 进程打开的文件描述符

  4. 信号的处理器

  5. 进程的当前目录

  6. 进程用户ID与进程组ID。

二.这些独占的资源

进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。

  1. 线程ID
    每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

  2. 寄存器组的值
    由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

  3. 线程的堆栈
    堆栈是保证线程独立运行所必须的(在一个进程的线程共享堆区,而进程中的线程各自维持自己堆栈)。
    线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。

  4. 错误返回码
    由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。
    所以,不同的线程应该拥有自己的错误返回码变量。

  5. 线程的信号屏蔽码
    由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

  6. 线程的优先级
    由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。


三.计算机网络部分

1.TCP的四次挥手,为什么要有四次挥手?

为什么握手三次,挥手四次?

  • 这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,我们也未必全部数据都发送给对方了,所以我们不可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,我们的ACK和FIN一般都会分开发送。
2.TCP和UDP的区别
  • tcp面向链接,udp面向无连接
  • tcp可靠,udp不可靠—》为什么tcp可靠延伸
  • tcp开销大 首部20字节 udp只有8
  • tcp面向字节流,udp面向报文流

报文区别:报文格式区别

3.grpc采用http2,那么http1.0 1.1 2的区别是什么
  • 支持任意环境使用,支持物联网、手机、浏览器,HTTP2.0协议兼容好,支持多种终端
  • HTTP/2 安全性有保证
  • http协议区别
  • 但是HTTP是应用层协议,肯定比直接用一些传输层协议效率低,但是gRPC比较关心通用性和兼容性,对性能不是特别在乎
4.多路复用IO的三种实现 select / poll / epoll

知乎讲解多路IO

  • I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

fd:文件描述符,就是IO需要的文件
简单点说也就是 int fd = socket(AF_INET,SOCK_STREAM, 0); 函数socket()返回的就是这个描述符。在传输中我们都要使用这个惟一的ID来确定要往哪个链接上传输数据。

  1. select:select是三者当中最底层的,它的事件的轮训机制是基于比特位的。每次查询都要遍历整个事件列表。
    理解select,首先要理解select要处理的fd_set数据结构,每个select都要处理一个fd_set结构。fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有

  2. poll:可以认为poll是一个增强版本的select,因为select的比特位操作决定了一次性最多处理的读或者写事件只有1024个,而poll使用一个新的方式优化了这个模型。不用进行比特位的操作,而是对事件本身进行操作就行

  3. epoll:上述的select或者poll操作都需要轮询所有的候选队列逐一判断是否有事件,而且事件队列是直接暴露给调用者的,比如上面select的write_fd和poll的fds,这样复杂度高,而且容易误操作。epoll给出了一个新的模式,直接申请一个epollfd的文件,对这些进行统一的管理,初步具有了面向对象的思维模式

select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

在这里插入图片描述
epoll与poll的比较
epoll存储活跃的连接,每次只处理活跃的连接数量占比很小
poll是每次将所有的连接交给操作系统去遍历,找出活跃的连接,因此连接越多,耗时越长

epoll 如何实现只处理活跃连接
epoll实现了eventpoll数据结构
数据结构中rdlist将活跃连接存储在链表中,当网卡发送报文时,增加节点,当读取一个事件后,链表删除节点,需要得到活跃连接就只需要遍历链表
数据结构中rdr使用红黑树(自平衡二叉树)将事件存储,例如:当有读事件时,就新增节点,事件复杂度为logN

5.TCP如何保证可靠传输
  1. 数据包编号,顺序传输

  2. 拥塞控制 :TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为 1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口

  3. 在这里插入图片描述

  4. 重传机制: 停止等待ARQ对应自动重传,连续ARQ对应GOback-N协议

  5. 校验和:TCP 将保持它首部和数据的检验和。
    这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

  6. 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。

6.HTTP1.1相比1.0有什么改进?
  1. 1.1 版的最大变化,就是引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。
  2. 1.1 版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
  3. 1.1版还新增了许多动词方法:PUT、PATCH、HEAD、 OPTIONS、DELETE。另外,客户端请求的头信息新增了Host字段,用来指定服务器的域名。
7.HTTPS中的数字证书是什么?
  • 数字证书则是由证书认证机构(CA, Certificate Authority)对证书申请者真实身份验证之后,用CA的根证书对申请人的一些基本信息以及申请人的公钥进行签名(相当于加盖发证书机构的公章)后形成的一个数字文件。数字证书主要包含了CA认证过的公钥和拥有者的基本信息
  • 数字证书用来确保服务器的真实性,且服务器发给用户的公钥没有被篡改
8.TIME_WAIT出现原因?如何解决?

注意!此时关闭连接的是Server端,而不是客户端了

  1. 为什么有TIME_WAIT?因为client端需要保证发出的ack被服务端成功接收,否则会占用服务器资源
  2. 在高并发短连接的TCP服务器上,当服务器处理完请求后主动请求关闭连接,这样服务器上会有大量的连接处于TIME_WAIT状态,服务器维护每一个连接需要一个socket,也就是每个连接会占用一个文件描述符,而文件描述符的使用是有上限的,如果持续高并发,会导致一些连接失败。
  3. 可设置套接字选项为SO_REUSEADDR,该选项的意思是,告诉操作系统,如果端口忙,但占用该端口TCP连接处于TIME_WAIT状态,并且套接字选项为SO_REUSEADDR,则该端口可被重用。如果TCP连接处于其他状态,依然返回端口被占用。该选项对服务程序重启非常有用。
9.TCP三次握手能否携带数据包

第一次和第二次是不可以携带数据的,但是第三次是可以携带数据的。

  • 假如第一次握手可以携带数据的话,那对于服务器是不是太危险了,有人如果恶意攻击服务器,每次都在第一次握手中的SYN报文中放入大量数据。而且频繁重复发SYN报文,服务器会花费很多的时间和内存空间去接收这些报文。

  • 第三次握手,此时客户端已经处于ESTABLISHED状态。对于客户端来说,他已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的。所以也就可以携带数据了。

10.TCP粘包的解决策略

粘包问题的最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪。我们通过使用某种方案给出边界,例如:

  1. 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
  2. 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
  3. 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。
  4. 使用更加复杂的应用层协议。
11.HTTP无状态的含义?
  1. 每一个HTTP请求是独立的,服务不能鉴别出两个请求是不是来自同一个用户。
  2. Web服务没有在内存中保留请求的任何内容(只有磁盘的信息才能在请求之间共享)。
12.GET和POST的区别

详细区别:

  1. get参数通过url传递,post放在request body中。

  2. get请求在url中传递的参数是有长度限制的,而post没有。

  3. get比post更不安内全,因为参容数直接暴露在url中,所以不能用来传递敏感信息。

  4. get请求只能进行url编码,而post支持多种编码方式

  5. get请求会浏览器主动cache,而post支持多种编码方式。

  6. get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。

  7. GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

  8. GET产生一个TCP数据包;POST产生两个TCP数据包。

拓展资料:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

13.POST请求头

请求头部常见字段


14.OSI7层模型

在这里插入图片描述

15.什么是ARQ

ARQ,自动重传请求,是OSI模型中数据链路层和传输层的一种纠错协议。通过确认和超时的机制来保证信息传输的可靠性。

  1. 停止等待ARQ协议:每次发完一个分组就停止发送,等待对方返回ACK,如果超时没有收到,那么就重发。缺点是信道利用率低,优点是简单
  2. 连续ARQ协议:发送方维持着一个一定大小的发送窗口,位于发送窗口内的所有分组都可连续发送出去,而中途不需要等待对方的确认。这样信道的利用率就提高了。而发送方每收到一个确认就把发送窗口向前滑动一个分组的位置。
    连续ARQ也分为累计确认和回退N两种,一个是确认最后一个,一种是逐个确认如果第三个丢了,就从第三个开始后面重新发。
    在这里插入图片描述

16.HTTPS原理

在这里插入图片描述
第一步、客户端发送请求,服务器将证书发送给客户端,证书的本质是第三方CA的私钥加密的内容,其内容是服务器的公钥。

第二步、客户端接收到证书后,用操作系统和浏览器内置的CA公钥去匹配验证证书,如果能解密,说明请求的是目标网站,不是中间人。

第三步、用CA公钥解密证书,并将服务器公钥解密出来。到这一步,客户端安全的拿到了服务器端的公钥。

第四步、生成随机数,用服务器公钥加密随机数发送到服务器端。

第五步、服务器端用服务器私钥解密信息,得到随机数

四.操作系统

1.什么是虚拟内存

虚拟内存

  • 虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。
    把内存看成一个字符数组,虚拟内存就是为了解决分配,维护的痛点
2.线程怎么进行同步的?/线程之间如何通信?
  1. 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
  3. 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操
3.操作系统的内存管理?
  1. 块式管理 : 远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
  2. 页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
  3. 段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
  4. 段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
4.分页机制和分段机制区别?

共同点 :

  1. 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
  2. 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
    区别 :
  3. 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
  4. 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
5.进程之间如何进行通信:
  1. 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
  2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  5. 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
  6. 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
  7. 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
6.用户态和内核态的切换?

二者的区别:

  1. 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
  2. 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

怎么切换?

  1. 系统调用: 系统调用是用户态主动要求切换到内核态的一种方式, 用户应用程序通过操作系统调用内核为上层应用程序开放的接口来执行程序。其实就是个接口

  2. 异常: 当cpu在执行用户态的应用程序时,发生了某些不可知的异常。 于是当前用户态的应用进程切换到处理此异常的内核的程序中去。

  3. 硬件设备的中断: 当硬件设备完成用户请求后,会向cpu发出相应的中断信号, 这时cpu会暂停执行下一条即将要执行的指令,转而去执行与中断信号对应的应用程序, 如果先前执行的指令是用户态下程序的指令,那么这个转换过程也是用户态到内核台的转换。

7.虚拟内存和物理内存是怎么映射的?

在这里插入图片描述

内存被分成固定大小的页(Page),然后再通过虚拟内存地址(Virtual Address) 到物理内存地址(Physical Address) 的地址转换(Address Translation),才能访问实际存放数据的物理内存位置。而我们的程序看到的内存地址,都是虚拟内存地址。

1.简单页表: 页表(Page Table):想要把虚拟内存地址,映射到物理内存地址,最直观的办法,就是来建一张映射表。这个映射表,能够实现虚拟内存里面的页,到物理内存里面的页的映射。这个映射表,在计算机里面,就叫作页表。实际上是切分为页号和偏移量,到页表中找到页号对应的地址,再加上偏移量就行了
2. 多级页表
多级页表就像一个多叉树的数据结构,所以我们常常称它为页表树(Page Table Tree)。这种数据结构其实和 B+ 树类似,允许一个结点存储多条记录,并且非叶子结点只存储索引,只有叶子结点存储数据。

使用多级页表后,同样的虚拟内存地址,偏移量的部分和上面简单页表一样不变,而原先的页号部分需要拆分成多段。4级—3级—2级—1级—物理地址
在这里插入图片描述
3.TLB
地址变换高速缓冲(Translation-Lookaside Buffer,TLB):是 CPU 中的一块缓存芯片,这块缓存存放了之前已经进行过地址转换的查询结果。有了 TLB ,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在 TLB 查询结果,而不需要多次访问内存来完成一次转换。

在这里插入图片描述


9.线程同步

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资 源使用冲突。有下面这几种方式:

  1. 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。 因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访 问此资源的最大线程数量
  3. 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较。

10.操作系统的内存管理主要做什么

内存的分配与回收(申请和释放比如malloc,和free),地址转换(逻辑->物理)


11.操作系统的内存管理机制

简单分为连续分配管理方式非连续分配管理方式这两种。连续分配管理方式是指为一个用户程 序分配一个连续的内存空间,常⻅的如 块式管理 。同样地,非连续分配管理方式允许一个程序 使用的内存分布在离散或者说不相邻的内存中,常⻅的如⻚式管理 和 段式管理。


12.CPU的多级缓存是什么

CPU多级缓存

这一个也不错,还介绍了MESI:CPU多级缓存


13.进程切换会发生什么

进程切换一定发生在中断/异常/系统调用处理过程中,常见的有以下情况:

  1. 时间片中断、IO中断后 更改优先级进程;(导致被中断进程进入就绪态);
  2. 阻塞式系统调用、虚拟地址异常;(导致被中断进程进入等待态)
  3. 终止用系统调用、不能继续执行的异常;(导致被中断进程进入终止态)
    举例说明:
  • 时钟中断:操作系统确定当前正在运行的进程的执行时间是否已经超过了最大允许时间段,如果超过了,进程必须切换到就绪态,调度另一个进程;
  • I/O中断: 操作系统确定是否发生了I/O活动。如果I/O活动是一个或多个进程正在等待的事件,操作系统就把所有相应的阻塞态转换到就绪态,操作系统必须决定继续执行当前处于运行态的进程,还是让具有高优先级的就绪态进程抢占这个进程。
  • 虚拟地址异常(内存失效):处理器访问一个虚拟内存地址,且此地址单元不在内存中,操作系统必须从外存中把包含这个引用的 内存块(页或段)调入内存中。在发出调入内存块的I/O请求之后,操作系统可以会执行一个进程切换,以恢复另一个进程的执行,发生内存失效的进程被置为阻塞态,当想要的块调入内存中时,该进程被置为就绪态;
  • 对于陷阱:操作系统确定错误或异常条件是否是致命的。如果是,当前正在运行的进程被转换到退出态,并发生进程切换;如果不是,操作系统 的动作取决于错误的种类 和操作系统的设计,其行为可以是试图恢复或通知用户,操作系统可能会进行一次进程切换或者继续执行当前正在运行的进程。
  • 最后操作系统可能被来自正在执行的程序的系统调用激活。例如,一个用户进程正在运行,并且正在执行一条请求I/O操作的指令,如打开文件,这个调用导致转移到作为操作系统代码一部分的一个例程上进行。通常,使用系统调用会导致把用户 线程置为阻塞态;

14.什么是文件描述符

介绍IO多路复用

文件描述符:Linux 系统中,把一切都看做是文件(一切皆文件),当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符

在linux操作系统中,每一个进程中都有一个文件描述符表,它是一个指针数组,系统默认初始化了数组的前3位。第0位指向标准的输入流(一般是键盘),第1位指向标准的输出流(一般是显示器),第2位指向标准的错误流(一般是也显示器)。

现在如果有一个进程中只打开了一个 hello.txt 文件,那么这个进程的文件描述符表的第3位就是指向这个 hello.txt 的指针。之后如果该进程创建了一个socket,那么这个文件描述符表的第4位就是指向这个socket的指针,因为在linux中一切皆文件,socket也是一个文件。我们所说的文件描述符就是进程中这个数组的下标,因此他也可以说是一个索引。

例子:我们的80端口创建一个socket,socket也是文件,那么这个进程的文件描述符表中又加1,那么一直加到1024,应为我们的系统限制一个进程最多只能打开1024个文件。(这就是为啥select()最多轮询1024个客户端)

15.零拷贝是什么

零拷贝


16.进程和线程的区别

参考:线程和进程的区别

  1. 进程,在一定的环境下,把静态的程序代码运行起来,通过使用不同的资源,来完成一定的任务。比如说,进程的环境包括环境变量,进程所掌控的资源,有中央处理器,有内存,打开的文件,映射的网络端口等等。一个系统中,有很多进程,它们都会使用内存。为了确保内存不被别人使用,每个进程所能访问的内存都是圈好的。一人一份,谁也不干扰谁。还有内存的分页,虚拟地址我就不深入探讨了。这里给大家想强调的就是,进程需要管理好它的资源。
  2. 线程作为进程的一部分,扮演的角色就是怎么利用中央处理器去运行代码。这其中牵扯到的最重要资源的是中央处理器和其中的寄存器,和线程的栈(stack)。这里想强调的是,线程关注的是中央处理器的运行,而不是内存等资源的管理。
17.什么是中断

操作系统中断

;