贝壳找房

贝壳一面

  1. ​ collection继承关系,有哪些类
  2. ​ 接口和抽象类,设计模式
  3. ​ jvm类加载
  4. ​ volatile,禁止指令重排序jdk1.8的变化
  5. ​ hashmap底层,是否能从红黑树转回链表
  6. ​ spring动态代理,jdk动态代理能否用抽象类
  7. ​ mybatis执行流程
  8. ​ 锁、多线程
  9. ​ 数据库、redis底层
  10. ​ mysql隔离级别、解决哪些问题,幻读怎么处理
  11. ​ 索引
  12. ​ 数据库场景题、大数据分页查询等
  13. ​ 代码:数组两数之和

贝壳二面

  1. ​ 计算机网络七层协议,url请求过程,IMCP、ARP
  2. ​ 多线程消费者、生产者模型手写
  3. ​ volatile、cas原理、底层实现
  4. ​ jdk1.6 sychronized优化、原理、monitor原理
  5. ​ jvm垃圾回收、卡表机制
  6. ​ 操作系统,页面置换算法,分页的原理、逻辑、物理地址、偏移量
  7. ​ 青蛙跳台阶算法
  8. ​ 数据库底层(不会)
  9. ​ 其他几不住了

作者:ZYf求offer
链接:https://www.nowcoder.com/discuss/427227
来源:牛客网


collection继承关系,有哪些类

Map接口和Collection接口是所有集合框架的父接口:

Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
img

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap。


接口和抽象类

抽象类和接口的对比

从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

相同点

接口和抽象类都不能实例化
都位于继承的顶端,用于被其他实现或继承
都包含抽象方法,其子类都必须覆写这些抽象方法

不同点
image-20200519143913748
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。

现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。

接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:

行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。


jvm类加载

类的生命周期

一个类的完整生命周期如下:

https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/类加载过程-完善.png

类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/类加载过程.png

加载

类加载过程的第一步,主要完成下面3件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面的文章中单独介绍到。

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

验证

https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/验证阶段.png

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。

基本数据类型的零值:

https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化

初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。

对于<clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。

对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化:

  1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。

卸载

卸载类即该类的Class对象被GC。

卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被GC

所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,jdk自带的BootstrapClassLoader,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。


volatile是Java虚拟机提供的轻量级的同步机制

首先应该了解三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

在了解的过程中,首先应该了解JMM(Java内存模型):

主要有三大特性:
1、保证可见性
2、不保证原子性
3、禁止指令重排序
如果要深入了解volatile以及三大特性,需要先从JMM谈起

JMM ( Java Memory Model Java内存模型)
JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范,通过这组规范,定义了程序中的各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定
1、线程解锁前,必须要把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁

JVM运行程序的实体是线程,而每个线程创建时,JVM都会为它创建一个工作内存(也叫做栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但是线程对变量的操作(读取、赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存的变量副本,不同线程之间无法访问对方的工作内存,线程之间的通信(传值)必须通过主内存来完成,简要访问过程如下图
线程获取变量简图

保证可见性

image-20200430164326463

硬盘速度<内存速度<CPU,这时就需要缓存处理。

image-20200430164532976

1、可见性

当没有加上volatile关键字时:

package interview;

import java.util.Timer;
import java.util.concurrent.TimeUnit;

/**
 * @author SoraNimi
 * @className volatile_practice
 * @email 434624198@qq.com
 * @github https://github.com/SoraNimi
 * 验证volatile可见性
 * 1.1
 */
class MyData {
    int number = 0;

    public void addTO60() {
        this.number = 60;
    }
}

/**
 * 验证volatile可见性
 * 1.1  假如int number=0;number变量之前根本没有添加volatile关键字修饰
 */
public class volatile_practice {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            //暂停一会儿线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTO60();
            System.out.println(Thread.currentThread().getName() + "\t updated number value: " + myData.number);
        }, "AAA").start();
        //第二个线程就是我们的main线程
        while (myData.number == 0) {
            //main线程一直循环
        }
        System.out.println(Thread.currentThread().getName() + " is over");
    }
}

结果:

AAA     come in
AAA     updated number value: 60

当加上volatile关键字时:

package interview;

import java.util.Timer;
import java.util.concurrent.TimeUnit;

/**
 * @author SoraNimi
 * @className volatile_practice
 * @email 434624198@qq.com
 * @github https://github.com/SoraNimi
 * 验证volatile可见性
 * 1.1
 */
class MyData {
    volatile int number = 0;

    public void addTO60() {
        this.number = 60;
    }
}

/**
 * 验证volatile可见性
 * 1.1  假如int number=0;number变量之前根本没有添加volatile关键字修饰
 */
public class volatile_practice {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            //暂停一会儿线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTO60();
            System.out.println(Thread.currentThread().getName() + "\t updated number value: " + myData.number);
        }, "AAA").start();
        //第二个线程就是我们的main线程
        while (myData.number == 0) {
            //main线程一直循环
        }
        System.out.println(Thread.currentThread().getName() + " is over");
    }
}

结果:

AAA come in
AAA updated number value: 60
main is over

image-20200430183314794


不保证原子性

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

package com.doinb.volatiles;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author doinb
 *
 *  volatile是java虚拟机提供的轻量级的同步机制(轻量级的synchronized),基本遵守JMM规范主要如下:
 *  1 保证可见性 -- 某一个线程修改了主物理内存的值后,需立即通知其他线程,称为可见性。
 *  2 不保证原子性
 *  3 禁止指令重排
 *
 *  JMM(java内存模型Java Memory Model)它描述的是一组规范或规则。
 *
 *  JMM关于同步的规定:
 *  1 线程解锁前,必须把共享变量的值刷新回主内存。
 *  2 线程加锁前,必须读取主内存的最新值到自己的工作内存。
 *  3 加锁解锁是统一把锁。
 *
 */
class MyData{
    volatile int number = 0;

    public void addT60(){
        this.number = 60;
    }

    // 请注意,number前面加了volatile关键字修饰,但是volatile不保证原子性,出现写覆盖; 添加 synchronized 可保证原子性
    public void addPlusPlus(){
        number++;
    }

}

public class VolatileDemo {
    /**
     *  1 验证 volatile 的可见性
     *      1.1 加入 int number = 0,number变量之前根本没有添加volatile关键字修饰,没有可见性
     *      1.2 添加了 volatile,可以解决可见性问题。
     *
     *  2 验证 volatile 不保证原子性
     *      2.1 原子性指的是什么意思?
     *      不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
     *      2.2 volatile 不保证原子性
     *      2.3 why 查看jvm字节码
     *      2.4 如何解决原子性?   #加synchronized,杀鸡用牛刀; # AtomicInteger可解决
     *
     *
     *
     */
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                }
            },"doinb_"+i).start();
        }

        // 需要等到上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少?
        while (Thread.activeCount() > 2){ // 一个程序,默认有两个线程,一个是main线程,一个是gc线程。
            Thread.yield(); // 主线程不执行
        }
        System.out.println(Thread.currentThread().getName()+"\t int type, finally number value: "+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t atomicInteger type, finally number value: "+myData.atomicInteger);
    }


    // 可以保证可见性,及时通知其他线程,主物理内存的值已经被修改。
    private static void seeOkByVolatile() {
        MyData myData = new MyData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT60();
            System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);
        },"AAA").start();

        while (myData.number == 0){
            // main线程就一直在这里等待循环,直到number值不再等于零;
        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over, main get number value: "+myData.number);
    }
}

结果:

main     int type, finally number value: 18295

证明number++在多线程下是非线程安全的,如何不加synchronized解决?

使用AtomicInteger

package com.doinb.volatiles;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author doinb
 *
 *  volatile是java虚拟机提供的轻量级的同步机制(轻量级的synchronized),基本遵守JMM规范主要如下:
 *  1 保证可见性 -- 某一个线程修改了主物理内存的值后,需立即通知其他线程,称为可见性。
 *  2 不保证原子性
 *  3 禁止指令重排
 *
 *  JMM(java内存模型Java Memory Model)它描述的是一组规范或规则。
 *
 *  JMM关于同步的规定:
 *  1 线程解锁前,必须把共享变量的值刷新回主内存。
 *  2 线程加锁前,必须读取主内存的最新值到自己的工作内存。
 *  3 加锁解锁是统一把锁。
 *
 */
class MyData{
    volatile int number = 0;

    public void addT60(){
        this.number = 60;
    }

    // 请注意,number前面加了volatile关键字修饰,但是volatile不保证原子性,出现写覆盖; 添加 synchronized 可保证原子性
    public void addPlusPlus(){
        number++;
    }


    // 创建一个原子类,默认初始值为0 -- 引入 CAS 自旋锁概念!非常重要
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement(); //类似 i++
    }
}

public class VolatileDemo {
    /**
     *  1 验证 volatile 的可见性
     *      1.1 加入 int number = 0,number变量之前根本没有添加volatile关键字修饰,没有可见性
     *      1.2 添加了 volatile,可以解决可见性问题。
     *
     *  2 验证 volatile 不保证原子性
     *      2.1 原子性指的是什么意思?
     *      不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
     *      2.2 volatile 不保证原子性
     *      2.3 why 查看jvm字节码
     *      2.4 如何解决原子性?   #加synchronized,杀鸡用牛刀; # AtomicInteger可解决
     *
     *
     *
     */
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                    myData.addMyAtomic();
                }
            },"doinb_"+i).start();
        }

        // 需要等到上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少?
        while (Thread.activeCount() > 2){ // 一个程序,默认有两个线程,一个是main线程,一个是gc线程。
            Thread.yield(); // 主线程不执行
        }
        System.out.println(Thread.currentThread().getName()+"\t int type, finally number value: "+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t atomicInteger type, finally number value: "+myData.atomicInteger);
    }


    // 可以保证可见性,及时通知其他线程,主物理内存的值已经被修改。
    private static void seeOkByVolatile() {
        MyData myData = new MyData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT60();
            System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);
        },"AAA").start();

        while (myData.number == 0){
            // main线程就一直在这里等待循环,直到number值不再等于零;
        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over, main get number value: "+myData.number);
    }
}

结果:

main     int type, finally number value: 19619
main     atomicInteger type, finally number value: 20000

参考:https://blog.csdn.net/zezezuiaiya/article/details/81456060


禁止指令重排序jdk1.8的变化

?目前在网络上没搜索到


hashmap底层实现原理

https://blog.csdn.net/tuke_tuke/article/details/51588156?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

是否能从红黑树转回链表

没有找到?我认为是可以的,只要到了阈值就可以


spring动态代理,jdk动态代理能否用抽象类

??


mybatis执行流程

在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图

1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。

2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。

4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。

5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。

6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。

7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。

8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。


大数据分页查询

https://blog.csdn.net/itguangit/article/details/92577439


url请求过程

概述

从输入URL到页面加载的主干流程如下:

1、浏览器构建HTTP Request请求

2、网络传输

3、服务器构建HTTP Response 响应

4、网络传输

5、浏览器渲染页面

image-20200520160822446

构建请求

1、应用层进行DNS解析

通过DNS将域名解析成IP地址。在解析过程中,按照浏览器缓存系统缓存路由器缓存ISP(运营商)DNS缓存根域名服务器顶级域名服务器主域名服务器的顺序,逐步读取缓存,直到拿到IP地址

这里使用DNS预解析,可以根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度

2、应用层生成HTTP请求报文

接着,应用层生成针对目标WEB服务器的HTTP请求报文,HTTP请求报文包括起始行、首部和主体部分

如果访问的google.com,则起始行可能如下

GET <https://www.google.com/> HTTP/1.1

首部包括域名host、keep-alive、User-Agent、Accept-Encoding、Accept-Language、Cookie等信息,可能如下

Host: www.google.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
X-Client-Data: CKm1yQEIhbbJAQijtskBCMG2yQEIqZ3KAQioo8oB
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

首部和主体内容之间有一个回车换行(CRLF),主体内容即要传输的内容。如果是get请求,则主体内容为空

3、传输层建立TCP连接

传输层传输协议分为UDP和TCP两种

UDP是无连接的协议,而TCP是可靠的有连接的协议,主要表现在:接收方会对收到的数据进行确认、发送方会重传接收方未确认的数据、接收方会将接收到数据按正确的顺序重新排序,并删除重复的数据、提供了控制拥挤的机制

由于HTTP协议使用的是TCP协议,为了方便通信,将HTTP请求报文按序号分为多个报文段(segment),并对每个报文段进行封装。使用本地一个大于1024以上的随机TCP源端口(这里假设是1030)建立到目的服务器TCP80号端口(HTTPS协议对应的端口号是443)的连接,TCP源端口和目的端口被加入到报文段中,学名叫协议数据单元(Protocol Data Unit, PDU)。因TCP是一个可靠的传输控制协议,传输层还会加入序列号、确认号、窗口大小、校验和等参数,共添加20字节的头部信息

image-20200520160845605

TCP协议是面向连接的,所以它在开始传输数据之前需要先建立连接。要建立或初始化一个连接,两端主机必须同步双方的初始序号。同步是通过交换连接建立数据分段和初始序号来完成的,在连接建立数据分段中包含一个SYN(同步)的控制位。同步需要双方都发送自己的初始序号,并且发送确认的ACK。此过程就是三次握手

第一次握手:主机A发往主机B,主机A的初始序号是X,设置SYN位,未设置ACK位

第二次握手:主机B发往主机A,主机B的初始序号是Y,确认号(ACK)是X+1,X+1确认号暗示己经收到主机A发往主机B的同步序号。设置SYN位和ACK位

第三次握手:主机A发往主机B,主机A的序号是X+1,确认号是Y+1,Y+1确认号暗示已经收到主机B发往主机A的同步序号。设置ACK位,未设置SYN位

三次握手解决的不仅仅有序号问题,还解决了包括窗口大小、MTU(Maximum Transmission Unit,最大传输单元),以及所期望的网络延时等其他问题

image-20200520160903715

构建TCP请求会增加大量的网络时延,常用的优化方式如下所示

(1)资源打包,合并请求

(2)多使用缓存,减少网络传输

(3)使用keep-alive建立持久连接

(4)使用多个域名,增加浏览器的资源并发加载数,或者使用HTTP2的管道化连接的多路复用技术

4、网络层使用IP协议来选择路线

处理来自传输层的数据段segment,将数据段segment装入数据包packet,填充包头,主要就是添加源和目的IP地址,然后发送数据。在数据传输的过程中,IP协议负责选择传送的路线,称为路由功能

image-20200520160920048

5、数据链路层实现网络相邻结点间可靠的数据通信

为了保证数据的可靠传输,把数据包packet封装成帧(Frame),并按顺序传送各帧。由于物理线路的不可靠,发出的数据帧有可能在线路上出错或丢失,于是为每个数据分块计算出CRC(循环冗余检验),并把CRC添加到帧中,这样接收方就可以通过重新计算CRC来判断数据接收的正确性。一旦出错就重传

将数据包packet封装成帧(Frame),包括帧头和帧尾。帧尾是添加被称做CRC的循环冗余校验部分。帧头主要是添加数据链路层的地址,即数据链路层的源地址和目的地址,即网络相邻结点间的源MAC地址和目的MAC地址

6、物理层传输数据

数据链路层的帧(Frame)转换成二进制形式的比特(Bit)流,从网卡发送出去,再把比特转换成电子、光学或微波信号在网络中传输

【总结】

上面的6个步骤可总结为:DNS解析URL地址、生成HTTP请求报文、构建TCP连接、使用IP协议选择传输路线、数据链路层保证数据的可靠传输、物理层将数据转换成电子、光学或微波信号进行传输

image-20200520160946550

网络传输

从客户机到服务器需要通过许多网络设备, 一般地,包括集线器、交换器、路由器等

【集线器】

集线器是物理层设备,比特流到达集线器后,集线器简单地对比特流进行放大,从除接收端口以外的所有端口转发出去

【交换机】

交换机是数据链路层设备,比特流到达交换机,交换机除了对比特流进行放大外,还根据源MAC地址进行学习,根据目的MAC地址进行转发。交换机根据数据帧中的目的MAC地址査询MAC地址表,把比特流从对应的端口发送出去

【路由器】

路由器是网络层设备,路由器收到比特流,转换成帧上传到数据链路层,路由器比较数据帧的目的MAC地址,如果有与路由器接收端口相同的MAC地址,则路由器的数据链路层把数据帧进行解封装,然后上传到路由器的网络层,路由器找到数据包的目的IP地址,并查询路由表,将数据从入端口转发到出端口。接着在网络层重新封装成数据包packet,下沉到数据链路层重新封装成帧frame,下沉到物理层,转换成二进制比特流,发送出去

image-20200520161007716

服务器处理及反向传输

服务器接收到这个比特流,把比特流转换成帧格式,上传到数据链路层,服务器发现数据帧中的目的MAC地址与本网卡的MAC地址相同,服务器拆除数据链路层的封装后,把数据包上传到网络层。服务器的网络层比较数据包中的目的IP地址,发现与本机的IP地址相同,服务器拆除网络层的封装后,把数据分段上传到传输层。传输层对数据分段进行确认、排序、重组,确保数据传输的可靠性。数据最后被传到服务器的应用层

HTTP服务器,如nginx通过反向代理,将其定位到服务器实际的端口位置,如8080。比如,8080端口对应的是一个NodeJS服务,生成响应报文,报文主体内容是google首页的HTML页面

接着,通过传输层、网络层、数据链路层的层层封装,最终将响应报文封装成二进制比特流,并转换成其他信号,如电信号到网络中传输

反向传输的过程与正向传输的过程类似,就不再赘述

浏览器渲染

客户机接受到二进制比特流之后,把比特流转换成帧格式,上传到数据链路层,客户机发现数据帧中的目的MAC地址与本网卡的MAC地址相同,拆除数据链路层的封装后,把数据包上传到网络层。网络层比较数据包中的目的IP地址,发现与本机的IP地址相同,拆除网络层的封装后,把数据分段上传到传输层。传输层对数据分段进行确认、排序、重组,确保数据传输的可靠性。数据最后被传到应用层

1、如果HTTP响应报文是301或302重定向,则浏览器会相应头中的location再次发送请求

2、浏览器处理HTTP响应报文中的主体内容,首先使用loader模块加载相应的资源

loader模块有两条资源加载路径:主资源加载路径和派生资源加载路径。主资源即google主页的index.html文件 ,派生资源即index.html文件中用到的资源

主资源到达后,浏览器的Parser模块解析主资源的内容,生成派生资源对应的DOM结构,然后根据需求触发派生资源的加载流程。比如,在解析过程中,如果遇到img的起始标签,会创建相应的image元素HTMLImageElement,接着依据img标签的内容设置HTMLImageElement的属性。在设置src属性时,会触发图片资源加载,发起加载资源请求

这里常见的优化点是对派生资源使用缓存

3、使用parse模块解析HTML、CSS、Javascript资源

【解析HTML】

HTML解析分为可以分为解码、分词、解析、建树四个步骤

(1)解码:将网络上接收到的经过编码的字节流,解码成Unicode字符

(2)分词:按照一定的切词规则,将Unicode字符流切成一个个的词语(Tokens)

(3)解析:根据词语的语义,创建相应的节点(Node)

(4)建树:将节点关联到一起,创建DOM树

【解析CSS】

页面中所有的CSS由样式表CSSStyleSheet集合构成,而CSSStyleSheet是一系列CSSRule的集合,每一条CSSRule则由选择器CSSStyleSelector部分和声明CSSStyleDeclaration部分构成,而CSSStyleDeclaration是CSS属性和值的Key-Value集合

CSS解析完毕后会进行CSSRule的匹配过程,即寻找满足每条CSS规则Selector部分的HTML元素,然后将其Declaration声明部分应用于该元素。实际的规则匹配过程会考虑到默认和继承的CSS属性、匹配的效率及规则的优先级等因素

【解析JS】

JavaScript一般由单独的脚本引擎解析执行,它的作用通常是动态地改变DOM树(比如为DOM节点添加事件响应处理函数),即根据时间(timer)或事件(event)映射一棵DOM树到另一棵DOM树

简单来说,经过了Parser模块的处理,浏览器把页面文本转换成了一棵节点带CSS Style、会响应自定义事件的Styled DOM树

4、构建DOM树、Render树及RenderLayer树

浏览器的解析过程就是将字节流形式的网页内容构建成DOM树、Render树及RenderLayer树的过程

使用parse解析HTML的过程,已经完成了DOM树的构建,接下来构建Render树

【Render树】

Render树用于表示文档的可视信息,记录了文档中每个可视元素的布局及渲染方式

RenderObject是Render树所有节点的基类,作用类似于DOM树的Node类。这个类存储了绘制页面可视元素所需要的样式及布局信息,RenderObject对象及其子类都知道如何绘制自己。事实上绘制Render树的过程就是RenderObject按照一定顺序绘制自身的过程

DOM树上的节点与Render树上的节点并不是一一对应的。只有DOM树的根节点及可视节点才会创建对应的RenderObject节点

【Render Layer树】

Render Layer树以层为节点组织文档的可视信息,网页上的每一层对应一个Render Layer对象。RenderLayer树可以看作Render树的稀疏表示,每个RenderLayer树的节点都对应着一棵Render树的子树,这棵子树上所有Render节点都在网页的同一层显示

RenderLayer树是基于RenderObject树构建的,满足一定条件的RenderObject才会建立对应的RenderLayer节点

下面是RenderLayer节点的创建条件:

(1)网页的root节点

(2)有显式的CSS position属性(relative,absolute,fixed)

(3)元素设置了transform

(4)元素是透明的,即opacity不等于1

(5)节点有溢出(overflow)、alpha mask或者反射(reflection)效果。

(6)元素有CSS filter(滤镜)属性

(7)2D Canvas或者WebGL

(8)Video元素

5、布局和渲染

布局就是安排和计算页面中每个元素大小位置等几何信息的过程。HTML采用流式布局模型,基本的原则是页面元素在顺序遍历过程中依次按从左至右、从上至下的排列方式确定各自的位置区域

简单情况下,布局可以顺序遍历一次Render树完成,但也有需要迭代的情况。当祖先元素的大小位置依赖于后代元素或者互相依赖时,一次遍历就无法完成布局,如Table元素的宽高未明确指定而其下某一子元素Tr指定其高度为父Table高度的30%的情况

Paint模块负责将Render树映射成可视的图形,它会遍历Render树调用每个Render节点的绘制方法将其内容显示在一块画布或者位图上,并最终呈现在浏览器应用窗口中成为用户看到的实际页面

主要绘制顺序如下:

(1)背景颜色

(2)背景图片

(3)边框

(4)子呈现树节点

(5)轮廓

6、硬件加速

开启硬件渲染,即合成加速,会为需要单独绘制的每一层创建一个GraphicsLayer

硬件渲染是指网页各层的合成是通过GPU完成的,它采用分块渲染的策略,分块渲染是指:网页内容被一组Tile覆盖,每块Tile对应一个独立的后端存储,当网页内容更新时,只更新内容有变化的Tile。分块策略可以做到局部更新,渲染效率更高

一个Render Layer对象如果需要后端存储,它会创建一个Render Layer Backing对象,该对象负责Renderlayer对象所需要的各种存储。如果一个Render Layer对象可以创建后端存储,那么将该RenderLayer称为合成层(Compositing Layer)

如果一个Render Layer对象具有以下的特征之一,那么它就是合成层:

(1)RenderLayer具有CSS 3D属性或者CSS透视效果。

(2)RenderLayer包含的RenderObject节点表示的是使用硬件加速的视频解码技术的HTML5 ”video”元素。

(3) RenderLayer包含的RenderObject节点表示的是使用硬件加速的Canvas2D元素或者WebGL技术。

(4)RenderLayer使用了CSS透明效果的动画或者CSS变换的动画。

(5)RenderLayer使用了硬件加速的CSSfilters技术。

(6)RenderLayer使用了剪裁(clip)或者反射(reflection)属性,并且它的后代中包括了一个合成层。

(7)RenderLayer有一个Z坐标比自己小的兄弟节点,该节点是一个合成层

最终的渲染流程如下所示:

image-20200520161036197

【重绘和回流】

重绘和回流是在页面渲染过程中非常重要的两个概念。页面生成以后,脚本操作、样式表变更,以及用户操作都可能触发重绘和回流

回流reflow是firefox里的术语,在chrome中称为重排relayout

回流是指窗口尺寸被修改、发生滚动操作,或者元素位置相关属性被更新时会触发布局过程,在布局过程中要计算所有元素的位置信息。由于HTML使用的是流式布局,如果页面中的一个元素的尺寸发生了变化,则其后续的元素位置都要跟着发生变化,也就是重新进行流式布局的过程,所以被称之为回流

前面介绍过渲染引擎生成的3个树:DOM树、Render树、Render Layer树。回流发生在Render树上。常说的脱离文档流,就是指脱离渲染树Render Tree

重绘是指当与视觉相关的样式属性值被更新时会触发绘制过程,在绘制过程中要重新计算元素的视觉信息,使元素呈现新的外观

由于元素的重绘repaint只发生在渲染层 render layer上。所以,如果要改变元素的视觉属性,最好让该元素成为一个独立的渲染层render layer

下面列举一些减少回流次数的方法

(1)不要一条一条地修改DOM样式,而是修改className或者修改style.cssText

(2)在内存中多次操作节点,完成后再添加到文档中去

(3)对于一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示

(4)在需要经常获取那些引起浏览器回流的属性值时,要缓存到变量中

(5)不要使用table布局,因为一个小改动可能会造成整个table重新布局。而且table渲染通常要3倍于同等元素时间

此外,将需要多次重绘的元素独立为render layer渲染层,如设置absolute,可以减少重绘范围;对于一些进行动画的元素,可以进行硬件渲染,从而避免重绘和回流。


ARP和ACMP

https://blog.csdn.net/qq_40179546/article/details/90300654?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

Last modification:May 23rd, 2020 at 06:49 pm
如果觉得我的文章对你有用,请随意赞赏