Mybatis(Plus)

1. Mybatis

MyBatis 是一个开源的Java持久层框架,用于将对象与关系数据库的表之间进行映射。MyBatis 通过 XML或注解配置文件描述 Java 对象与数据库之间的映射关系,并提供了一些方便的查询语言(类似于SQL)来进行数据库操作。
使用 MyBatis 来操作 MySQL 数据库,将数据存储在 MySQL 中,或从 MySQL 中检索数据,同时使用 MyBatis 进行数据映射和数据库操作的管理。它们通常一起使用,以构建 Java 应用程序的持久层。

数据映射:

MyBatis 的核心功能之一是提供简单且强大的数据映射。使用 XML或注解来定义 SQL查询和映射结果,将数据库表记录映射到 Java对象。

  • 1.1 XML 映射文件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 定义查询 -->
    <select id="selectUser" resultType="User">
    SELECT * FROM users WHERE id = #{id}
    </select>
    <!-- 映射结果到对象 -->
    <resultMap id="BaseResultMap" type="User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    </resultMap>
  • 1.2 注解方式
    1
    2
    @Select("SELECT * FROM users WHERE id = #{id}")
    User selectUser(int id);

核心功能:

  • 动态 SQL:MyBatis 允许你在 XML 中编写动态 SQL 语句,可以根据条件动态构建 SQL 查询。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <select id="selectUsers" parameterType="map" resultType="User">
    SELECT * FROM users
    WHERE 1=1
    <if test="username != null">
    AND username = #{username}
    </if>
    <if test="password != null">
    AND password = #{password}
    </if>
    </select>
  • 参数传递:MyBatis 支持多种参数传递方式,包括单个参数、多个参数、Map 和注解等。
    1
    2
    @Select("SELECT * FROM users WHERE id = #{id} AND username = #{username}")
    User selectUserByIdAndUsername(@Param("id") int id, @Param("username") String username);
  • 批处理:MyBatis 允许执行批处理操作,可以有效地执行一组 SQL 语句。
    1
    2
    3
    4
    5
    6
    7
    8
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    for (User user : userList) {
    userMapper.insertUser(user);
    }
    sqlSession.flushStatements();
    sqlSession.commit();
    sqlSession.close();

事务管理:

  • MyBatis 也提供了事务管理的支持。可以通过配置数据源和事务管理器来实现事务的控制。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 数据源配置 -->
    <dataSource type="POOLED">
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydatabase"/>
    <property name="username" value="root"/>
    <property name="password" value="password"/>
    </dataSource>
    <!-- 事务管理器配置 -->
    <transactionManager type="JDBC"/>
  • MyBatis 可以很容易地与 Spring 框架集成,通过 Spring 的事务管理来控制数据库事务。
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- Spring 配置 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>

SpringBoot实体类 —— VO/DTO/PO

  • VO:View Object,主要用于展示层。它的作用是把某个指定前端页面的所有数据封装起来。他的作用主要是减少传输数据量大小和保护数据库隐私数据(如用户密码、用户邮箱等相关信息)不外泄,同时保护数据库的结构不外泄。

  • DTO:Data Transfer Object,数据传输对象,用于展示层与服务层之间的数据传输对象。(注:实际开发中还存在BO,其作用和DTO类似,当业务逻辑不复杂时一般会被合并。)

  • PO:Persistant Object,持久化对象,和数据库形成映射关系。简单说PO就是每一个数据库中的数据表,一个字段对应PO中的一个变量。(也就是我们常用的Entities)

    1、从前端页面中收到JSON格式数据,后端接口中将其封装为一个VO对象;接口接收到VO对象后将其转换为DTO对象,并调用业务类方法对其进行处理;然后处理为PO对象,调用Dao接口连接数据库进行数据访问(查询、插入、更新等)2、后端从数据库得到结果后,根据Dao接口将结果映射为PO对象,然后调用业务类方法将其转换为需要的DTO对象,再根据前端页面实际需求,转换为VO对象进行返回。
  • 类型转换:上述过程中,VO/DTO/PO等实体类中字段常常会存在多数相同,根据业务需求少数不同。为避免频繁的set和get操作对其进行转换,spring为我们提供了多种方法。(1)使用BeanUtils:(springframework包下)(2)使用BeanUtils:(Apache包下)(3)使用modelMapper??

  • DO(Data Object):通常表示数据库中的数据实体,对应数据库表的结构。它主要用于数据存储和数据库操作,包含与数据库表字段一一对应的属性。类中通常包含与数据库表字段对应的成员变量、getter 和 setter 方法。它不应包含业务逻辑,主要负责数据的持久化和映射。
    尽管 PO 和 DO 在一些情况下用法相似,但它们的侧重点有所不同。PO 更侧重于与数据库的交互,强调持久化和数据表映射;而 DO 侧重于在不同层之间传递数据,强调业务逻辑层面的数据封装。

  • BO(Business Object):通常表示业务层的业务实体,主要用于封装业务逻辑。BO 类一般包含与业务逻辑相关的属性和方法,与具体的数据存储形式无关。包含了一些业务逻辑的操作,比如计算、验证等。它不应直接与数据库进行交互,而是通过调用 Service 层或 DAO 层的方法实现数据的获取和存储。


2. Mybatis-Plus

  • 基于MyBatis:MyBatis-Plus是MyBatis的增强工具包,是在MyBatis基础上的扩展。只做增强不做改变,为简化开发、提高效率而生。它提供了更多的便捷、高效的开发功能,简化了开发人员的编码工作,大幅度提高了开发效率。
  • 功能:MyBatis-Plus 集成了MyBatis的核心功能,同时提供了更多针对CRUD操作、条件构造器、分页、代码生成器等功能的封装。
  • 简化操作:可以减少重复的CRUD代码,提供了一些便捷的API接口和工具,使得开发人员能够更方便地进行数据库操作。
  • 引入 MybatisPlus依赖,可以直接代替 Mybatis依赖
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
    </dependency>
  • MyBatisPlus 的配置项继承了 MyBatis原生配置和一些自己特有的配置。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mybatis-plus:
    type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
    mapper-locations:"classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
    configuration:
    map-underscore-to-camel-case: true # 是否开户下划线和驼峰的映射
    cache-enabled: false # 是否开户二级缓存
    global-config:
    db-config:
    id-type: assign_id # id为雪花算法生成
    update-strategy: not_null # 更新笑略:只更新非空字段

BaseMapper

  • 定义 Mapper接口并继承 BaseMapper类,泛型指定要与数据库映射的 Java实体类(pojo类);
    MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息,自动实现 CRUD的逻辑
    • 默认以类名驼峰转下划线作为表名(User类 -> user表)
    • 默认把名为id的字段作为主键
    • 默认把变量名驼峰转下划线作为表的字段名(createTime类属性 -> create_time表字段)
      1
      public interface UserMapper extends BaseMapper<User>{}
  • 如果实体类和表的对应关系不符合 mp的约定,就要自行配置。可以使用注解:
    • @TableName:指定表名称及全局配置
    • @Tableld:指定id字段及相关配置;
      • IdType的常见类型有:AUTO、ASSIGN ID(默认使用,雪花算法)、INPUT
    • @TableField:指定普通字段及相关配置。使用 @TableField的常见场景是:
      • 1、成员变量名与数据库字段名不一致 2、成员变量名以is开头,且是布尔值
        3、成员变量名与数据库关键字冲突 4、成员变量不是数据库字段

条件构造器

MyBatisPlus支持 使用 Wrapper构造各种复杂的where条件,而不需要在 xml中写sql语句。可以满足日常开发的所有需求。

  • QueryWrapper 和LambdaQueryWrapper通常用来构建 select、delete、update的 where条件部分
  • UpdateWrapper 和LambdaUpdateWrapper通常只有在 set语句比较特殊才使用
  • 尽量使用 LambdaQueryWrapper和 LambdaUpdateWrapper避免硬编码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 原sql:SELECT id,username,info,balance FROM user WHERE username LIKE ? AND balance >= ?
    void testQuerywrapper() {
    QueryWrapper<User> wrapper = new QueryWrapper<User>() // 1.构建查询条件
    .select("id", "username", "info", "balance")
    .like("username", "o")
    .ge("balance", 1000);
    List<User> users = userMapper.selectList(wrapper); // 2.查询
    }
    // 使用 Lambda替代上方法中的硬编码
    void testLambdaQuerywrapper() {
    LambdaQuerywrapper<User> wrapper = new LambdaQuerywrapper<User>()
    .select(User::getid, User::getUsername, User::getInfo, User::getBalance)
    .like(User::getUsername, "o")
    .ge(User::getBalance, 1000)
    List<User> users = userMapper.selectList(wrapper);
    }
    // 原sql:UPDATE user SET balance = 2000 WHERE (username = "jack")
    void testUpdateByQuerywrapper() {
    User user = new User()// 1.要更新的数据
    user.setBalance(2000);
    QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");// 2.更新的条件
    userMapper.update(user, wrapper);// 3.执行更新
    }
    // 原sql:UPDATE user SET balance = balance - 200 WHERE id in (1,2,4)
    void testUpdatewrapper() {
    List<Long> ids = List.of(1L2L4L);
    Updatewrapper<User> wrapper = new Updatewrapper<User>()
    .setSql("balance = balance - 200")
    .in("id", ids) ;
    userMapper.update(null, wrapper) ;
    }

自定义SQL

我们可以利用 MyBatisPlus的 Wrapper来构建复杂的 Where条件,然后自己定义SQL语句中剩下的部分。
在业务层编写wrapper包含sql中的where部分,在mapper方法声明wrapper变量名称“ew”,最后在mapper对应的xml中自定义sql编写where以外的部分(解决了 不能在业务层编写sql 和 使用mp简化查询语句编写 的矛盾??

IService接口

  • 简单业务方法,直接在controller中调用对应的IService中的方法;
    对于复杂业务,需要在自定义Servicelmpl中编写逻辑,调用对应的BaseMapper中的方法;
    当BaseMapper不足以满足需求时,需要在mapper中编写自定义sql(处理where之外的sql,如update…);
    对于mapper中自定义sql,简单的使用注解编写,复杂的在xml中编写。。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 1、自定义Service接口继承IService接口
    public interface IUserService extends IService<User> {
    }
    // 2、自定义Service实现类,实现自定义接口并继承Servicelmpl类(否则要自己一个个实现IService接口的方法)
    public class UserServiceImpl
    extends ServiceImpl<UserMapper, User> // 泛型中指定mapper和实体类类型
    implements IUserService {
    }
    // 使用方法
    class IUserServiceTest {
    @Autowired
    private IUserService userService;
    @Test
    void testSaveUser() {
    User user = new User();
    user.setUsername(uLiLeiu);
    user.setPassword("123");
    userService.save(user); // 如果方法与BaseMapper中的重复,就不需要BaseMapper了??
    }
    }
  • IService的 Lambda方法:在 自定义的 Servicelmpl类中进行 复杂操作
    1. 需求:复杂查询,查询条件如下(name: 用户名关键字,可以为空;status:用户状态,可以为空;minBalance:最小余额,可以为空;maxBalance:最大余额,可以为空)
      1
      2
      3
      4
      5
      6
      7
      8
      public List<User> queryUsers(String name, Integer status, Integer minBalance Integer maxBalance) {
      return lambdaQuery()
      .like(name != null, User::getUsername, name)
      .eg(status != null, User::getStatus, status)
      .ge(minBalance != null, User::getBalance, minBalance)
      .le(maxBalance != null,User::getBalance, maxBalance)
      .list(); // 如果查询一个记录就是.one
      }
    2. 需求:复杂更新,要求如下(按id更新,更新为扣后余额,如果扣减后余额为0,则将用户status修改为冻结状态(2))
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Transaction
      public List<User> deductBalance(Long id,Integer money) {
      ...
      // 4.扣城余额 update tb_user set balance = balance - ?
      int remainBalance = user.getBalance() - money;
      lambdaUpdate()
      .set(User::getBalance, remainBalance)
      .set(remainBalance == 0, User::getStatus, 2) // 如果余额0,修改状态
      .eq(User::getId, id) // 相当于 where
      .eq(User::getBalance,user.getBalance()) // 乐观锁
      .update(); // 更新
      }
  • IService批量新增(批处理):开启 rewriteBatchedStatements=true参数

分页插件

首先,要在配置类中注册MyBatisplus的核心插件,同时添加分页插件
接着,就可以使用分页的API了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // 拦截器的形式实现插件
// 1. 初始化核心插件
MybatisplusInterceptor interceptor = new MybatisplusInterceptor();
// 2. 添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSOL);
pageInterceptor.setMaxLimit(100L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor); // 添加到核心拦截器
return interceptor;
}
}
1
2
3
4
5
6
7
8
void testPageQuery() {
int pageNo = 1, pageSize = 5; // 分页参数
Page<User> page = Page.of(pageNo,pageSize);
page.addOrder(new OrderItem("balance"false)); // 排序参数,通过OrderItem来指定
// 分页查询
Page<User> p = userService.page(page);
// 总条数 p.getTotal(); 总页数 p.getPages(); 分页数据 List<User> records = p.getRecords();
}

MySQL

MySQL是一个开源的关系型数据库管理系统(RDBMS),广泛用于Web应用程序和中小型企业数据库。默认端口是 3306

启动并连接 MySQL

  • 启动 MySQL 服务器(然后尝试连接到 MySQL 服务器;一般情况下 服务器不会自动关闭)
    1
    2
    net start MySQL  # 打开命令提示符(以管理员身份运行),运行以下命令来启动 MySQL 服务
    net stop MySQL # 停止 MySQL 服务
  • 另外,通过 Navicat 创建与本地(或远程) 的 MySQL(或其他)数据库的连接,以对数据库可视化管理
    本机 username: root,password: “0xx1xx”

SQL 术语


关系(Relation):通常是指数据库表(table)。每个关系对应数据库中的一个表格,由多个行和列组成,其中每一行通常代表表格中的一个数据记录,而每一列代表记录中的一个属性(字段)。
属性(attribute):列的名字,上图有学号、姓名、班级、兴趣爱好、班主任、课程、授课主任、分数.
依赖 (relation):列属性间存在的某种联系
元组(tuple):每一个行,如第二行 (1301,小明,13班,篮球,王老师,英语,赵英,70) 就是一个元组
模式(schema):这里指逻辑结构,如 学生信息(学号,姓名,班级,兴趣爱好,班主任,课程,授课主任,分数)的笼统表述。(数据库模式是数据库的结构描述,包括表格、字段、关系、视图、索引等元素的组织方式。描述了数据库中不同关系表格之间的关联和数据的组织方式。通常包含了数据库中表格的定义,包括表格的名称、字段的名称、字段的数据类型、主键等信息。)
(domain):数据类型,如string、integer等,上图中每一个属性都有它的数据类型 (即域)
(key):由关系的一个或多个属性组成,任意两个键相同的元组,所有属性都相同。需要保证表示键的属性最少。一个关系可以存在好几种键,工程中一般从这些候选键中选出一个作为主键 (primary key)
主键(Primary Key):主键是一个表格中的一列或一组列,它的值用来唯一标识表格中的每一行记录。主键的值不能重复,且不能为空。这意味着每一行记录在主键列上必须有唯一的值,用于区分记录。主键用于建立表格之间的关系和确保数据的完整性。通常,每个表格都有一个主键,但也可以由多个列组成复合主键
候选键(candidate key):由关系的一个或多个属性组成,候选键都具备键的特征,都有资格成为主键
超键(super key):包含键的属性集合,无需保证属性集的最小化。每个键也是超键。可以认为是键的超集。
外键(foreign key):如果某一个关系A中的一个(组)属性是另一个关系B的键,则该(组)属性在A中称为外键。
主属性 (prime attribute):所有候选键所包含的属性都是主属性
投影 (proiection):选取特定的列,如将关系学生信息投影为学号、姓名即得到上表中仅包含学号、姓名的列
选择 (selection):按照一定条件选取特定元组,如选择上表中分数>80的元组
笛卡儿积 (交叉连接Cross join):第一个关系每一行分别与第二个关系的每一行组合
自然连接(natural join):第一个关系中每一行与第二个关系的每一行进行匹配,如果得到有交叉部分则合并,若无交叉部分则舍弃。
连接(theta join):即加上约束条件的笛卡儿积,先得到笛卡儿积,然后根据约束条件删除不满足的元组.
外连接 (outer join):执行自然连接后,将舍弃的部分也加入,并且匹配失败处的属性用NULL代替。
除法运算(division):关系R除以关系S的结果为T,则T包含所有在R但不在S中的属性,且T的元组与S的元组的所有组合在R中。


SQL 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE DATABASE mydatabase;  /* 创建数据库 */
USE mydatabase; /* 选择数据库 */
CREATE TABLE users ( -- 创建表
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255),
email VARCHAR(255)
);
DROP TABLE users; -- 删除表
ALTER TABLE users ADD col CHAR(20); -- 添加列
ALTER TABLE mytable DROP COLUMN col; -- 删除列
ALTER TABLE mytable CHANGE col col1 CHAR(32) NOT NULL DEFAULT '123'; -- 修改列和属性
-- 插入数据
INSERT INTO users (username, email) VALUES ('user1', 'user1@example.com');
-- 插入检索出来的数据
INSERT INTO mytable1(col1, col2)
-- 更新数据
UPDATE users SET email='newemail@example.com' WHERE username='user1';
-- 删除数据
DELETE FROM users WHERE username='user1';

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SELECT distinct id 
FROM users;
LIMIT 2, 3;
ORDER BY col1 DESC, col2 ASC;
WHERE col is NULL;
-- 子查询:只能返回一个字段的数据,可以将子查询的结果作为 WHRER 语句的过滤条件,配合 (not )in
SELECT * FROM mytable1
WHERE col1 IN (SELECT col2
FROM mytable2);
-- 分组:将数据按照一个或多个列的值分成不同的组,常与聚合函数(如 SUM、COUNT、AVG 等)一起以对每个组聚合操作
-- GROUP BY 子句: 用于指定按哪些列进行分组; HAVING 子句: 用于对分组后的数据进行筛选。
-- WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
SELECT col, COUNT(*) AS num
FROM mytable
WHERE col > 2
GROUP BY col
HAVING num >= 2
ORDER BY SUM(column2) DESC;
  1. SELECT:查询出属性,用 AS 给列名、计算字段和表名取别名,以简化 SQL 语句以及连接相同表;
    select 中 sql 函数计算出的值作为查询出的属性(select round(count()/3, 2) from ..)
    select 中可以加一个select 用于属性计算(select id, count(
    )/(select count(*) from Users) per from ..)
    若有重复列,使用 DISTINCT 去除重复值
  2. LIMIT:LIMIT 2, 3 返回第 3 ~ 5 行。配合排序实现获取最大/最小值。。
  3. WHERE:过滤行,AND 和 OR 用于连接多个过滤条件。优先处理 AND,使用 () 决定优先级;
    is 搭配 null,IN 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
  4. ORDER:可以按多个列进行排序,并指定不同的排序方式,默认升序ASC, 降序DESC
  5. GROUP BY 可以放在 WHERE 前、后,想清楚在分组前、后过滤
  6. 子查询的结果需要指定别名。

连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 连接(内连接):返回多个表中匹配条件满足的行,不匹配的行不会被包括在结果集中。
-- 使用 (INNER )JOIN 关键字,条件语句使用 ON 而不是 WHERE,连接可以替换子查询且效率一般会更快。
SELECT A.value, B.value
FROM tablea AS A JOIN tableb AS B
ON A.key = B.key;
-- 自连接:可以看成内连接的一种,只是连接的表是自身而已。
SELECT e1.name
FROM employee AS e1 INNER JOIN employee AS e2
ON e1.department = e2.department AND e2.name = "Jim";
-- 自然连接:自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
-- 内连接和自然连接的区别: 内连接提供连接的列,而自然连接自动连接所有同名列。
SELECT A.value, B.value FROM tablea AS A NATURAL JOIN tableb AS B;
-- 外连接:保留了没有关联的那些行。分为左,右外连接以及全外连接。
-- 左连接返回左表的所有行以及与右表匹配的行。如果右表中没有匹配的行,将会返回 NULL 值。
-- on 后等于新连接出了一张表,where 在新表上进行查询
select Employee.name, Bonus.bonus
from Employee left join Bonus
on Employee.empId = Bonus.empId
where Bonus.bonus < 1000 or Bonus.bonus is null

SQL 函数

  1. 数学函数:SUM():计算指定列的总和。 AVG():计算指定列的平均值。 MAX():找出指定列的最大值。 MIN():找出指定列的最小值。 COUNT():计算指定列的行数。 ROUND(x,y):把 x 四舍五入到 y 位小数。 ABS():返回绝对值。
  2. 字符串函数:CONCAT():连接两个或多个字符串。 SUBSTRING()SUBSTR():从字符串中提取子字符串。 LENGTH():返回字符串的长度。 UPPER():将字符串转换为大写。 LOWER():将字符串转换为小写。 TRIM():去除字符串首尾的空格或其他指定字符。 REPLACE():替换字符串中的子串。
  3. 日期和时间函数:NOW()CURRENT_TIMESTAMP():返回当前日期和时间。 DATE():从日期时间值中提取日期部分。 TIME():从日期时间值中提取时间部分。 YEAR():从日期中提取年份。 MONTH():从日期中提取月份。 DAY():从日期中提取天。HOUR():从时间中提取小时。 MINUTE():从时间中提取分钟。 SECOND():从时间中提取秒。
  4. 逻辑函数: IF()CASE:根据条件返回不同的值,if(rating<3, 1, 0);COALESCE():返回第一个非空值。
  5. 聚合函数: GROUP_CONCAT():将组内的值连接成一个字符串。 GROUP_BY:分组聚合查询结果。
  6. 窗口函数:ROW_NUMBER():为结果集的每行分配一个唯一的行号。 RANK():为结果集中的行分配排名。 DENSE_RANK():为结果集中的行分配密集排名。 OVER():定义窗口以进行窗口函数计算。

MySQL 数据类型

  1. 整数类型: 用于存储整数值,包括 TINYINT, SMALLINT, MEDIUMINT, INT, 和 BIGINT
  2. 浮点数类型: 用于存储浮点数,包括 FLOATDOUBLEDECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
    FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽。如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分
  3. 定点数类型: 用于存储定点数,包括 DECIMALNUMERIC
  4. 字符串类型: 用于存储文本数据,主要有 CHAR, VARCHAR 两种类型,一种是定长的,一种是变长的。
    VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
  5. 二进制数据类型: 用于存储二进制数据,包括 BINARY, VARBINARY, TINYBLOB, BLOB, MEDIUMBLOB, 和 LONGBLOB.
  6. 日期和时间类型(Date and Time Types): DATE 用于存储日期。日期格式为’YYYY-MM-DD’,如’2023-10-12’。 TIME 用于存储时间。时间格式为’HH:MM:SS’,如 ‘14:30:45’。 YEAR 用于存储年份,可以使用两位或四位格式(’YY’或’YYYY’)
    DATETIME 用于存储日期和时间。能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。它与时区无关,格式为’YYYY-MM-DD HH:MM:SS’,如’2023-10-12 14:30:45’。
    TIMESTAMP 用于存储日期和时间,在插入或更新时自动记录当前时间。使用 4 个字节,只能表示从 1970 年到 2038 年。时区有关,即一个时间戳在不同的时区所代表的具体时间是不同的。应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
  7. 布尔类型: 用于存储布尔值,包括 BOOLEAN, BOOL, TINYINT(1)
  8. 枚举类型: 用于存储枚举值,其中一个预定义的枚举值,如 ENUM('value1', 'value2', ...)
  9. 集合类型: 用于存储一个或多个预定义的集合值,如 SET('value1', 'value2', ...)
  10. 自动增长类型: 用于自动生成唯一标识符,如 AUTO_INCREMENT

函数依赖

记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。
如果 {A1,A2,… ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 A->B,如果能找到 A 的真子集 A’,使得 A’-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖。
对于 A->B,B->C,则 A->C 是一个传递函数依赖。
有依赖:学号 -> 姓名、学院, 学院 -> 院长, 学号、课程-> 成绩
则 成绩Grade 完全函数依赖于键码(学号,课程),它没有任何冗余数据,每个学生的每门课都有特定的成绩。姓名, 学院 和 院长 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。

MySQL 范式

范式理论是为了解决四种异常。不符合范式的关系,会产生很多异常:1、冗余数据。2、修改异常: 修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。3、删除异常: 删除一个信息,那么也会丢失其它信息。4、插入异常: 例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。

  1. 第一范式 (1NF):属性不可分。
  2. 第二范式 (2NF):每个非主属性完全函数依赖于键码。可以通过分解来满足。(一张表分解成多张表)
  3. 第三范式 (3NF):非主属性不传递函数依赖于键码。可以进行分解。
    for more:https://blog.csdn.net/calcular/article/details/79332453

事务管理

  • 事务:指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
    • 原子性(Atomicity)事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
    • 一致性(Consistency)数据库在事务执行前后都保持一致性状态。一致性状态下,所有事务对一个数据的读取结果都是相同的。
    • 隔离性(Isolation)一个事务所做的修改在最终提交以前,对其它事务是不可见的。
    • 持久性(Durability)一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。
  • 并发一致性:产生不一致的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。
    • 丢失修改:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
    • 读脏数据:T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
    • 不可重复读:T2 读取一个数据,T1 对该数据做了修改。 T2 再次读取这个数据时读取的结果和第一次读取的结果不同。
    • 幻影读:T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据 结果和第一次不同。
      并发控制可以通过 封锁 来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的 隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
  • 封锁
    • 封锁粒度:MySQL 中提供了两种封锁粒度,行级锁以及表级锁。
    • 读写锁:1、排它锁(Exclusive),简写为 X 锁,又称写锁。 2、共享锁(Shared),简写为 S 锁,又称读锁。
    • 意向锁:使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。
    • 封锁协议:1. 三级封锁协议 2. 两段锁协议
  • 隔离等级:MySQL支持 四种 标准的事务隔离级别,这些隔离级别定义了事务之间的可见性和并发控制。
    READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE 脏读 不可重复读 幻影读
    未提交读(允许一个事务读取另一个事务未提交的数据)
    提交读(一个事务只能读取到另一个事务已经提交的数据) ×
    可重复读(事务执行期间,一个事务多次读取同一行数据时,会得到相同的结果) × ×
    可串行化(最高的隔离级别,确保事务串行执行) × × ×
  • 数据库默认隔离级别
    oracle数据库默认的隔离级别是:读已提交。
    mysql数据库默认的隔离级别是:可重复读。
  • InnoDB 中如何防止幻读 or MVCC 实现
    • 1、执行普通 select,此时会以 MVCC 快照读的方式读取
      (1)一致性非锁定读(快照读),普通的SELECT,通过多版本并发控制(MVCC)实现。
      (2)在快照读下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”。
      (3)只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读。
    • 2、执行 select…for update/lock in share mode、insert、update、delete 等当前读
      (1)一致性锁定读(当前读),SELECT … FOR UPDATE/SELECT … LOCK IN SHARE MODE/INSERT/UPDATE/DELETE,通过锁实现。
      (2)在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用 Next-key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      START TRANSACTION;  -- 启动事务,用于标志事务的开始。在这之后的 SQL 语句都将在一个事务中执行 
      COMMIT; -- 提交事务,当所有的 SQL 语句都执行成功时,使用 `COMMIT` 命令来提交事务,将更改永久保存到数据库。提交后,事务结束。
      ROLLBACK; -- 回滚事务,如果在事务执行的过程中发生了错误或者不符合条件,可以使用 `ROLLBACK` 命令来回滚事务,撤销所有的更改,使数据库回到事务开始前的状态。

      SAVEPOINT savepoint_name; -- 保存点,用于创建一个保存点,以便在事务中的某一时刻回滚到这个保存点。可以在事务中设置多个保存点。
      ROLLBACK TO savepoint_name; -- 回滚到保存点,如果在事务中的某一步出现问题,可以回滚到之前设置的某个保存点,而不必回滚整个事务。

      -- 设置事务隔离级别,包括 `READ UNCOMMITTED`、`READ COMMITTED`、`REPEATABLE READ` 和 `SERIALIZABLE`。
      SET TRANSACTION ISOLATION LEVEL level;

      -- MySQL 默认是自动提交模式,即每个 SQL 语句都会自动成为一个事务并提交。命令关闭自动提交,再通过 `COMMIT` 手动提交,或者通过 `ROLLBACK` 回滚。
      SET autocommit = 0;

      --示例 1
      START TRANSACTION;
      -- 执行一系列 SQL 语句
      -- 提交事务
      COMMIT;

      --示例 2
      START TRANSACTION;
      -- 执行一系列 SQL 语句
      -- 如果发生错误,回滚事务
      ROLLBACK;

MySQL 存储引擎

  • InnoDB
    • 是 MySQL 默认的事务型存储引擎。 实现了四个标准的隔离级别,默认级别是可重复读。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读
    • 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等
    • 支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
  • MyISAM
    • 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它
    • 提供了大量的特性,包括压缩表、空间数据索引等。
    • 不支持事务
    • 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
    • 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作非常慢
    • 如果指定了 DELAY KEY WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作

MySQL 索引

在MySQL中,索引是一种用于提高数据库查询效率的数据结构。它类似于书的目录,通过在数据库表上创建索引,可以快速定位并访问表中的特定数据行,而无需全表扫描。索引在数据库的性能优化中扮演着关键的角色,特别是在大型数据集上。
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。

  • 索引类型
    1. B+Tree 索引:是大多数 MySQL 存储引擎的默认索引类型。因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。
      InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
    2. 哈希索引:能以 O(1) 时间进行查找,但是失去了有序性。存在限制:无法用于排序与分组;只支持精确查找,无法用于部分查找和范围查找。
      InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
    3. 全文索引:MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
    4. 空间数据索引:MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。必须使用 GIS 相关的函数来维护数据
  • 索引的优点:大大减少了服务器需要扫描的数据行数。帮助服务器避免进行排序和分组,也就不需要创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。
  • 索引的使用场景
    • 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。
    • 对于中到大型的表,索引就非常有效。
    • 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。

B+Tree 索引

是大多数 MySQL 存储引擎的默认索引类型因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组可以指定多个列作为索引列,多个索引列共同组成键.
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引.
主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

索引优化

1.独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。例如下面的查询不能使用 actor id 列的索引:
2.多列索引
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor id和 film id 设置为多列索引。
3.索引列的顺序
4.前缀索引对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。对于前缀长度的选取需要根据索引选择性来确定。
5.覆盖索引


MySQL - 一条 SQL 的执行过程详解

https://pdai.tech/md/db/sql-mysql/sql-mysql-execute.html

在系统和 MySQL 进行交互之前,MySQL 驱动会帮我们建立好连接,然后我们只需要发送 SQL 语句就可以执行 CRUD 了。
java 系统在通过 MySQL 驱动和 MySQL 数据库连接的时候是基于 TCP/IP 协议的,多线程请求的时候频繁的创建和销毁连接显然是不合理的。在访问 MySQL 数据库的时候,建立的连接并不是每次请求都会去创建的,而是从数据库连接池中去获取,这样就解决了因为反复的创建和销毁连接而带来的性能损耗问题了。MySQL 的架构体系中也已经提供了这样的一个池子,也是数据库连池。双方都是通过数据库连接池来管理各个连接的,这样一方面线程之前不需要是争抢连接,更重要的是不需要反复的创建的销毁连接。

网络连接必须由线程来处理

网络中的连接都是由线程来处理的,所谓网络连接说白了就是一次请求,每次请求都会有相应的线程去处理的。也就是说对于 SQL 语句的请求在 MySQL 中是由一个个的线程去处理的。

SQL 接口:MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接口去处理。
查询解析器:会将 SQL 接口传递过来的 SQL 语句进行解析,翻译成 MySQL 自己能认识的语言
MySQL 查询优化器:查询优化器内部具体怎么实现的我们不需要是关心,我需要知道的是 MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据成本最小原则来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本。MySQL 优化器 会计算 「IO 成本 + CPU」 成本最小的那个索引来执行
存储引擎:查询优化器会调用存储引擎的接口,去执行 SQL,也就是说真正执行 SQL 的动作是在存储引擎中完成的。数据是被存放在内存或者是磁盘中的(存储引擎是一个非常重要的组件,后面会详细介绍)
执行器:执行器是一个非常重要的组件,因为前面那些组件的操作最终必须通过执行器去调用存储引擎接口才能被执行。执行器最终最根据一系列的执行计划去调用存储引擎的接口去完成 SQL 的执行

存储引擎

当我们系统发出这样的查询去交给 MySQL 的时候,MySQL 会按照我们上面介绍的一系列的流程最终通过执行器调用存储引擎去执行,流程图就是上面那个。
在执行这个 SQL 的时候 SQL 语句对应的数据要么是在内存中,要么是在磁盘中,如果直接在磁盘中操作,那这样的随机IO读写的速度肯定让人无法接受的,所以每次在执行 SQL 的时候都会将其数据加载到内存中,这块内存就是 InnoDB 中一个非常重要的组件:缓冲池 Buffer Pool

关于Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系:
Buffer Pool 是 MySQL 的一个非常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的
Undo log 记录的是数据操作前的样子
redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有)
bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义)
、、,,
,。

从准备更新一条数据到事务的提交的流程描述

  • 首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中
  • 在数据被缓存到缓存池的同时,会写入 undo log 日志文件更
  • 新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中
  • 完成以后就可以提交事务,在提交的同时会做以下三件事
    • 将redo log buffer中的数据刷入到 redo log 文件中
    • 将本次操作记录写入到 bin log文件中
    • 将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记
  • 至此表示整个更新事务已经完成

关系型数据库是如何工作的

SQL DB 组成

核心组件

进程管理器(process manager): 很多数据库具备一个需要妥善管理的进程/线程池。再者,为了实现纳秒级操作,一些现代数据库使用自己的线程而不是操作系统线程
网络管理器(network manager): 网路 I/O 是个大问题,尤其是对于分布式数据库。所以一些数据库具备自己的网络管理器
文件系统管理器(File system manager): 磁盘 I/O 是数据库的首要瓶颈。具备一个文件系统管理器来完美地处理OS文件系统甚至取代OS文件系统,是非常重要的。
内存管理器(memory manager): 为了避免磁盘 I/O 带来的性能损失,需要大量的内存。但是如果你要处理大容量内存你需要高效的内存管理器,尤其是你有很多查询同时使用内存的时候。
安全管理器 (Security Manager) : 用于对用户的验证和授权
客户端管理器 (Client manager) : 用于管理客户端连接。

工具

备份管理器 (Backup manager) : 用于保存和恢复数据
恢复管理器 (Recovery manager) : 用于崩溃后重启数据库到一个一致状态。
监控管理器 (Monitor manager) : 用于记录数据库活动信息和提供监控数据库的工具。
管理员管理器(Administration manager) : 用于保存元数据(比如表的名称和结构),提供管理数据库、模式、表空的工具

查询管理器

查询解析器 (Query parser) : 用于检查查询是否合法
查询重写器 (Query rewriter) : 用于预优化查询
查询优化器 (Query optimizer) : 用于优化查询
查询执行器 (Query executor) : 用于编译和执行查询

数据管理器

事务管理器 (Transaction manager) : 用于处理事务
缓存管理器 (Cache manager): 数据被使用之前置于内存,或者数据写入磁盘之前置于内存
数据访问管理器 (Data access manager) : 访问磁盘中的数据

数据查询的流程

  • 客户端管理器: 客户端管理器是处理客户端通信的。客户端可以是一个(网站)服务器或者一个最终用户或最终应用。客户端管理器通过一系列知名的API(JDBC, ODBC, OLE-DB …)提供不同的方式来访问数据库。客户端管理器也提供专有的数据库访问API。
    JDBC 是 Java 编程语言中用于连接和操作数据库的标准API。可以理解为 JDBC 是 Java 与数据库之间的桥梁,允许 Java 应用程序与 MySQL 数据库进行通信和交互。MyBatis 是一个开源的Java持久层框架,用于将对象与关系数据库的表之间进行映射,使用 MyBatis 来操作 MySQL 数据库。)
  • 查询管理器:查询首先被解析并判断是否合法、然后被重写,去除了无用的操作并且加入预优化部分、接着被优化以便提升性能,并被转换为可执行代码和数据访问计划、然后计划被编译、最后,被执行
  • 数据管理器:在这一步,查询管理器执行了查询,需要从表和索引获取数据,于是向数据管理器提出请求。
  • 客户端管理器

管理用户权限

1
2
3
4
5
6
-- 创建用户
CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
-- 授予用户权限
GRANT ALL PRIVILEGES ON mydatabase.* TO 'newuser'@'localhost';
-- 刷新权限
FLUSH PRIVILEGES;

备份和恢复

1
2
3
4
# 备份数据库
mysqldump -u username -p mydatabase > backup.sql
# 恢复数据库
mysql -u username -p mydatabase < backup.sql

VMware / SSH

虚拟机 (VM)

  • 虚拟机(Virtual Machine)是一种利用软件模拟硬件的技术,使得一台物理计算机可以同时运行多个虚拟的操作系统或应用程序实例。虚拟机技术提供了更好的资源利用率、隔离性和灵活性,常见的虚拟机有硬件虚拟机和软件虚拟机两种。
  • 它包括虚拟的 CPU、内存、硬盘、网络接口等组件。
  • 可以在虚拟机上安装操作系统和应用程序,就像在物理计算机上一样。
  • 虚拟机和容器的区别:虚拟机模拟整个操作系统,资源占用相对较多;容器共享主机的操作系统内核,更轻量级,启动速度快。

VMware Workstation

  • VMware Workstation 是一款用于虚拟化的桌面软件,它允许用户在单个物理计算机上运行多个虚拟操作系统。
  • 创建虚拟机:启动 VMware Workstation,
    1、创建新虚拟机,选择ISO文件,填写虚拟机的名称以及将来保存的位置;
    2、虚拟机配置:按照向导的指示配置虚拟机,包括选择操作系统、分配内存、创建虚拟硬盘等;安装操作系统:
    3、启动虚拟机,按照正常流程安装选定的操作系统。
  • 虚拟机克隆: VMware Workstation 允许你克隆虚拟机,以便快速创建相似的虚拟机。
  • 虚拟机导入:(除了创建虚拟机外) 你可以导入其他虚拟化平台的虚拟机。导入虚拟机时,通常使用的是虚拟机的配置文件和虚拟硬盘文件。(存放于 C:\Users\蔡枫\Documents\Virtual MachinesD:\Virtual Machines
    • 虚拟机配置文件: 这个文件包含了虚拟机的设置、硬件配置、网络配置等信息。在 VMware 中,这个文件通常有一个扩展名为 .vmx。当你导入虚拟机时,你需要选择这个配置文件。
    • 虚拟硬盘文件: 这个文件包含了虚拟机的硬盘数据,即操作系统和应用程序的安装等信息。在 VMware 中,虚拟硬盘文件的格式通常是 .vmdk。你也需要选择这个文件来导入虚拟机。
  • 在VMware界面中操作虚拟机非常不友好,一般推荐使用专门的SSH客户端
    市面上常见有:Xshell,Finshell,MobarXterm

CentOS

  • 一个基于Red Hat Enterprise Linux(RHEL)源代码构建的开源操作系统。默认账号:root,,账号2:leo,密码:
  • 虚拟机目录:D:\VMCentOS-7,(网络适配器)名称:ens33;默认路由(网关):192.168.111.2,DNS:192.168.111.2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [leo@localhost ~]$ ifconfig
    ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
    inet 192.168.111.128 netmask 255.255.255.0 broadcast 192.168.111.255 # IPv4 子网掩码 广播地址
    inet6 fe80::2304:6b2e:1716:b51 prefixlen 64 scopeid 0x20<link> # IPv6地址
    ether 00:0c:29:1a:cb:1a txqueuelen 1000 (Ethernet) # MAC 地址
    RX packets 1704 bytes 1628741 (1.5 MiB)
    RX errors 0 dropped 0 overruns 0 frame 0
    TX packets 850 bytes 71942 (70.2 KiB)
    TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
    设置 ipv4 method为手动(网卡地址改为静态IP,这样可以避免每次启动虚拟机IP都变化)

Kali Linux

Kali Linux(简称Kali)是一个基于Debian Linux的专业渗透测试和网络安全评估发行版。它提供了大量的安全工具,方便安全专业人员进行渗透测试、网络扫描、恢复密码等操作。默认账号:kali,默认密码:kali
专业工具集:预装了大量的渗透测试和安全评估工具,包括 Metasploit、Wireshark、Nmap、Aircrack-ng 等。


SSH

  1. SSH(Secure Shell)是一种用于在网络上进行加密通信的协议。它通常用于通过不安全的网络(例如互联网)安全地访问远程计算机上的终端。SSH 协议的主要目的是提供加密和身份验证,确保在客户端和服务器之间传输的数据是安全的。
  2. SSH 协议有两个主要组件:SSH客户端:用于发起远程连接的计算机上的程序。通过 SSH 客户端,用户可以连接到远程计算机并执行命令; SSH服务器:运行在远程计算机上的程序,接受来自客户端的连接并提供终端访问或执行其他操作。
  3. SSH 还可以用于文件传输和端口转发等用途,使其成为管理和维护远程服务器的强大工具。在日常工作中,开发人员、系统管理员和网络管理员经常使用 SSH 来远程管理和维护服务器。

SSH命令行工具

Windows命令提示符和PowerShell都支持SSH命令。你可以使用ssh命令来连接到远程服务器

1
2
ssh username@hostname  # 用户名 @ 公网IP
ssh -i 密钥文件 username@hostname # 密钥登录

SSH客户端:MobarXterm

如果远程服务器上的操作系统的界面使用起来不方便,可以使用ssh客户端连接并操控远程服务器,包括本机上的虚拟机;
在VMware界面中操作虚拟机非常不友好,一般推荐使用专门的SSH客户端,常见有 Xshell,Finshell,MobarXterm

连接虚拟机

1、打开MobarXterm,点击session按钮,进入会话管理:
2、在弹出的session管理页面中,填写信息:Remote Host(虚拟机的ipv4地址),Specify name(root)
3、输入密码:(与虚拟机的一样?)cx0xx1xx,连接本机上 VMware中的 CentOS7系统的虚拟机
4、连接(虚拟机)成功后,进入操作界面了,通过 MobarXterm界面 对 VMware中的虚拟机 进行操作。

使用 MobarXterm 操作 Docker

1、启动虚拟机,使用 MobarXterm 连接虚拟机成功后,进入操作界面。在 MobarXterm界面上 对 VMware中的虚拟机 进行操作。
2、安装配置 Docker

1
2
3
4
5
6
7
8
9
10
# 首先要安装一个yum工具,并配置Docker的yum源
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 安装Docker
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl start docker # 启动Docker
systemctl stop docker # 停止Docker
systemctl restart docker # 重启
systemctl enable docker # 设置开机自启
docker ps # 执行docker ps命令,如果不报错,说明安装启动成功

3、配置阿里云镜像加速,重启docker

Linux

Linux 是一种免费、开源的类Unix操作系统内核。它最初由芬兰的Linus Torvalds在1991年创建,并迅速发展成为一个庞大且活跃的开源社区项目。Linux内核是操作系统的核心部分,但通常与 GNU 工具和其他软件一起使用,形成完整的(GNU/Linux)操作系统,通常被称为 Linux发行版(如 Ubuntu、Red Hat、CentOS)

GNU

GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容:
1、以任何目的运行此程序的自由;2、再复制的自由;3、改进此程序,并公开发布改进的自由。

发行版

Linux 发行版是 Linux 内核及各种应用软件的集成版本。

基于的包管理工具 商业发行版 社区发行版
RPM Red Hat Fedora / CentOS
DPKG Ubuntu Debian

Bash

是一种Unix和Linux操作系统中常用的命令行解释器和脚本语言。它是许多Linux发行版和Unix系统默认的命令行shell,使用户能够以文本方式与系统进行交互,用于与操作系统进行交互、管理文件、执行命令和编写脚本。
命令行shell是一种计算机程序,它允许用户通过文本界面与计算机操作系统进行交互,以执行各种命令和操作。用户可以在命令行shell中输入文本命令,然后系统会解释和执行这些命令。这种方式通常与图形用户界面 GUI相对。
Bash支持许多功能,包括:

  • 执行命令和程序:Bash允许用户执行各种命令和程序,可以通过命令行或脚本文件来执行。
  • 变量和环境设置:Bash支持变量,用户可以定义和使用环境变量,这些变量可用于存储数据和配置。
  • 文件操作:用户可以使用Bash执行文件和目录的操作,如创建、复制、移动、删除文件等。
  • 管道和重定向:Bash允许将命令的输出连接到其他命令的输入,还支持文件重定向,使用户能够将命令的输出保存到文件或从文件中读取输入。
  • 条件语句和循环:Bash支持条件语句(如if语句)和循环结构(如for循环和while循环),以实现更复杂的控制流程。
  • 脚本编写:用户可以编写Bash脚本,这是一系列命令和操作的集合,用于执行自动化任务、自定义系统配置和处理数据等。

而在 Windows 系统上,PowerShell 是一个更现代、功能更强大的命令行工具,特别适用于系统管理和自动化任务。尽管命令行程序cmd 仍然存在并且仍然可以使用,但PowerShell已成为Windows系统管理和开发的首选工具。

WSL

WSL(Windows Subsystem for Linux)允许用户直接在 Windows 上运行Linux 环境(包括大多数命令行工具、实用程序和应用程序),无需修改,无需单独的虚拟机或双重启动。与完整虚拟机相比,WSL 需要的资源(CPU、内存和存储)更少。
WSL是 Windows10/11 自带的一个功能,默认是关闭的,旨在为希望同时使用 Windows 和 Linux 的用户提供无缝且高效的体验。能够在 Bash shell 中运行Linux,并选择您的发行版(Ubuntu、Debian、OpenSUSE、Kali、Alpine 等)。

  • 开启 Hype-V:ctrl+r - control - 程序 - 开启Windows功能 - 勾选Hype-V
  • 启动 msl:以管理员打开powershell输入下列命令
    1
    dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
  • 启用虚拟化
    1
    dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
  • 更新wsl版本;或手动下载最新版本的 Linux 内核更新包,运行(双击运行 - 系统将提示您提供提升的权限,选择“是”以安装)
    1
    wsl --update
  • 将 WSL 2 设置为默认版本
    1
    wsl --set-default-version 2
  • 安装Linux发行版,创建账号用户密码,Installation successful
    1
    wsl --install -d Ubuntu
  • 至此,已成功安装并设置了一个与 Windows 操作系统完全集成的 Linux 发行版!下次,您可以以管理员身份打开 PowerShell 或 cmd 命令控制台,输入 wsl 命令即可打开安装好的Linux发行版(如Ubuntu)了。
    注意:在wsl中,本地磁盘都位于 /mnt 目录。比如 c 盘位于 /mnt/c 。要进入 d 盘,执行命令 cd /mnt/d
  • PowerShell 进出 wsl:
    1
    2
    3
    4
    PS C:\Windows\system32> wsl
    root@caifeng7138:/mnt/c/Windows/system32# exit
    logout
    PS C:\Windows\system32>

Linux 命令

Linux 操作系统拥有丰富的命令行工具。可以使用--help获取指令的基本用法与选项介绍;在终端中使用man命令查看每个命令的手册页以获取更多信息,例如 man lsdoc:/usr/share/doc 存放着软件的一整套说明文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# **管理员:** 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
sudo + 命令
which # 指令搜索。-a : 将所有指令列出,而不是只列第一个
# **文件和目录管理:**
ls # 列出目录中的文件和子目录,是 list 的缩写(dir 命令通常不是默认可用的)
cd # 切换当前工作目录。
pwd # 显示当前工作目录的路径。
mkdir # 创建新目录。
rmdir / rm -r # 删除目录。
touch # 创建新文件或更新文件的访问时间戳。
vim + 文件名 # 编辑文件
cp # 复制文件或目录。
mv # 移动文件或目录,也可用于重命名文件。
rm # 删除文件。
cat # 查看文件内容。
tac # 是 cat 的反向操作,从最后一行开始打印。
more # 和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
head # 取得文件前几行,-n指定n行
tail # 是 head 的反向操作,只是取得是后几行。
od # 以字符或者十六进制的形式显示二进制文件。
whereis # 文件搜索。速度比较快,因为它只搜索几个特定的目录。
locate # 文件搜索。可以用关键字或者正则表达式进行搜索
find # 文件搜索。可以使用文件的属性和权限进行搜索。example: find . -name "shadow*"
## **压缩,打包:**
gzipg # zip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。
tar # 打包。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。
## **系统管理:**
ps # 查看运行中的进程。
top # 实时显示进程信息。示例: 两秒钟刷新一次 ## top -d 2
netstat # 查看占用端口的进程
kill # 终止进程。
shutdown / reboot # 关机或重启系统。
uname # 显示系统信息。
date # 显示或设置系统时间。
useradd / userdel # 添加或删除用户。
passwd # 更改用户密码。
## **包管理:** RPM 和 DPKG 为最常见的两类软件包管理工具;YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能
apt-get # 安装、更新和删除软件包。(Debian/Ubuntu)
yum # 安装、更新和删除软件包。(Red Hat/CentOS)
snap install # 下载最新
dpkg # 直接管理软件包。(Debian/Ubuntu)
rpm # 直接管理软件包。(Red Hat/CentOS)
pip # Python包管理器。
## **网络工具:**
ping # 测试网络连接。
curl / wget # 下载文件或内容。
ssh # 远程登录到其他计算机。
scp # 安全拷贝文件到远程主机。
ifconfig # 显示网络接口的配置信息,包括IP地址、子网掩码等。(Windows系统上是 ipconfig)

二、磁盘

Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:

  • IDE 磁盘: /dev/hd[a-d]
  • SATA/SCSI/SAS 磁盘: /dev/sd[a-p]
    其中文件名后面的序号的确定与系统检测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。

三、分区

磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。

四、文件系统

Linux 文件系统是一种层次化的组织结构,用于存储和管理文件。这个文件系统通常由一个根目录和一系列子目录、文件以及链接组成。
Linux 系统是以文件目录系统为根基的,一切东西都是文件,包括硬件、进程、命令、系统设置等等。

  1. 树状结构: Linux文件系统采用树状结构,以根目录(/)作为顶层目录。树状结构允许文件和目录以层次结构进行组织,方便用户和应用程序定位和访问文件。(Windows 中的目录斜杠与之相反,是右斜杠 \)
  2. 根目录(/): 根目录是文件系统的最顶层目录,包含整个文件系统的目录和文件。所有的文件和目录都位于根目录下或其子目录中。
  3. 目录(文件夹): 目录是用于组织和存储文件的容器。它类似于Windows中的文件夹。目录中可以包含文件和其他子目录。
  4. 文件: 文件是数据的容器。在Linux中,一切都是文件,包括文本文件、二进制文件、设备文件等。
  5. 路径: 路径是指定文件或目录位置的方式。绝对路径从根目录开始,而相对路径从当前工作目录开始。例如,/home/user/documents 是一个绝对路径,表示文档目录在用户主目录下。

十、进程管理


Vim

Vim(Vi Improved)是一款强大的文本编辑器,广泛用于Linux和Unix系统。它是Vi编辑器的改进版本,提供了许多额外的功能和改进。

启动和退出

打开文件:进入正常模式

1
vim filename

退出Vim:

  • 进入命令模式(按 Esc 键),然后输入 :q,按 Enter;按 u 来撤销上一步操作。
  • 如果有未保存的更改,使用 :q! 来强制退出,或者 :wq 保存并退出。

模式驱动

  1. 普通模式:在这个模式下,键盘输入被解释为命令。你可以使用这些命令来移动光标、删除文本、复制粘贴等。
  2. 插入模式:在这个模式下,你可以输入文本。按下 “i” 进入插入模式,按 “Esc” 退出插入模式并返回到普通模式。
  3. 可视模式:这模式下,你可以选择文本块以进行复制、剪切或其他操作。按下 “v” 进入可视模式,然后使用移动命令来选择文本。
  4. 命令行模式:在这个模式下,你可以输入各种命令,例如保存文件、退出 Vim、搜索等。按下 “:” 进入命令行模式。
  5. 选择模式*与可视模式有些类似,但是在选择后直接进入插入模式。按下 “v” 进入可视模式,然后按 “Shift” 和 “v” 进入选择模式。

移动光标

  • 移动光标: h:左移 - j:下移 - k:上移 - l:右移

  • 移动到行首或行尾: 0:移动到行首 - $:移动到行尾

搜索和替换

  • 搜索: 在命令模式下,按 /,然后输入要搜索的文本,按 Enter

  • 替换: 在命令模式下,输入 :%s/old/new/g 来将所有匹配的 old 替换为 new

操 作 系 统

硬件结构

1.1 CPU是如何执行程序的

  • 图灵机:基本思想是用机器来模拟人们用纸笔进行数学运算的过程,而且还定义了计算机由哪些部分组成,程序又是如何执行的。

  • 冯诺依曼模型
    1945 年冯诺依曼和其他计算机科学家们提出了计算机具体实现的报告,其遵循了图灵机的设计,而且还提出用电子元件构造计算机,并约定了用二进制进行计算和存储,还定义计算机基本结构为 5 个部分,分别是中央处理器(CPU)、内存、输入设备、输出设备、总线。

    • 内存:我们的程序和数据都是存储在内存,存储的区域是线性的。数据存储的单位是一个二进制位(bit),即 0 或 1。最小的存储单位是字节(byte),1 字节等于 8 位。内存的地址是从 0 开始编号的,然后自增排列,最后一个地址为内存总字节数-1,这种结构好似我们程序里的数组,所以内存的读写任何一个数据的速度都是一样的。
    • 中央处理器:即 CPU,32 位和 64 位 CPU 最主要区别在于一次能计算多少字节数据:32 位 CPU 一次可以计算 4 个字节;64 位 CPU 一次可以计算 8 个字节;这里的 32 位和 64 位,通常称为 CPU 的位宽。这样设计,是为了能一次计算更大的数值, 8 位的 CPU一次只能计算 1 个字节0~255 范围内的数值,CPU 位宽越大,可以计算的数值就越大。
      CPU 内部还有一些组件,常见的有寄存器、控制单元和逻辑运算单元等。其中,控制单元负责控制 CPU工作,逻辑运算单元负责计算,而寄存器可以分为多种类,常见的寄存器种类:
      • 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
      • 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址。
      • 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。
    • 总线:用于 CPU 和内存以及其他设备之间的通信,总线可分为 3 种:
      • 地址总线,用于指定 CPU 将要操作的内存地址;
      • 数据总线,用于读写内存的数据;
      • 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线;
        当 CPU 要读写内存数据的时候,一般需要通过两个总线:首先要通过「地址总线」来指定内存的地址;再通过「数据总线」来传输数据;
    • 输入、输出设备:输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和 CPU 进行交互的,这时就需要用到控制总线了。
  • 线路位宽 与 CPU 位宽
    为了避免低效率的串行传输的方式,线路的位宽最好一次就能访问到所有的内存地址。 CPU 要想操作的内存地址就需要地址总线,如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种情况,所以 CPU 一次只能操作 2 个内存地址,如果想要 CPU 操作 4G 的内存,那么就需要 32 条地址总线,因为 2 ^ 32 =4G 。
    CPU 的位宽最好不要小于线路位宽,比如 32 位 CPU 控制 40 位宽的地址总线和数据总线的话,工作起来就会非常复杂且麻烦,所以 32 位的 CPU 最好和 32 位宽的线路搭配,因为 32 位 CPU 一次最多只能操作32 位宽的地址总线和数据总线。

  • 程序执行的基本过程
    程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。
    第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
    第二步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
    第三步,CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;

  • a = 1 + 2 执行具体过程(P13
    、、

1.2 存储器金字塔(小林p20)

、、

1.3 如何写出让 CPU 跑得更快的代码?(

,,

1.4 CPU缓存一致性(

、、

1.5 CPU 是如何执行任务的?(p65)

、、

1.6 软中断

。。


操作系统结构

Linux内核 vs Windows内核

,,


内存管理

虚拟内存

,,


进程与线程

,,


调度算法

,,


文件管理

,,


设备管理


网络系统

,,


Linux指令

,,

计 算 机 网 络

OSI 七层模型

OSI(开放系统互连)模型是计算机网络领域中的一个概念模型,将网络通信划分为七个层次,每个层次负责特定的功能。
每个层次都通过下一层次提供的服务来完成其功能。通过将网络协议和功能划分到不同的层次,OSI 模型使得网络设计和实现更加模块化和灵活,有助于不同厂商和组织开发兼容的网络设备和应用。
1、物理层(Physical Layer): 主要关注网络硬件和物理介质。定义了数据传输的物理特性,如电压、电流和传输介质。
2、数据链路层(Data Link Layer): 主要负责点对点的直接通信。 提供了物理层的上层接口,确保数据在相邻节点之间的可靠传输。主要包括两个子层:逻辑链路控制(LLC)和介质访问控制(MAC)。
3、网络层(Network Layer): 提供了在网络上寻址和路由的机制。负责将数据包从源节点传输到目标节点,可能需要通过多个中间节点。有许多不同的协议在这一层运行,最著名的是 IP。
4、传输层(Transport Layer): 主要负责端到端的通信。提供了端到端的可靠数据传输,如 TCP,UDP(不可靠但更高效)
5、会话层(Session Layer): 提供了建立、管理和终止会话的机制。管理数据的对话控制,确保通信的顺序性和同步性。
6、表示层(Presentation Layer): 负责数据的编码、解码和加密。 提供了在不同系统上进行数据格式转换的机制,以确保应用层能够正确地理解数据。
7、应用层(Application Layer): 最高层,直接为用户提供服务。 提供了网络服务和应用软件之间的接口,包括文件传输、电子邮件、远程登录等。

TCP/IP 模型

对于不同设备上的进程间通信,就需要⽹络通信。而要兼容多种多样的设备,就协商出了⼀套通⽤的⽹络协议。(OSI也是)
TCP/IP 模型是实际互联网中最广泛使用的模型,因为它直接映射到互联网的基本协议,其层次划分与 OSI 模型略有不同。
⽹络协议通常是由上到下,分成 5 层,分别是应⽤层,传输层,⽹络层,数据链路层和物理层。

  1. 应用层:提供网络服务和应用程序间的接口,使不同软件能够通过网络通信。当两个不同设备的应⽤需要通信的时候,应⽤就把应⽤数据传给下⼀层,也就是传输层;只需要专注于为⽤户提供应⽤功能,不⽤去关⼼数据是如何传输的。
    ⽽且应⽤层是⼯作在操作系统中的⽤户态,传输层及以下则⼯作在内核态。协议:HTTP,SMTP,DNS等。
  2. 传输层: 应⽤层的数据包会传给传输层,传输层是为应⽤层提供⽹络⽀持的。提供端到端的通信服务,确保数据的可靠传输。处理数据的分段和重新组装,同时提供错误检测和纠正。协议: 主要包括 TCP 和 UDP。
    • TCP 的全称叫传输层控制协议,⼤部分应⽤使⽤的正是 TCP 传输层协议,⽐如 HTTP 应⽤层协议。TCP 相⽐ UDP 多了很多特性,⽐如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对⽅。
    • UDP 相对简单,只负责发送数据包,不保证数据包是否能抵达对⽅,但它实时性相对更好,传输效率也⾼。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应⽤层上实现就可以。
    • 应⽤需要传输的数据可能会⾮常⼤,当传输层的数据包⼤⼩超过 MSS(TCP最⼤报⽂段⻓度)就要将数据包分块,这样即使中途有⼀个分块丢失或损坏了,只需要重新发送这⼀个分块⽽不是整个数据包。每个分块称为⼀个 TCP 段(TCP Segment)。
    • 当设备作为接收⽅时,传输层则要负责把数据包传给应⽤,但是⼀台设备上可能会有很多应⽤在接收或者传输数据,因此需要⽤⼀个编号将应⽤区分开来,这个编号就是端⼝。⽐如 80 端⼝通常是 Web 服务器⽤的,22 端⼝通常是远程登录服务器⽤的。⽽对于浏览器(客户端)中的每个标签栏都是⼀个独⽴的进程,操作系统会为这些进程分配临时的端⼝号。由于传输层的报⽂中会携带端⼝号,因此接收⽅可以识别出该报⽂是发送给哪个应⽤。
  3. 网络层: 处理主机之间的数据包路由,选择最佳路径将数据包从源主机传输到目标主机。协议:IP,ICMP,ARP等。
    • IP 协议会将传输层的报⽂作为数据部分,再加上 IP 包头组装成 IP 报⽂,如果 IP 报⽂⼤⼩超过 MTU(以太⽹中⼀般为1500 字节)就会再次进⾏分⽚,得到⼀个即将发送到⽹络的 IP 报⽂。
    • ⽤ IP地址给设备进⾏编号,对于 IPv4 协议,IP 地址共 32 位,分成四段,每段 8 位。将 IP 地址分成两种意义:⼀个是⽹络号,负责标识该 IP 地址是属于哪个⼦⽹的;⼀个是主机号,负责标识同⼀⼦⽹下的不同主机;这需要配合⼦⽹掩码才能算出 IP 地址 的⽹络号和主机号。那么在寻址的过程中,先匹配到相同的⽹络号,才会去找对应的主机。
    • 除了寻址能⼒, IP 协议还有另⼀个重要的能⼒就是路由。实际场景中,两台设备并不是⽤⼀条⽹线连接起来的,⽽是通过很多⽹关、路由器、交换机等众多⽹络设备连接起来的,那么就会形成很多条⽹络的路径,因此当数据包到达⼀个⽹络节点,就需要通过算法决定下⼀步⾛哪条路径。
    • 所以,IP 协议的寻址作⽤是告诉我们去往下⼀个⽬的地该朝哪个⽅向⾛,路由则是根据「下⼀个⽬的地」选择路径。寻址更像在导航,路由更像在操作⽅向盘。
  4. 链路层: 需要有⼀个专⻔的层来标识⽹络中的设备,让数据在⼀个链路中传输,这就是数据链路层,它主要为⽹络层提供链路级别传输的服务。(负责在物理网络介质上传输数据帧。它包括了对硬件的驱动程序和操作系统中的网络接口卡(NIC)
    • 协议: 以太网、Wi-Fi、PPP 等。
    • 每⼀台设备的⽹卡都会有⼀个 MAC 地址,它就是⽤来唯⼀标识设备的。路由器计算出了下⼀个⽬的地 IP 地址,再通过 ARP 协议找到该⽬的地的 MAC 地址,这样就知道这个 IP 地址是哪个设备的了。
  5. 物理层:当数据准备要从设备发送到⽹络时,需要把数据包转换成电信号,让其可以在物理介质中传输,这⼀层就是物理层,它主要是为数据链路层提供⼆进制传输的服务。

网络协议

网络协议是计算机网络中用于通信的规则和约定的集合。它们定义了在通信系统中数据如何被传输、编码、压缩、路由和解码。网络协议是计算机网络正常运行的基础,它确保了不同设备和应用程序之间的互操作性。以下是一些常见的网络协议:

  1. HTTP (Hypertext Transfer Protocol):
    描述:用于在Web浏览器和Web服务器之间传输超文本的应用层协议。 特点:无状态协议,通常基于请求-响应模型。
    HTTPS (Hypertext Transfer Protocol Secure):
    描述:HTTP的安全版本,通过使用TLS/SSL协议进行加密,提供数据的安全性和隐私。
    特点:加密通信,用于保护敏感信息,如登录凭证和支付信息。
  2. TCP (Transmission Control Protocol):
    描述:面向连接的传输层协议,提供可靠的、有序的、基于字节流的数据传输。
    特点:建立连接、数据传输完整性、流量控制、拥塞控制。
    UDP (User Datagram Protocol):
    描述:面向无连接的传输层协议,提供不可靠但高效的数据传输。 特点:无连接、不提供流控制、不保证数据完整性。
  3. IP (Internet Protocol):
    描述:定义了在网络上发送和接收数据的方式,是一种网络层协议。 特点:用于标识和定位设备,IPv4和IPv6是最常见的版本。
  4. DNS (Domain Name System):
    描述:用于将域名映射到IP地址的分布式数据库系统。 特点:提供域名解析服务,将易记的域名转换为数值型IP地址。
  5. FTP (File Transfer Protocol):
    用于在计算机之间传输文件的标准网络协议。 支持文件上传、下载、删除等操作,可以进行匿名访问或需要身份验证。
  6. SMTP (Simple Mail Transfer Protocol): 在计算机网络上传递和传输电子邮件的协议。 定义了邮件的发送规则,常用于发送邮件。
    POP3 (Post Office Protocol 3): 从远程服务器上下载电子邮件到本地计算机的协议。通常用于接收邮件,支持在线和离线模式。
    IMAP (Internet Message Access Protocol): 与POP3类似,也是用于从远程服务器上下载电子邮件的协议,但提供更多的功能。 支持在邮件服务器上管理邮件,可以通过多个设备同步邮件状态。

网络攻击

  1. DDoS 攻击 (Distributed Denial of Service): 攻击者试图通过将大量伪造的请求发送到目标系统,使其超负荷,导致服务不可用。
  2. 恶意软件 (Malware): 恶意软件是一类有意设计用于破坏、干扰计算机系统或窃取信息的软件,包括病毒、蠕虫、木马等。
  3. 钓鱼攻击 (Phishing): 攻击者伪装成可信任的实体,通过虚假的电子邮件、网站等手段,诱导用户揭示敏感信息如用户名、密码等。
  4. 社会工程学攻击: 攻击者利用心理学和人类行为来欺骗、迫使或引导个人执行某些操作,通常涉及人际交往,以获取机密信息。
  5. SQL 注入攻击: 攻击者通过在用户输入的数据中注入 SQL 代码,从而执行恶意 SQL 查询,破坏数据库的完整性或获取敏感信息。
  6. 跨站脚本攻击 (XSS): 攻击者将恶意脚本注入到网页中,使用户在浏览时执行这些脚本,从而窃取用户信息或劫持用户会话。
  7. 跨站请求伪造 (CSRF): 攻击者通过伪造用户已经认证的请求,利用用户的身份执行未经授权的操作。
  8. 中间人攻击 (Man-in-the-Middle): 攻击者截取和修改通信数据流,使得通信的两个实体认为他们正在直接通信。
  9. DNS 缓存投毒 (DNS Spoofing): 攻击者通过篡改 DNS 查询结果,将用户导向恶意网站。

HTTP

2.1 HTTP 常见面试题

2.1.1 HTTP 基本概念

  • HTTP 是?
    超⽂本传输协议,也就是HyperText Transfer Protocol。超⽂本,它就是超越了普通⽂本的⽂本,它是⽂字、图⽚、视频等的混合体,最关键有超链接,能从⼀个超⽂本跳转到另外⼀个超⽂本。HTML 是最常⻅的超⽂本,本身只是纯⽂字⽂件,但内部⽤很多标签定义了图⽚、视频等的链接,再经过浏览器的解释呈现出⼀个⽂字、有画⾯的⽹⻚。
    HTTP 是⼀个在计算机世界⾥专⻔在「两点」之间「传输」⽂字、图⽚、⾳频、视频等「超⽂本」数据的「约定和规范」。
    1
    2
    3
    4
    5
    6
    7
    8
    # 请求报文: 由请求行、请求头、空行和请求体组成。
    GET /index.html HTTP/1.1
    Host: www.example.com
    Connection: keep-alive
    # 响应报文: 由状态行、响应头、空行和响应体组成。
    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 123
  • 常见状态码
    • 1xx:1xx 类状态码属于提示信息,是协议处理中的⼀种中间状态,实际⽤到的⽐较少。
    • 2xx:2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
      「200 OK」是最常⻅的成功状态码,表示⼀切正常。如果是⾮ HEAD 请求,服务器返回的响应头都会有 body 数据。
      「204 No Content」也是常⻅的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
      「206 Partial Content」是应⽤于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部⽽是其⼀部分,也是服务器处理成功的状态。
    • 3xx:表示客户端请求的资源发送了变动,需要客户端⽤新的 URL 新发送请求获取资源,也就是重定向
      「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改⽤新的 URL 再次访问。
      「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要⽤另⼀个 URL 来访问。
      301 和 302 都会在响应头⾥使⽤字段 Location ,指明后续要跳转的 URL,浏览器会⾃动重定向新的 URL。
      「304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲⽂件,也称缓存重定向,⽤于缓存控制。
    • 4xx:表示客户端发送的报⽂有误,服务器⽆法处理,也就是错误码的含义。
      「400 Bad Request」表示客户端请求的报⽂有错误,但只是个笼统的错误。
      「403 Forbidden」表示服务器禁⽌访问资源,并不是客户端的请求出错。
      「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以⽆法提供给客户端。
    • 5xx:表示客户端请求报⽂正确,但是服务器处理时内部发⽣了错误,属于服务器端的错误码。
      「500 Internal Server Error」与 400 类型,是个笼统通⽤的错误码,服务器发⽣了什么错误,我们并不知道。
      「501 Not Implemented」表示客户端请求的功能还不⽀持,类似“即将开业,敬请期待”的意思。
      「502 Bad Gateway」通常是服务器作为⽹关或代理时返回的错误码,表示服务器⾃身⼯作正常,访问后端服务器发⽣错误。
      「503 Service Unavailable」表示服务器当前很忙,暂时⽆法响应服务器,类似“⽹络服务正忙,请稍后重试”。
  • http 常⻅字段有哪些?
    • Host 字段:客户端发送请求时,⽤来指定服务器的域名。
    • Content-Length 字段:服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据⻓度。
    • Connection 字段:最常⽤于客户端要求服务器使⽤ TCP 持久连接,以便其他请求复⽤。HTTP/1.1 版本的默认连接都是持久连接,但为了兼容⽼版本的 HTTP,需要指定 Connection ⾸部字段的值为 Keep-Alive 。
      Connection: keep-alive,⼀个可以复⽤的 TCP 连接就建⽴了,直到客户端或服务器主动关闭连接。但是这不是标准字段。
    • Content-Type 字段:⽤于服务器回应时,告诉客户端,本次数据是什么格式。Content-Type: text/html; harset=utf-8 表明发送的是⽹⻚,⽽且编码是UTF-8。
      客户端请求的时候,可以使⽤ Accept 字段声明⾃⼰可以接受哪些数据格式。
    • Content-Encoding 字段:说明数据的压缩⽅法。Content-Encoding: gzip,表示服务器返回的数据采⽤了 gzip ⽅式压缩,告知客户端需要⽤此⽅式解压。
      客户端在请求时,⽤ Accept-Encoding 字段说明⾃⼰可以接受哪些压缩⽅法。
  • Cookie和Session:
    • Cookie: 由服务器发送到浏览器,保存在浏览器上。包含了与用户相关的信息,如用户ID、会话ID等。
    • Session: 在服务器端保存用户状态的机制,通常使用Cookie来标识用户。
  • RESTful API: 基于HTTP协议设计的一种简洁和统一的Web服务风格。

2.1.2 Get 与 Post

  • GET 和 POST 的区别?
    Get ⽅法的含义是请求从服务器获取资源,这个资源可以是静态的⽂本、⻚⾯、图⽚视频等。
    POST ⽅法则是相反操作,它向 URI 指定的资源提交数据,数据就放在报⽂的 body ⾥。
  • GET 和 POST ⽅法都是安全和幂等的吗?
    在 HTTP 协议⾥,所谓的「安全」是指请求⽅法不会破坏服务器上的资源,「幂等」意思是多次执⾏相同操作结果都相同。
    那么很明显 GET ⽅法就是安全且幂等的,因为它是只读操作,⽆论操作多少次,服务器上的数据都是安全的且每结果都相同。
    因为POST 是新增或提交数据的操作,会修改服务器上的资源,所以不安全,且多次提交数据就会创建多个资源所以不是幂等的。

2.1.3 HTTP 特性

  • HTTP(1.1) 的优点有哪些,怎么体现的?
    1. 简单:HTTP 基本的报⽂格式就是 header + body ,头部信息也是 key-value 简单⽂本的形式,易于理解和使用
    2. 灵活和易于扩展:HTTP协议⾥的各类请求⽅法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发⼈员⾃定义和扩充。同时 HTTP 由于是⼯作在应⽤层(OSI 第七层),则它下层可以随意变化。
      HTTPS 也就是在 HTTP 与 TCP 层之间增加了SSL/TLS安全传输层,HTTP/3甚⾄把 TCP层换成了基于 UDP 的 QUIC。
    3. 应⽤⼴泛和跨平台:互联⽹发展⾄今,HTTP 的应⽤范围⾮常的⼴泛,从台式机的浏览器到⼿机上的各种 APP,从看新闻、刷贴吧到购物、理财、吃鸡,HTTP 的应⽤⽚地开花,同时天然具有跨平台的优越性。
  • HTTP的缺点?
    1. ⽆状态双刃剑:⽆状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,把更多的 CPU 和内存⽤来对外提供服务。⽆状态的坏处,既然服务器没有记忆能⼒,它在完成有关联性的操作时会⾮常麻烦。对于⽆状态的问题,解法⽅案有很多种,其中⽐较简单的⽅式⽤ Cookie 技术:通过在请求和响应报⽂中写⼊ Cookie 信息来控制客户端的状态。
    2. 明⽂传输双刃剑:明⽂意味着在传输过程中的信息,是可⽅便阅读的,通过浏览器的 F12 控制台或 Wireshark 抓包都可以直接⾁眼查看,为调试⼯作带了极⼤便利。但正是这样,HTTP 的所有信息都暴露在了光天化⽇下,相当于信息裸奔。
    3. 不安全:HTTP 的安全问题,可以⽤ HTTPS 的⽅式解决,也就是通过引⼊ SSL/TLS 层,使得在安全上达到了极致。
      • 通信使⽤明⽂(不加密),内容可能会被窃听。⽐如,账号信息容易泄漏,那你号没了。
      • 不验证通信⽅的身份,因此有可能遭遇伪装。⽐如,访问假的淘宝、拼多多,那你钱没了。
      • ⽆法证明报⽂的完整性,所以有可能已遭篡改。⽐如,⽹⻚上植⼊垃圾⼴告,视觉污染,眼没了。
  • HTTP/1.1 的性能如何?
    HTTP 协议是基于 TCP/IP,并且使⽤了「请求 - 应答」的通信模式,所以性能的关键就在这两点⾥。
    1. ⻓连接:早期 HTTP/1.0 性能上的⼀个很⼤的问题,那就是每发起⼀个请求,都要新建⼀次 TCP 连接,⽽且是串⾏请求,做了⽆谓的 TCP 连接建⽴(三次握⼿)和断开(四次挥手)。为了解决上述问题,HTTP/1.1 提出了⻓连接的通信⽅式,也叫持久连接,好处在于减少了TCP 连接的重复建⽴和断开所造成的额外开销,减轻了服务器端的负载。
      持久连接的特点是,只要任意⼀端没有明确提出断开连接,则保持 TCP 连接状态。
    2. 管道⽹络传输:HTTP/1.1 采⽤了⻓连接的⽅式,这使得管道(pipeline)⽹络传输成为了可能。即可在同⼀个 TCP 连接⾥⾯,客户端可以发起多个请求,只要第⼀个请求发出去了,不必等其回来,就可以发第⼆个请求出去,可以减少整体的响应时间。
      但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求,可能导致「队头堵塞」。
    3. 队头阻塞:「请求 - 应答」的模式加剧了 HTTP 的性能问题。 因为当顺序发送的请求序列中的⼀个请求因为某种原因被阻塞时,在后⾯排队的所有请求也⼀同被阻塞了,会招致客户端⼀直请求不到数据。
      总之 HTTP/1.1 的性能⼀般般,后续的 HTTP/2 和 HTTP/3 就是在优化 HTTP 的性能。

2.1.4 HTTPS 与 HTTP(小林p27)

  • HTTP 与 HTTPS 有哪些区别?
    1. HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
    2. HTTP 连接建⽴相对简单,TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。⽽ HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
    3. HTTP 的端⼝号是 80,HTTPS 的端⼝号是 443。
    4. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
  • HTTPS 解决了 HTTP 的哪些问题?
    HTTP 由于是明⽂传输,所以安全上存在以下三个⻛险:
    1. 窃听⻛险,⽐如通信链路上可以获取通信内容,⽤户号容易没。
    2. 篡改⻛险,⽐如强制植⼊垃圾⼴告,视觉污染,⽤户眼容易瞎。
    3. 冒充⻛险,⽐如冒充淘宝⽹站,⽤户钱容易没。
      HTTPS 在 HTTP 与 TCP 层之间加⼊了 SSL/TLS 协议,可以很好的解决了上述的⻛险。信息加密:交互信息⽆法被窃取;校验机制:⽆法篡改通信内容,篡改了就不能正常显示;身份证书:证明淘宝是真的淘宝⽹。。
  • HTTPS 是如何解决上⾯的三个⻛险的?
    1. 混合加密:通过混合加密的⽅式可以保证信息的机密性,解决了窃听的⻛险。
      ???HTTPS 采⽤的是对称加密和⾮对称加密结合的「混合加密」⽅式:在通信建⽴前采⽤⾮对称加密的⽅式交换「会话秘钥」,后续就不再使⽤⾮对称加密; 在通信过程中全部使⽤对称加密的「会话秘钥」的⽅式加密明⽂数据。
      采⽤「混合加密」的⽅式的原因:对称加密只使⽤⼀个密钥,运算速度快,密钥必须保密,⽆法做到安全的密钥交换;⾮对称加密使⽤两个密钥:公钥和私钥,公钥可以任意分发⽽私钥保密,解决了密钥交换问题但速度慢。
    2. 摘要算法:摘要算法能够为数据⽣成独⼀⽆⼆的「指纹」,⽤于校验数据的完整性,解决了篡改的⻛险。
      客户端在发送明⽂之前会通过摘要算法算出明⽂的「指纹」,发送的时候把「指纹 + 明⽂」⼀同加密成密⽂后,发送给服务器,服务器解密后,⽤相同的摘要算法算出发送过来的明⽂,通过⽐较客户端携带的「指纹」和当前算出的「指纹」做⽐较,若「指纹」相同,说明数据是完整的。
    3. 数字证书:客户端先向服务器端索要公钥,然后⽤公钥加密信息,服务器收到密⽂后,⽤⾃⼰的私钥解密。这就存在些问题,如何保证公钥不被篡改和信任度?借助第三⽅权威机构 CA(数字证书认证机构),将服务器公钥放在数字证书(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。
  • ???HTTPS 是如何建⽴连接的?其间交互了什么?
    SSL/TLS 协议基本流程:客户端向服务器索要并验证服务器的公钥、双⽅协商⽣产「会话秘钥」、双⽅采⽤「会话秘钥」进⾏加密通信。前两步也就是 SSL/TLS 的建⽴过程,也就是握⼿阶段。
    SSL/TLS 的「握⼿阶段」涉及四次通信,可⻅下图:p32
  • “https和http相⽐,就是传输的内容多了对称加密,可以这么理解吗?”
    建⽴连接时候:https ⽐ http多了 TLS 的握⼿过程;
    传输内容的时候:https 会把数据进⾏加密,通常是对称加密数据;
  • “TLS 和 SSL 需要区分吗?”
    这两实际上是⼀个东⻄。SSL 是 “Secure Sockets Layer 的缩写,中⽂叫做「安全套接层」,SSL 标准化之后的名称改为 TLS(是 “Transport Layer Security” 的缩写),中⽂叫做 「传输层安全协议」;两者可以视作同⼀个东⻄的不同阶段。
  • ???“为啥 ssl 的握⼿是 4 次?”
    SSL/TLS 1.2 需要 4 握⼿,需要 2 个 RTT 的时延。把每个交互合在⼀起发送,就是 4 次握⼿:p40

2.1.5 HTTP/1.1、HTTP/2、HTTP/3 演变

  • 说说 HTTP/1.1 相⽐ HTTP/1.0 提⾼了什么性能?
    性能上的改进:1、使⽤ TCP ⻓连接的⽅式改善了 HTTP/1.0 短连接造成的性能开销。2、⽀持管道(pipeline)⽹络传输,只要第⼀个请求发出去了,不必等其回来,就可以发第⼆个请求出去,可以减少整体的响应时间。
    性能瓶颈:1、请求 / 响应头部(Header)未经压缩就发送,⾸部信息越多延迟越⼤。只能压缩 Body 的部分;2、发送冗⻓的⾸部。每次互相发送相同的⾸部造成的浪费较多;3、服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端⼀直请求不到数据,也就是队头阻塞;4、没有请求优先级控制;5、请求只能从客户端开始,服务器只能被动响应。
  • 对于上⾯的 HTTP/1.1 的性能瓶颈,HTTP/2 做了什么优化?
    HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。HTTP/2 相⽐ HTTP/1.1 性能上的改进:
    1. 头部压缩:HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是⼀样的或是相似的,那么,协议会帮你消除重
      复的部分。这就是所谓的 HPACK 算法:在客户端和服务器同时维护⼀张头信息表,所有字段都会存⼊这个表,⽣成⼀个索引号,以后就不发送同样字段了,只发送索引号,这样就提⾼速度了。
    2. ⼆进制格式:HTTP/2 不再像 HTTP/1.1 ⾥的纯⽂本形式的报⽂,⽽是全⾯采⽤了⼆进制格式,头信息和数据体都是⼆进制,并且统称为帧(frame):头信息帧和数据帧。计算机收到报⽂后,⽆需再将明⽂的报⽂转成⼆进制,⽽是直接解析⼆进制报⽂,这增加了数据传输的效率。
    3. 数据流:HTTP/2 的数据包不是按顺序发送的,同⼀个连接⾥⾯连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。每个请求或回应的所有数据包,称为⼀个数据流(Stream)。每个数据流都标记着⼀个独⼀⽆⼆的编号,其中规定客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。
      客户端还可以指定数据流的优先级。优先级⾼的请求,服务器就先响应该请求。
    4. 多路复⽤:HTTP/2 是可以在⼀个连接中并发多个请求或回应,⽽不⽤按照顺序⼀⼀对应。
      移除了 HTTP/1.1 中的串⾏请求,不需要排队等待,也就不会再出现队头阻塞问题,降低延迟,⼤幅度提⾼连接的利⽤率。
    5. 服务器推送:HTTP/2 ⼀定程度上改善了传统的「请求 - 应答」⼯作模式,服务不再被动响应,也可主动向客户端发送消息。
  • HTTP/2 有哪些缺陷?HTTP/3 做了哪些优化?
    HTTP/2 主要的问题在于,多个 HTTP 请求在复⽤⼀个 TCP 连接,下层的 TCP 协议是不知道有多少个 HTTP 请求的。⼀旦发⽣了丢包,就会触发 TCP 的重传机制,在⼀个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来。
    • HTTP/1.1 中的管道( pipeline)传输中如果有⼀个请求阻塞了,那么队列后请求也统统被阻塞住了
    • HTTP/2 多个请求复⽤⼀个TCP连接,⼀旦发⽣丢包,就会阻塞住所有的 HTTP 请求。
      这都是基于 TCP 传输层的问题,所以 HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!(⼤家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输)

2.2 HTTP/1.1如何优化?

使⽤ KeepAlive 将 HTTP/1.1 从短连接改成⻓链接。这个确实是⼀个优化的⼿段,它是从底层的传输层这⼀⽅向⼊⼿的,通过减少 TCP 连接建⽴和断开的次数,来减少了⽹络传输的延迟,从⽽提⾼ HTTP/1.1 协议的传输效率。
但其实还可以从其他⽅向来优化 HTTP/1.1 协议,⽐如有如下 3 种优化思路:

  1. 尽量避免发送 HTTP 请求;
    对于⼀些具有重复性的 HTTP 请求,⽐如每次请求得到的数据都⼀样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过⽹络获取服务器的响应了,这样的话 HTTP/1.1的性能肯定⾁眼可⻅的提升。
    所以,避免发送 HTTP 请求的⽅法就是通过缓存技术,HTTP 设计者早在之前就考虑到了这点,因此 HTTP 协议的头部有不少是针对缓存的字段。
    那缓存是如何做到的呢?客户端会把第⼀次请求以及响应的数据保存在本地磁盘上,其中将请求的 URL 作为 key,⽽响应作为 value,两者形成映射关系。这样当后续发起相同的请求时,就可以先在本地磁盘上通过 key 查到对应的 value,也就是响应,如果找到了,就直接从本地读取该响应。毋庸置疑,读取本次磁盘的速度肯定⽐⽹络请求快得多,如下图:
    缓存的响应不是最新的?服务器在发送 HTTP 响应时,会估算⼀个过期的时间,并把这个信息放到响应头部中,这样客户端在查看响应头部的信息时,⼀旦发现缓存的响应是过期的,则就会重新发送⽹络请求。
    如果客户端从第⼀次请求得到的响应头部中发现该响应过期了,客户端重新发送请求,假设服务器上的资源并没有变更,还是⽼样⼦,那么就不用在服务器的响应带上这个资源。只需要客户端在重新发送请求时,在请求的 Etag 头部带上第⼀次请求的响应头部中的摘要,这个摘要是唯⼀标识响应的资源,当服务器收到请求后,会将本地资源的摘要与请求中的摘要做个⽐较。
    如果不同,那么说明客户端的缓存已经没有价值,服务器在响应中带上最新的资源。如果相同,说明客户端的缓存还是可以继续使⽤的,那么服务器仅返回不含有包体的 304 Not Modified 响应,告诉客户端仍然有效,这样就可以减少响应资源在⽹络中传输的延时。
  2. 在需要发送 HTTP 请求时,考虑如何减少请求次数;(p47)
    • 减少重定向请求次数:如果重定向请求越多,那么客户端就要多次发起 HTTP 请求;另外,服务端这⼀⽅往往不只有⼀台服务器,⽐如源服务器上⼀级是代理服务器,然后代理服务器才与客户端通信,这时客户端重定向就会导致客户端与代理服务器之间需要 2 次消息传递。
      如果重定向的⼯作交由代理服务器完成,就能减少 HTTP 请求次数了;⽽且当代理服务器知晓了重定向规则后,可以进⼀步减少消息传递次数
    • 合并请求:如果把多个访问⼩⽂件的请求合并成⼀个⼤的请求,虽然传输的总资源还是⼀样,但是减少请求,也就意味着减少了重复发送的 HTTP 头部。另外由于 HTTP/1.1 是请求响应模型,如果第⼀个发送的请求,未收到对应的响应,那么后续的请求就不会发送,于是为了防⽌单个请求的阻塞,所以⼀般浏览器会同时发起 5-6 个请求,每⼀个请求都是不同的 TCP 连接,那么如果合并了请求,也就会减少 TCP 连接的数量,因⽽省去了 TCP 握⼿和慢启动过程耗费的时间。
      即合并请求的⽅式就是合并资源,以⼀个⼤资源的请求替换多个⼩资源的请求。但是这样会带来新的问题,当⼤资源中的某⼀个⼩资源发⽣变化后,客户端必须重新下载整个完整的⼤资源⽂件,这显然带来了额外的⽹络消耗。
    • 延迟发送请求:不要⼀⼝⽓吃成⼤胖⼦,⼀般 HTML ⾥会含有很多 HTTP 的 URL,当前不需要的资源,我们没必要也获取过来,于是可以通过「按需获取」的⽅式,来减少第⼀时间的 HTTP 请求次数。请求⽹⻚的时候,没必要把全部资源都获取到,⽽是只获取当前⽤户所看到的⻚⾯资源,当⽤户向下滑动⻚⾯的时候,再向服务器获取接下来的资源,这样就达到了延迟发送请求的效果。
  3. 减少服务器的 HTTP 响应的数据⼤⼩;
    对响应的资源进⾏压缩,这样就可以减少响应的数据⼤⼩,从⽽提⾼⽹络传输的效率。压缩的⽅式⼀般分为 2 种,分别是:
    • ⽆损压缩:指资源经过压缩后,信息不被破坏,还能完全恢复到压缩前的原样,适合⽤在txt、exe、程序源代码。
      gzip 就是⽐较常⻅的⽆损压缩。客户端⽀持的压缩算法,会在 HTTP 请求中通过头部中的 Accept-Encoding: gzip, deflate, br 字段告诉服务器;服务器收到后,会从中选择⼀个服务器⽀持的或者合适的压缩算法,然后使⽤此压缩算法对响应资源进⾏压缩,最后通过响应头部中的 content-encoding: gzip 字段告诉客户端该资源使⽤的压缩算法。
    • 有损压缩:将次要的数据舍弃,牺牲⼀些质量来减少数据量、提⾼压缩⽐,解压的数据会与原始数据不同但是⾮常接近,经常⽤于压缩多媒体数据⽐如⾳频、视频、图⽚。
      可以通过 HTTP 请求头部中的 Accept: audio/*; q=0.2, audio/basic,告诉服务器期望的资源质量。
      相同图⽚质量下,WebP 格式的图⽚⼤⼩都⽐ Png 格式的图⽚⼩,网站可以考虑使⽤ WebP 格式的图⽚。
      ⾳视频主要是动态的,每个帧都有时序的关系,通常时间连续的帧之间的变化很⼩。只需在⼀个静态的关键帧,使⽤增量数据来表达后续的帧,这样便减少了很多数据。视频常⻅的编码格式有 H264、H265 等,⾳频常⻅的编码格式有 AAC、AC3。

2.3 HTTPS RSA 握⼿解析(p55)

  • ???TLS 的握⼿过程
    有了 TLS 协议,能保证 HTTP 通信是安全的了,那么在进⾏ HTTP 通信前,需要先进⾏ TLS 握⼿。,如下图

2.4 HTTPS ECDHE 握⼿解析(p?)

  • ???

2.5 HTTPS 如何优化?

由裸数据传输的 HTTP 协议转成加密数据传输的 HTTPS 协议,给应⽤数据套了个「保护伞」,提⾼安全性的同时也带来了性能消耗。HTTPS 相⽐ HTTP 协议多⼀个 TLS 协议握⼿过程,⽬的是为了通过⾮对称加密握⼿协商或者交换出对称加密密钥,这个过程最⻓可以花费掉 2 RTT,接着后续传输的应⽤数据都得使⽤对称加密密钥来加密/解密。
⾄今⼤部分⽹址都已从 HTTP 迁移⾄ HTTPS 协议,因此针对HTTPS 的优化是⾮常重要的。
。。。


TCP

3.1 TCP 三次握⼿与四次挥⼿(小林p122)

  • TCP头格式
    1. 序列号:在建⽴连接时由计算机⽣成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送⼀次数据,就「累加」⼀次该「数据字节数」的⼤⼩。⽤来解决⽹络包乱序问题。
    2. 确认应答号:指下⼀次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。⽤来解决不丢包的问题。
    3. 控制位:
      ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建⽴连接时的 SYN 包之外该位必须设置为 1 。
      RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
      SYN:该位为 1 时,表示希望建⽴连接,并在其「序列号」的字段进⾏序列号初始值的设定。
      FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
  • 为什么需要 TCP 协议? TCP ⼯作在哪⼀层?
    IP 层是「不可靠」的,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中的数据的完整性。
    如果需要保障⽹络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。TCP 是⼀个⼯作在传输层的可靠数据传输的服务,它能确保接收端接收的⽹络包是⽆损坏、⽆间隔、⾮冗余和按序的。
  • 什么是 TCP ?
    TCP 是⾯向连接的、可靠的、基于字节流的传输层通信协议。
    ⾯向连接:⼀定是「⼀对⼀」才能连接,不能像 UDP 协议可以⼀个主机同时向多个主机发送消息,也就是⼀对多是⽆法做到的;
    可靠的:⽆论的⽹络链路中出现了怎样的链路变化,TCP 都可以保证⼀个报⽂⼀定能够到达接收端;
    字节流:消息是「没有边界」的,所以⽆论我们消息有多⼤都可以进⾏传输。并且消息是「有序的」,当「前⼀个」消息没有收到的时候,即使它先收到了后⾯的字节,那么也不能扔给应⽤层去处理,同时对重复的报⽂会⾃动丢弃
  • 什么是 TCP 连接?
    简单来说就是,⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗⼝⼤⼩称为连接。
    建⽴⼀个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
    Socket:由 IP 地址和端⼝号组成;序列号:⽤来解决乱序问题等;窗⼝⼤⼩:⽤来做流量控制
  • 如何唯⼀确定⼀个 TCP 连接呢?
    TCP 四元组可以唯⼀的确定⼀个连接,四元组包括:源地址、源端⼝、⽬的地址、⽬的端⼝
    源地址和⽬的地址的字段(32位)是在 IP 头部中,作⽤是通过 IP 协议发送报⽂给对⽅主机。
    源端⼝和⽬的端⼝的字段(16位)是在 TCP 头部中,作⽤是告诉 TCP 协议应该把报⽂发给哪个进程。
  • 有⼀个 IP 的服务器监听了⼀个端⼝,它的 TCP 的最⼤连接数是多少?
    服务器通常固定在某个本地端⼝上监听,等待客户端的连接请求。因此,客户端 IP 和 端⼝是可变的,其理论值计算公式如下
    最大 TCP 连接数 = 客户端的 IP 数 * 客户端的端口数
  • UDP 协议?
    UDP 不提供复杂的控制机制,利⽤ IP 提供⾯向「⽆连接」的通信服务。UDP 协议非常简单,头部只有 8 个字节( 64 位),
    ⽬标和源端⼝:主要是告诉 UDP 协议应该把报⽂发给哪个进程。包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计。
  • UDP 和 TCP 有什么区别呢?
    1. 连接:TCP 是⾯向连接的传输层协议,传输数据前先要建⽴连接。UDP 是不需要连接,即刻传输数据。
    2. 服务对象:TCP 是⼀对⼀的两点服务,即⼀条连接只有两个端点。UDP ⽀持⼀对⼀、⼀对多、多对多的交互通信
    3. 可靠性:TCP是可靠交付数据的,数据可以⽆差错、不丢失、不重复、按需到达。UDP尽最⼤努⼒,不保证可靠交付数据。
    4. 拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使⽹络⾮常拥堵了,也不会影响 UDP 的发送速率。
    5. ⾸部开销:TCP ⾸部⻓度较⻓,会有⼀定的开销,⾸部在没有使⽤「选项」字段时是 20 个字节,如果使⽤了「选项」字段则会变⻓的。UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
    6. 传输⽅式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是⼀个包⼀个包的发送,是有边界的,但可能丢包和乱序。
    7. 分⽚不同:
      • TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩,则会在传输层进⾏分⽚,⽬标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了⼀个分⽚,只需要传输丢失的这个分⽚。
      • UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩,则会在 IP 层进⾏分⽚,⽬标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了⼀个分⽚,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样传输效率⾮常差,所以通常 UDP 的报⽂应该⼩于 MTU。
  • TCP 和 UDP 应⽤场景:
    由于 TCP 是⾯向连接,能保证数据的可靠性交付,因此经常⽤于:FTP ⽂件传输,HTTP / HTTPS
    由于 UDP ⾯向⽆连接,它可以随时发送数据,再加上UDP本身的处理既简单⼜⾼效,因此经常⽤于:包总量较少的通信,如 DNS、 SNMP 等,视频、⾳频等多媒体通信,⼴播通信
  • 为什么 UDP 头部没有「⾸部⻓度」字段,⽽ TCP 头部有「⾸部⻓度」字段呢?
    原因是 TCP 有可变⻓的「选项」字段,⽽ UDP 头部⻓度则是不会变化的,⽆需多⼀个字段去记录 UDP 的⾸部⻓度。
  • 为什么 UDP 头部有「包⻓度」字段,⽽ TCP 头部则没有「包⻓度」字段呢?
    先说说 TCP 是如何计算负载数据⻓度:???

TCP 建立(p131)

  • TCP 三次握⼿过程和状态变迁
    TCP 是⾯向连接的协议,所以使⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏的。
    1. ⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态
    2. 客户端会随机初始化序号(client_isn),将此序号置于 TCP ⾸部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表示向服务端发起连接,不包含应⽤层数据,之后客户端处于 SYN-SENT 状态。
    3. 服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn),将此序号填⼊TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1 , 接着把 SYN和 ACK 标志位置为 1 。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,之后服务端处于 SYN-RCVD 状态。
    4. 客户端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部 ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报⽂发送给服务端,这次报⽂可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。(可以发现第三次握⼿是可以携带数据的,前两次握⼿是不可以携带数据的,这也是⾯试常问的题。
    5. 服务器收到客户端的应答报⽂后,也进⼊ ESTABLISHED 状态。⼀旦完成三次握⼿,双⽅都处于 ESTABLISHED 状态,此时连接就已建⽴完成,客户端和服务端就可以相互发送数据了。

3.2 TCP 重传、滑动窗口、流量控制、拥塞控制(小林p?)

-

3.3 TCP 实战抓包分析(小林p?)

-

3.4 TCP 半链接队列和全连接队列(小林p?)

-

3.5 TCP 内核参数(小林p?)

-


四、IP

4.1 IP基础知识

4.1.1 IP 基本认识(p314

  • ⽹络层的主要作⽤是:实现主机与主机之间的通信,也叫点对点(end to end)通信。
    IP 的作用是在复杂的网络环境中将数据包发送给最终目的主机。

  • ⽹络层与数据链路层有什么关系呢?
    IP 的作⽤是主机之间通信⽤的,⽽ MAC 的作⽤则是实现「直连」的两个设备之间通信,⽽ IP 则负责在「没有直连」的两个⽹络之间进⾏通信传输。

    如果⼩林只有⾏程表⽽没有⻋票,就⽆法搭乘交通⼯具到达⽬的地。相反,如果除了⻋票⽽没有⾏程表,恐怕也很难到达⽬的地。因为⼩林不知道该坐什么⻋,也不知道该在哪⾥换乘。因此,只有两者兼备,既有某个区间的⻋票⼜有整个旅⾏的⾏程表,才能保证到达⽬的地。与此类似,计算机⽹络中也需要「数据链路层」和「⽹络层」这个分层才能实现向最终⽬标地址的通信。 还有重要⼀点,旅⾏途中我们虽然不断变化了交通⼯具,但是旅⾏⾏程的起始地址和⽬的地址始终都没变。其实,在⽹络中数据包传输中也是如此,源IP地址和⽬标IP地址在传输过程中是不会变化的,只有源 MAC 地址和⽬标 MAC ⼀直在变化。

4.1.2 IP 地址的基础知识

  • 在 TCP/IP ⽹络通信时,为了保证能正常通信,每个设备都需要配置正确的 IP 地址,否则⽆法实现正常的通信。
    IP 地址(IPv4 地址)由 32 位正整数来表示,IP 地址在计算机是以⼆进制的⽅式处理的。⽽⼈类为了⽅便记忆采⽤了点分⼗进制的标记⽅式,也就是将 32 位 IP 地址以每 8 位为组,共分为 4 组,每组以「 . 」隔开,再将每组转换成⼗进制。

    那么,IP 地址最⼤值是 2^32,也即最⼤允许 43 亿台计算机连接到⽹络。 实际上,IP 地址是以⽹卡数来配置的。像服务器、路由器等设备都是有 2 个以上的⽹卡,是会有 2 个以上的 IP 地址;更何况 IP 地址是由「⽹络标识」和「主机标识」这两个部分组成的,所以实际能够连接到⽹络的计算机个数更是少了很多。 根据⼀种可以更换 IP 地址的技术 NAT ,使得可连接计算机数超过 43 亿台。
  • IP 地址的分类
    互联⽹诞⽣之初,IP 地址显得很充裕,于是科学家们设计了 5 种分类地址,分别是 A 类、B 类、C 类、D 类、E 类。
    下图中⻩⾊部分为分类号,⽤以区分 IP 地址类别

  • 什么是 A、B、C 类地址?
    ??p318

4.1.3 IP 协议相关技术(p340

  • DNS 域名解析
    域名⽅便⼈类记忆,使用 DNS 域名解析将域名⽹址⾃动转换为具体的 IP 地址。

    • 域名的层级关系
      DNS 中的域名都是⽤句点来分隔的,⽐如 www.server.com ,这⾥的句点代表了不同层次之间的界限。
      在域名中,越靠右的位置表示其层级越⾼,层级关系类似⼀个树状结构:根 DNS 服务器、顶级域 DNS 服务器(com)、权威 DNS 服务器(server.com) 根域的 DNS 服务器信息保存在互联⽹中所有的 DNS 服务器中。这样⼀来,任何 DNS 服务器就都可以找到并访问根域 DNS 服务器了。因此,客户端只要能够找到任意⼀台 DNS 服务器,就可以通过它找到根域 DNS 服务器,然后再⼀路顺藤摸⽠找到位于下层的某台⽬标 DNS 服务器
    • 域名解析的⼯作流程
      1. 客户端⾸先会发出⼀个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端 的 TCP/IP 设置中填写的 DNS 服务器地址)。
      2. 本地域名服务器收到客户端的请求后,如果缓存⾥的表格能找到 www.server.com, 则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“⽼⼤, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最⾼层次的,它不直接⽤于域名解析,但能指明⼀条道路。
      3. 根 DNS 收到来⾃本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管 理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
      4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“⽼⼆, 你能告诉我 www.server.com 的 IP 地址吗?”
      5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
      6. 本地 DNS 于是转向问权威 DNS 服务器:“⽼三,www.server.com 对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
      7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
      8. 本地 DNS 再将 IP 地址返回客户端,客户端和⽬标建⽴连接。
  • ARP 与 RARP 协议
    ??p342

  • DHCP 动态获取 IP 地址

。。

  • NAT ⽹络地址转换

。。

  • ICMP 互联⽹控制报⽂协议

。。

  • IGMP 因特⽹组管理协

。。

4.2 ping 的工作原理

-


五、网络综合篇

5.1 键入网址到网页显示,期间发生了什么?

5.1.1 浏览器做的第⼀步⼯作是解析 URL

  • ⾸先浏览器做的第⼀步⼯作就是要对 URL 进⾏解析,从⽽⽣成发送给 Web 服务器的请求信息。 所以图中的⻓⻓的 URL 实际上是请求服务器⾥的⽂件资源。当没有文件路径名时,就代表访问根⽬录下事先设置的默认⽂件,也就是 /index.html 或者 /default.html 这些⽂件。
  • 对 URL 进⾏解析之后,浏览器确定了 Web 服务器和⽂件名,接下来就是根据这些信息来⽣成 HTTP 请求消息了。

5.1.2 真实地址查询 —— DNS

通过浏览器解析 URL 并⽣成 HTTP 消息后,需要委托操作系统将消息发送给 Web 服务器。
但在发送之前,还有⼀项⼯作需要完成,那就是查询服务器域名对应的 IP 地址,因为委托操作系统发送消息时,必须提供通信对象的 IP 地址。所以,有⼀种服务器就专⻔保存了 Web 服务器域名与 IP 的对应关系,它就是 DNS 服务器。
使用 DNS 域名解析将域名⽹址⾃动转换为具体的 IP 地址。

5.1.3 指南好帮⼿ —— 协议栈(p379

???

5.1.4 可靠传输 —— TCP

-

5.1.5 远程定位 —— IP

TCP 模块在执⾏连接、收发、断开等各阶段操作时,都需要委托 IP 模块将数据封装成⽹络包发送给通信对象

5.1.6 两点传输 —— MAC

⽣成了 IP 头部之后,接下来⽹络包还需要在 IP 头部的前⾯加上 MAC 头部

5.1.7. 出⼝ —— ⽹卡

⽹络包只是存放在内存中的⼀串⼆进制数字信息,没有办法直接发送给对⽅。因此,我们需要将数字信息转换为电
信号,才能在⽹线上传输,也就是说,这才是真正的数据发送过程

5.1.8 送别者 —— 交换机

下⾯来看⼀下包是如何通过交换机的。交换机的设计是将⽹络包原样转发到⽬的地。交换机⼯作在 MAC 层,也称
为⼆层⽹络设备

5.1.9 出境⼤⻔ —— 路由器

。。

5.1.10 互相扒⽪ —— 服务器 与 客户端

5.2 Linux如何收发网络包?

电脑与电脑之间通常都是通话⽹卡、交换机、路由器等⽹络设备连接到⼀起,那由于⽹络设备的异构性,国际标准化组织定义了⼀个七层的 OSI ⽹络模型,但是这个模型由于⽐较复杂,实际应⽤中并没有采⽤,⽽是采⽤了更为简化的 TCP/IP 模型,Linux ⽹络协议栈就是按照了该模型来实现的。
TCP/IP 模型主要分为应⽤层、传输层、⽹络层、⽹络接⼝层四层,每⼀层负责的职责都不同,这也是 Linux ⽹络协议栈主要构成部分。
当应⽤程序通过 Socket 接⼝发送数据包,数据包会被⽹络协议栈从上到下进⾏逐层处理后,才会被送到⽹卡队列中,随后由⽹卡将⽹络包发送出去。⽽在接收⽹络包时,同样也要先经过⽹络协议栈从下到上的逐层处理,最后才会被送到应⽤程序。


六、学习心得

操作系统和计算机⽹络有多重要呢?如果没有操作系统,我们的⼿机和电脑可以说是废铁了,如果没有计算机⽹络,我们的⼿机和电脑就是⼀座「孤岛」了,孤岛的世界很单调,也没有什么⾊彩,也正是因为计算机⽹络,才创造出这么丰富多彩的互联⽹世界。

身为程序员的我们,那更应该深刻理解和掌握它们,虽然我们⽇常 CURD 的⼯作中,即使不熟悉它们,也不妨碍我们写代码,但是当出现问题时,没有这些基础知识,你是⽆厘头的,根本没有思路下⼿,这时候和别⼈差距就显现出来了,可以说是程序员之间的分⽔岭。事实上,我们⼯作中会有大量的时间都是在排查和解决问题,编码的时间其实⽐较少,如果计算机基础学的很扎实,虽然不敢保证我们能 100% 解决,但是⾄少遇到问题时,我们有⼀个排查的⽅向,或者直接就定位到问题所在,然后再⼀步⼀步尝试解决,解决了问题,⾃然就体现了我们⾃身的实⼒和价值,职场也会越⾛越远。

当然,计算机⽹络最⽜逼的资料,那必定 RFC ⽂档,它可以称为计算机⽹络世界的「法规」,也是最新、最权威和最正确的地⽅了,困惑⼤家的 TCP 为什么三次握⼿和四次挥⼿,其实在 RFC ⽂档⼏句话就说明⽩了。
TCP 协议的 RFC ⽂档:https://datatracker.ietf.org/doc/rfc1644

实战系列:在学习书籍资料的时候,不管是 TCP、UDP、ICMP、DNS、HTTP、HTTPS 等协议,最好都可以亲⼿尝试抓数据
报,接着可以⽤ Wireshark ⼯具看每⼀个数据报⽂的信息,这样你会觉得计算机⽹络没有想象中那么抽象了,因为它们被你「抓」出来了,并毫⽆保留地显现在你⾯前了,于是你就可以肆⽆忌惮地「扒开」它们,看清它们每⼀个头信息。

SpringBoot

Spring Boot 是一个用于简化 Spring 应用程序开发的框架,它提供了大量的注解来简化配置和开发。

SpringFramework没有解决了什么问题?

为什么有了SpringFramework还会诞生SpringBoot?简单而言,因为虽然Spring的组件代码是轻量级的,但它的配置却是重量级的。
一开始,Spring用XML配置,而且是很多XML配置。Spring 2.5引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式XML配置。Spring 3.0引入了基于Java的配置,这是一种类型安全的可重构配置方式,可以代替XML。
所有这些配置都代表了开发时的损耗。因为在思考Spring特性配置和解决业务问题之间需要进行思维切换,所以编写配置挤占了编写应用程序逻辑的时间。和所有框架一样,Spring实用,但与此同时它要求的回报也不少。
除此之外,项目的依赖管理也是一件耗时耗力的事情。在环境搭建时,需要分析要导入哪些库的坐标,而且还需要分析导入与之有依赖关系的其他库的坐标,一旦选错了依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。
1.jsp中要写很多代码、控制器过于灵活,缺少一个公用控制器 2.Spring不支持分布式,这也是EJB仍然在用的原因之一。

SringBoot的概述

  • SpringBoot解决上述Spring的缺点
    SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。
  • SpringBoot的特点
    1、为基于Spring的开发提供更快的入门体验
    2、开箱即用,没有代码生成,也无需XML配置。同时也可以修改默认值来满足特定的需求
    3、提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等
    SpringBoot不是对Spring功能上的增强,而是提供了一种快速使用Spring的方式
  • SpringBoot的核心功能
    • 起步依赖:起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。
    • 自动配置:Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的
  • 为什么添加一个starter-web模块,一个简单的web程序便完成了呢?
    SpringBoot最强大的地方在于约定大于配置,只要你引入某个模块的xx-start包,它将自动注入配置,提供了这个模块的功能;
    比如这里我们在POM中添加了如下的包
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    它内嵌了Tomcat并且提供了默认的配置,比如默认端口是8080;可以在application.properties或者application.yml中修改配置。

SpringBoot 注解

  1. @SpringBootApplication:该注解用于标记主应用程序类,通常位于项目的顶层包中。定义在main方法入口类处,用于启动sping boot应用项目。
    它包含了 @Configuration@EnableAutoConfiguration@ComponentScan 注解,用于自动配置和组件扫描。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Configuration
    @EnableAutoConfiguration
    @ComponentScan
    public @interface SpringBootApplication {

    /**
    * Exclude specific auto-configuration classes such that they will never be applied.
    * @return the classes to exclude
    */
    Class<?>[] exclude() default {};

    }
    @EnableAutoConfiguration:让spring boot根据类路径中的jar包依赖当前项目进行自动配置,在src/main/resources的META-INF/spring.factories
  2. @ImportResource:加载xml配置,一般是放在启动main类上
  3. @ConfigurationProperties(prefix="person"):可以新建一个properties文件,ConfigurationProperties的属性prefix指定properties的配置的前缀,通过location指定properties文件的位置
  4. @PostConstruct:spring容器初始化时,要执行该方法
  5. @ComponentScan:注解会告知Spring扫描指定的包来初始化Spring
  6. @RestController:用于标记一个类,表示它是一个控制器类,处理 HTTP 请求并返回 JSON 或 XML 等数据。
    @GetMapping@PostMapping@PutMapping@DeleteMapping:这些注解分别用于将 HTTP GET、POST、PUT 和 DELETE 请求映射到相应的处理方法上。
    @RequestMapping:用于将 HTTP 请求映射到控制器方法上,指定请求的 URL 路径和请求方法(GET、POST、PUT、DELETE 等)
    1
    @RequestMapping(path = "/user/detail", method = RequestMethod.POST)
    @RequestParam:用于从请求参数中获取值,并将其传递给控制器方法的参数。
    1
    2
    3
    4
    5
    public List<CopperVO> getOpList(HttpServletRequest request,
    @RequestParam(value = "pageIndex", required = false) Integer pageIndex,
    @RequestParam(value = "pageSize", required = false) Integer pageSize) {

    }
  7. @PathVariable:用于从 URL 路径中获取值,并将其传递给控制器方法的参数。
  8. @RequestBody:用于将请求体中的数据绑定到控制器方法的参数上,通常用于接收 JSON 或 XML 数据。
    @ResponseBody: 表示方法返回的对象应该直接写入HTTP响应体,而不是通过视图解析器进行渲染。
  9. @Autowired 是 Spring 框架自带的注解用于进行依赖注入,将 Spring 托管的 bean 注入到需要它们的类中。
    @Resource是 Java EE 提供的注解,用于实现依赖注入。它可以用于字段、setter 方法、构造函数等地方,用于告诉容器注入指定名称或类型的 bean。
    @Bean: 用于定义Spring Bean,通常在 @Configuration 类中使用。
    @Component 是 Spring 框架中用于声明一个类为 Spring 管理的组件(Bean)的注解。
  10. @Service@Repository@Controller:这些注解分别用于标记服务类、仓库类和控制器类,以便 Spring Boot 可以自动扫描并创建这些组件。都属于 @Component 的衍生注解,用于更明确地表示类的职责。
  11. @Configuration:用于定义配置类,通常与 @Bean 注解一起使用,用于配置第三方库或复杂的 bean。
  12. @Override 是Java中的一个注解(Annotation),用于标识方法是否是一个覆盖(重写)了父类或接口中的方法。它提供了编译时的检查,确保您正确地重写了父类或接口中的方法,以防止由于拼写错误或方法签名不匹配而导致的错误。
  13. @Value:用于注入属性值,从配置文件中获取属性值。
  14. @Profile:用于定义不同环境下的配置文件,可以根据不同的配置文件加载不同的配置。
  15. @Conditional:用于根据条件决定是否创建某个 bean。
  16. @Param:给参数变量起别名。如果只有一个参数,并且在mapper对应的xml文件中里使用,则必须加别名。
  17. @Async 是 Spring 框架中用于实现异步方法调用的注解。通过在方法上添加 @Async 注解,Spring Boot 就会在调用该方法时使用另一个线程来执行,而不是阻塞当前线程等待方法执行完成。
  18. @Order:如 @Order(1),值越小优先级超高,越先运行

Spring 🍃

Spring 是一个开源的轻量级框架,用于构建企业级 Java 应用程序。它提供了广泛的基础设施支持和许多可重用的库,以简化企业级应用程序的开发。Spring 框架的设计目标是促进松耦合、可维护性和可测试性的编码实践。
Spring的一个最大的目的就是使 JAVA EE 开发更加容易。同时,Spring之所以与Struts、Hibernate等单层框架不同,是因为Spring致力于提供一个以统一的、高效的方式构造整个应用,并且可以将单层框架以最佳的组合揉和在一起建立一个连贯的体系。可以说Spring是一个提供了更完善开发环境的一个框架,可以为POJO对象提供企业级的服务。

官方的项目和教程地址,在学习Spring时,一定要把它当做生态体系,而是不是一个简单的开发框架。
Spring简单例子引入Spring要点:https://pdai.tech/md/spring/spring-x-framework-helloworld.html

核心功能和特点

  • 非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API
  • 控制反转(IoC):Inversion of Control,指的是将对象的创建权交给 Spring 去创建。Spring 的 IoC 容器管理对象的生命周期和配置,开发者不再需要手动 new创建对象。这种反转控制的方式使得应用程序更加松散耦合、易于测试和维护。Spring 管理一切,管理项目中的对象和整合其他对象。
  • 依赖注入:DI——Dependency Injection,是指依赖的对象不需要手动调用 setXX 方法去设置,而是通过配置赋值。
  • 面向切面编程(AOP):Spring 提供了强大的 AOP 支持,通过 AOP 可以更好地处理横切关注点,如事务管理、安全性、日志记录等。AOP 可以提高代码的模块性和可维护性。
  • 容器:Spring 是一个容器,因为它包含并且管理应用对象的生命周期
  • 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。
  • 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上 Spring 自身也提供了表现层的 SpringMVC 和持久层的 Spring JDBC)
    • 模型视图控制器(MVC):Spring MVC 是一个强大的 Web 框架,通过注解配置和可插拔的视图解析器简化了 Web 应用的开发。它提供了清晰的分层结构,使得开发更加模块化。
    • Spring 简化了数据访问的过程,提供了一致的编程模型,支持 JDBC 和 ORM 框架。这使得数据访问更加灵活、简单,且易于集成各种数据源。
    • Spring 真正的利用了一些现有的技术,像 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术。
    • Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些API应用难度大大降低。
  • 事务管理:Spring 提供了声明式事务管理,通过注解或 XML 配置来管理事务。这样可以将事务管理从业务代码中解耦,使代码更加干净和易于理解。Spring 提供了一致的事务管理接口,可向下扩展到(使用一个单一的数据库,例如)本地事务并扩展到全局事务(例如,使用 JTA)
  • 灵活性和可扩展性:Spring 的模块化结构使得可以仅使用需要的功能,从而保持应用程序的轻量级。Spring 的组件是可插拔的,可以轻松地集成第三方库。
    • ??Spring 可以使开发人员使用 POJOs(Plain old Java object)开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品。
    • Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只要担心你需要的,而其它的就可以忽略了。
    • 轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。
  • 测试支持:Spring 框架鼓励并支持测试驱动开发(TDD)。提供许多工具和类库来进行单元测试和集成测试,保障应用程序的质量。
    ??测试一个用 Spring 编写的应用程序很容易,因为环境相关的代码被移动到这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易。

Spring Framework

下图来自官方文档 Spring-framework 5.0;需要注意的是,虽然这个图来源于Spring Framwork5.0 M4 版本,但是它依然是V4版本的图,比如Spring 5版本中的web模块已经去掉了Portlet模块,新增了WebFlux模块等。
包含了 Spring 框架的所有模块,可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。

Core Container(Spring的核心容器)

Spring 的核心容器是其他模块建立的基础,没有这些核心容器,也不可能有 AOP、Web 等上层的功能。

  • Beans 模块:提供了框架的基础部分,包括控制反转和依赖注入。
  • Core 核心模块:封装了 Spring 框架的底层部分,包括资源访问、类型转换及一些常用工具类。
  • Context 上下文模块:建立在 Core 和 Beans 模块的基础之上,集成 Beans 模块功能并添加资源绑定、数据验证、国际化、Java EE 支持、容器生命周期、事件传播等。ApplicationContext 接口是上下文模块的焦点。
  • SpEL 模块:提供了强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从 Spring 容器获取 Bean,它也支持列表投影、选择和一般的列表聚合等。

Data Access/Integration(数据访问/集成)

  • JDBC 模块:提供了一个 JDBC 的样例模板,使用这些模板能消除传统冗长的 JDBC 编码还有必须的事务控制,而且能享受到 Spring 管理事务的好处。
  • ORM 模块:提供与流行的“对象-关系”映射框架无缝集成的 API,包括 JPA、JDO、Hibernate 和 MyBatis 等。而且还可以使用 Spring 事务管理,无需额外控制事务。
  • OXM 模块:提供了一个支持 Object /XML 映射的抽象层实现,如 JAXB、Castor、XMLBeans、JiBX 和 XStream。将 Java 对象映射成 XML 数据,或者将XML 数据映射成 Java 对象。
  • JMS 模块:指 Java 消息服务,提供一套 “消息生产者、消息消费者”模板用于更加简单的使用 JMS,JMS 用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
  • Transactions 事务模块:支持编程和声明式事务管理。

Web模块

  • Web 模块:提供了基本的Web开发集成特性,例如多文件上传功能、使用的 Servlet监听器的IOC容器初始化以及Web应用上下文。
  • Servlet 模块:提供了一个 Spring MVC Web 框架实现。Spring MVC 框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的 JSP 标签,完全无缝与 Spring 其他技术协作。
  • WebSocket 模块:提供了简单的接口,用户只要实现响应的接口就可以快速的搭建 WebSocket Server,从而实现双向通讯。
  • Webflux 模块: Spring WebFlux 是 Spring Framework 5.x中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。Spring WebFlux 用于创建基于事件循环执行模型的完全异步且非阻塞的应用程序。

AOP、Aspects、Instrumentation 和 Messaging

  • AOP 模块:提供了面向切面编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态的把这些功能添加到需要的代码中,这样各司其职,降低业务逻辑和通用功能的耦合。
    1. AOP:即面向方面(切面)编程。是一种编程思想,是对OOP的补充,可以进一步提高编程的效率。
      例如,把多个业务组件的共同功能封装成一个“系统组件”,如对所有service类进行权限检查、记录日志、事务管理…
    2. 织入:将方面Aspect(处理系统组件的额外的bean)[切点Pointcut(声明织入到哪些对象的哪些位置)+ 通知Advice(处理的逻辑(前,后,返回,异常)] —> 织入Weaving(编译时/装载时/运行时) —> 目标对象Target(程序中开发好的bean)的连接点Joinpoint(可以织入代码的地方)
    3. AOP的实现
      • AspectJ:语言级的实现(一门新语言)。编译期织入代码。
      • Spring AOP:纯Java实现。运行时通过代理的方式织入代码,只支持方法类型的连接点。
        • JDK动态代理:在运行时创建接口的代理实例。Spring AOP 默认方式,在接口的代理实例中织入代码。
        • CGLib动态代理:不要求目标类实现接口,通过继承目标类来创建代理对象。即可以代理没有实现接口的类。
        • 如果你的目标类已经实现了接口,并且你希望代理对象是目标类的接口的实现,那么可以使用JDK动态代理。如果你的目标类没有实现接口,或者你需要代理非公共方法(final、private等),那么可以使用CGLib动态代理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class ServiceLogAspect {
    // 切点:service包下的所有方法
    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
    public void pointcut() {}
    // 前置通知
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
    // 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    String ip = request.getRemoteHost();
    String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
    logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
    } }
  • Aspects 模块:提供与 AspectJ 的集成,是一个功能强大且成熟的面向切面编程(AOP)框架。
    Instrumentation 模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用。
  • messaging 模块:Spring 4.0 以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。
  • jcl 模块: Spring 5.x中新增了日志框架集成的模块。

Test模块

Spring 支持 Junit 和 TestNG 测试框架,而且还额外提供了一些基于 Spring 的测试功能,比如在测试 Web 框架时,模拟 Http 请求的功能。包含Mock Objects, TestContext Framework, Spring MVC Test, WebTestClient。

结合Spring历史版本和SpringBoot看发展


控制反转(IOC)

  • IoC Container管理的是Spring Bean, 那么Spring Bean是什么呢?
    Spring里面的bean就类似是定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能。

  • Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。
    在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

    • 谁控制谁,控制什么?传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;
      • 谁控制谁?当然是IoC 容器控制了对象;
      • 控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。????????
    • 为何是反转,哪些方面反转了?有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;
      • 为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;
      • 哪些方面反转了?依赖对象的获取被反转了。
  • IoC能做什么?
    IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。
    传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
    其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

  • IoC和DI是什么关系?
    控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式。
    DI—Dependency Injection:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。?????
    谁依赖于谁?当然是应用程序依赖于IoC容器;????!!!!!!!
    为什么需要依赖?应用程序需要IoC容器来提供对象需要的外部资源;
    谁注入谁?很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
    注入了什么?就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

  • Ioc 配置的三种方式

    • xml 配置:就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。
    • Java 配置:将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中
    • 注解配置:通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。设置ComponentScan的basePackage, 比如 context:component-scan base-package=’tech.pdai.springframework’>, 或者@ComponentScan(“tech.pdai.springframework”)注解,或者 new AnnotationConfigApplicationContext(“tech.pdai.springframework”)指定扫描的basePackage.
  • 依赖注入的三种方式

    • 构造方法注入(Construct注入)
    • setter注入
    • 基于注解的注入(接口注入)
      、、??

切面编程(AOP)

https://pdai.tech/md/spring/spring-x-framework-aop.html

、、


SpringMVC请求流程和案例

  • 什么是MVC
    MVC英文是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计规范。本质上也是一种解耦。用一种业务逻辑、数据、界面显示分离的方法,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
    Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
    View(视图)是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的。
    Controller(控制器)是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

  • 什么是Spring MVC?
    简单而言,Spring MVC是Spring在Spring Container Core和AOP等技术基础上,遵循上述Web MVC的规范推出的web开发框架,目的是为了简化Java栈的web开发。

  • Spring MVC的请求流程
    Spring Web MVC 框架也是一个基于请求驱动的Web 框架,并且也使用了前端控制器模式来进行设计,再根据请求映射 规则分发给相应的页面控制器(动作/处理器)进行处理。
    核心架构的具体流程步骤如下:
    1、首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行 处理,作为统一访问点,进行全局的流程控制;
    2、DispatcherServlet——>HandlerMapping, HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象(包含一 个Handler 处理器(页面控制器)对象、多个HandlerInterceptor 拦截器)对象,通过这种策略模式,很容易添加新 的映射策略;
    3、DispatcherServlet——>HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器, 即适配器设计模式的应用,从而很容易支持很多类型的处理器;
    4、HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处 理方法,完成功能处理;并返回一个ModelAndView 对象(包含模型数据、逻辑视图名);
    5、ModelAndView 的逻辑视图名——> ViewResolver,ViewResolver 将把逻辑视图名解析为具体的View,通过这种策 略模式,很容易更换其他视图技术;
    6、View——>渲染,View 会根据传进来的Model 模型数据进行渲染,此处的Model 实际是一个Map 数据结构,因此 很容易支持其他视图技术;
    7、返回控制权给DispatcherServlet,由DispatcherServlet 返回响应给用户,到此一个流程结束。
    补充:
    1、Filter(ServletFilter):进入Servlet前可以有preFilter, Servlet处理之后还可有postFilter
    2、LocaleResolver:在视图解析/渲染时,还需要考虑国际化(Local),显然这里需要有LocaleResolver.
    3、ThemeResolver:如何控制视图样式呢?SpringMVC中还设计了ThemeSource接口和ThemeResolver,包含一些静态资源的集合(样式及图片等),用来控制应用的视觉风格。


拦截器

在Spring Boot中,你可以使用拦截器(Interceptor)来处理请求前、请求后或请求处理过程中的逻辑。拦截器通常用于执行一些跟请求处理相关的任务,例如身份验证、日志记录、权限检查等。以下是在Spring Boot中使用拦截器的基本步骤:

  1. 创建拦截器类
    首先,你需要创建一个Java类,实现HandlerInterceptor接口,该接口定义了拦截器的方法,包括preHandlepostHandleafterCompletion等。这些方法分别用于在请求处理前、请求处理后和请求完成后执行相应的逻辑。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Component
    public class LoginTicketInterceptor implements HandlerInterceptor {
    // 登录信息拦截器(检查登录凭证是否有效)
    // 每次请求,不管是什么请求,都要检查登录信息;这在interceptor中完成,而不是在每个controller重写一遍
    @Autowired
    private UserService userService;
    @Autowired
    private HostHolder hostHolder;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String ticket = CookieUtil.getValue(request, "ticket"); // 从cookie中获取凭证
    if (ticket != null) {
    LoginTicket loginTicket = userService.findLoginTicket(ticket); // 查询凭证
    if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
    User user = userService.findUserById(loginTicket.getUserId()); // 若凭证有效,查询用户
    hostHolder.setUser(user); // 在本次请求中持有用户
    }
    }
    return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    // 将登录用户的信息添加到ModelAndView对象中,这样在视图中可以方便地使用这些信息
    User user = hostHolder.getUser();
    if (user != null && modelAndView != null) modelAndView.addObject("loginUser", user);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // 清除hostHolder中持有的用户信息。确保每个请求处理完成后都不会泄漏用户信息或状态,以便下一个请求可以从头开始。
    hostHolder.clear();
    }
    }
  2. 配置拦截器
    在Spring Boot应用程序中,通常在配置类中配置拦截器。你可以继承WebMvcConfigurerAdapter类(或实现WebMvcConfigurer接口)并覆盖addInterceptors方法,将你的拦截器添加到拦截器链中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    // 设置不拦截静态资源
    registry.addInterceptor(loginTicketInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    } }

Spring 事务管理

  1. 事务:是由N步数据库操作序列组成的逻辑扩行单元,这系列操作要么全执行,要么全放弃执行事务的特性(ACID)。报错会回滚。
  2. 常见的并发异常:第一类丢失更新、第二类丢失更新脏读、不可重复读、幻读。
  3. 事务隔离级别定义了多个并发事务之间的可见性和影响的程度。在关系型数据库中,常见的隔离级别包括:
    DEFAULT:使用数据库的默认隔离级别。通常是READ_COMMITTED。
    READ_UNCOMMITTED:允许一个事务读取另一个事务尚未提交的数据。最低的隔离级别,不推荐在生产环境中使用,因为可能会导致脏读、不可重复读和幻读问题。
    READ_COMMITTED:保证一个事务不会读取到其他并发事务未提交的数据。这是大多数数据库的默认隔离级别。
    SERIALIZABLE:最高的隔离级别,确保事务串行执行,完全隔离了其他事务的影响。通常性能较差,不常用。
  4. 悲观锁 (数据库)
    共享锁 (S锁):事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
    排他锁 (X锁):事务A对某数据加了排他锁后,其他事对该数据既不能加共享锁,也不能加排他锁。
  5. 乐观锁(自定义):版本号、时间戳等;若变化则取消本次更新,否则就更新数据(版本号+1)在更新数据前,检查版本号是否发生变
  6. 声明式事务:通过XML / 通过注解,声明某方法的事务特征。
    通过 @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) 注解配置事务的属性,以控制事务的隔离级别(isolation)、传播行为(propagation)等。
    常见的传播行为包括:REQUIRED, REQUIRES_NEW, SUPPORTS, NESTED
  7. 编程式事务:通过 TransactionTemplate 管理事务并通过它执行数据库的操作。(业务复杂/只处理其中几条代码时用)

统一异常处理

  • @ControllerAdvice:用于修饰类,表示该类是Controller的全局配置类(放在/controller/advice/下);
    在此类中,可以对Controller进行如下三种全局配置:异常处理方案、绑定数据方案、绑定参数方案
  • @ExceptionHandler:用于修饰(@ControllerAdvice所修饰类中的)方法,在Controller出现异常后被调用,用于处理捕获到的异常
  • @ModelAttribute:用于修饰方法,使其在Controller方法执行前被调用,用于为Model对象绑定参数(如获取从页面post的数据)
  • @DataBinder:用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器
    1
    2
    3
    4
    5
    6
    7
    8
    @ControllerAdvice(annotations = Controller.class)          // 表示扫描所有controller类
    public class ExceptionAdvice {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
    @ExceptionHandler({Exception.class}) // 指处理所有类型的异常
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
    logger.error("服务器发生异常: " + e.getMessage()); // 打印日志
    for (StackTraceElement element : e.getStackTrace()) {
    logger.error(element.toString()); } } // 打印具体报错

Spring Security

  • Spring Security是一个专注于为Java应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义的需求
    • 对身份的 认证 授权 提供全面的、可扩展的支持
    • 防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等
    • 支持与Servlet API、Spring MVC等Web技术集成
  • 核心概念:
    • 认证(Authentication):验证用户的身份。
    • 授权(Authorization):确定用户是否有权执行某个操作。
    • 过滤器链(Filter Chain):一系列的过滤器,用于处理认证和授权的请求。Spring Security的处理在 Spring MVC之前( Security底层是基于filter,可以拦截大量请求)
  • 基本配置:Spring Security 配置通常通过 Java 配置或 XML 配置完成。
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      @EnableWebSecurity   // Java 配置示例
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http
      .authorizeRequests()
      .antMatchers("/", "/home").permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .loginPage("/login")
      .permitAll()
      .and()
      .logout()
      .permitAll();
      }
      @Autowired
      public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      auth
      .inMemoryAuthentication()
      .withUser("user").password("{noop}password").roles("USER");
      }
      }
  • 安全过滤器链:Spring Security 的核心是安全过滤器链,它负责处理请求的认证和授权过程。在配置中,你可以通过 HttpSecurity 来定制过滤器链。
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      http
      .authorizeRequests() // 对请求进行授权
      .antMatchers("/", "/home").permitAll() // 允许所有用户访问
      .anyRequest().authenticated() // 其他请求需要身份验证
      .and()
      .formLogin() // 定义登录操作
      .loginPage("/login") // 指定登录页
      .permitAll()
      .and()
      .logout() // 定义登出操作
      .permitAll();
  • SecurityContext 是 Spring Security 中用于存储当前执行身份验证操作的上下文信息的接口。
    它通常包含了与认证(Authentication)相关的信息,例如当前已认证的用户、用户的权限等。 SecurityContextHolder 负责管理 SecurityContext,而 SecurityContext 的实现则是 SecurityContextImpl
    • 1
      2
      3
      4
      5
      6
      7
      8
      // 在认证成功后,将认证对象放入 SecurityContext
      UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      context.setAuthentication(authenticationToken);
      SecurityContextHolder.setContext(context);
      // 在需要获取当前用户信息的地方
      SecurityContext context = SecurityContextHolder.getContext();
      Authentication authentication = context.getAuthentication();

Java 并发

理论基础

多线程的出现是要解决什么问题的?

众所周知,CPU、内存、V0 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
CPU 增加了缓存,以均衡与内存的速度差异; // 导致 可见性 问题
操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/0 设备的速度差异; // 导致 原子性 问题
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。 // 导致 有序性 问题

线程不安全是指什么?

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

举例说明并发出现线程不安全的本质什么? 保障 可见性,原子性和有序性。

可见性【CPU缓存引起】:一个线程对共享变量的修改,另外一个线程能够立刻看到。
原子性【分时复用引起】:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
有序性【重排序引起】:即程序执行的顺序按照代码的先后顺序执行。

Java是怎么解决并发问题的? 3个关键字,JMM 和 8个 Happens-Before

  • Java 内存模型是个很复杂的规范,推荐:https://pdai.tech/md/java/jvm/java-jvm-jmm.html
  • 理解的第一个维度:核心知识点
    JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
    具体来说,这些方法包括(1)volatile、synchronized 和 final 三个关键字;(2)Happens-Before
  • 规则理解的第二个维度:可见性,有序性,原子性
    • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行要么不执行。(Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronizedLock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。)
    • 可见性:Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。(另外,通过synchronizedLock也能够保证可见性,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。)
    • 有序性:通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
  • 关键字: volatile、synchronized 和 final
  • Happens-Before 规则:JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
    • 1.单一线程原则Single Thread rule:在一个线程内,在程序前面的操作先行发生于后面的操作。
    • 2.管程锁定规则Monitor Lock Rule:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
    • 3.??

线程安全是不是非真即假?…不是

一个类在可以被多个线程安全调用时就是线程安全的。
线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变、
  2. 绝对线程安全、
  3. 相对线程安全、
  4. 线程兼容
  5. 线程对立。
    ,,

线程安全有哪些实现思路?

  1. 互斥同步
    synchronized 和 ReentrantLock。
  2. 非阻塞同步
  3. 无同步方案
    ,,

如何理解并发和并行的区别?

并发:单核CPU宏观上可以处理多线程,是通过CPU调度实现的交替执行
并行:在多核CPU系统中利用每个处理器处理一个可以并发执行的程序,这样多个程序便可以同时执行
上下文切换:线程上下文是指某一时间点 CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程)。多线程往往会比单线程更快更能够提高并发,但同时更多的线程意味着线程创建销毁开销加大、上下文非常频繁,程序反而不能支持更高的TPS。
时间片:时间片是CPU分配给各个任务(线程)的时间。多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,利用了时间片轮转的方式进行调度。
建议:合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。高并发,低耗时的情况,建议少线程;低并发,高耗时的情况,建议多线程;高并发,高耗时的情况,要分析任务类型、增加排队、加大线程数


线程基础

进程、线程、纤程

  1. Java 进程:在操作系统中,进程是基本的资源分配单位,操作系统通过进程来管理计算机的资源(指令加载到CPU、数据到内存、磁盘、网络,),每个进程有一个唯一的进程标识符(PID)用来区分。
    当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
    Java中,一个进程通常是指一个独立运行的 Java 虚拟机(JVM)实例;每个 Java 进程都有自己的内存空间、程序计数器、寄存器等资源,启动 Java 程序就启动了一个 Java 进程。
  2. Java 线程:线程是操作系统中的基本执行单元(能够直接执行的最小代码块),是CPU调度和分派的基本单位。一个进程可包含多个线程,每个线程独立执行不同的任务,共享进程的资源。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。同一时刻一个CPU核心只能运行一线程,8核CPU同时可以执行8个线程代码。
    • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
    • 异步调用
      以调用方角度来讲,如果需要等待结果返回,才能继续运行就是同步;不需等待结果返回就能继续运行,是异步
      1. 设计:多线程可以让方法执行变为异步的(即不要巴巴干等着),比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
      2. 结论:比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
    • 效率提升
      1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活
      2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。
      3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
    • 进程与线程对比:
      进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
      进程拥有共享的资源,如内存空间等,供其内部的线程共享
      进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication);不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
      线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
      线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
  3. 纤程:Java19才支持虚拟线程(纤程【协程】)
    • 纤程可以在一个线程内部创建多个纤程这些纤程之间可以共享同一个线程的资源
    • 纤程是在同一个进程内部运行的,不需操作系统的介入,可以在用户空间内实现协作式多任务处理(jvm级别的)。因此纤程的创建和销毁开销很小,可更高效地利用系统资源。
    • 先有进程,然后进程可以创建线程,线程是依附在进程里面的。
      线程里面可以包含多个协程;进程之间不共亭全局变量。线程之间共享全局变量,但是要注意资源竞争的问题
  4. 常用方法
    • start() 与 run()
      • 类型:run方法是同步方法(按代码顺序执行),而start方法是异步方法(创建新线程,进入就绪状态,等待OS调用)
      • 作用:run方法的作用是存放(执行)任务代码,不产生新线程;而start方法创建,启动一个新线程,并调用其 run方法
      • 执行次数:run方法可以被执行无数次,而start方法只能被执行一次,原因在于线程不能被重复启动
    • setName():给当前线程取名字
    • getName():获取当前线程的名字。线程存在默认名称:子线程是 Thread-索引,主线程是 main
    • sleep():让当前线程休眠多少毫秒再继续执行,Thread.sleep(0):让操作系统立刻重新进行一次cpu竞争。
      • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞);睡眠结束后的线程未必会立刻得到执行
      • 可以使用 interrupt() 打断正在睡眠的线程,这时 sleep() 会抛出 InterruptedException.
      • 建议用 TimeUnit的 sleep() 代替 Thread 的 sleep 来获得更好的可读性,其底层还是 sleep 方法.
      • 在循环访问锁的过程中,可以加入sleep让线程阻塞时间,防止大量占用cpu资源其它线程。
    • yield():提示线程调度器尽力让出当前线程对CPU的使用,让当前线程从 Running?? 进入 Runnable 就绪状态
    • setPriority():更改该线程的优先级,1~10,(CPU忙时)优先级越大被CPU调度的几率越高。
    • getPriority():返回该线程的优先级
    • isInterrupted():判断是否打断,不会清楚打断标记
    • interrupt():中断这个线程,异常处理机制。实际上,并不会直接中断线程的执行,只是设置线程的中断标记,线程可以在适当的时候检查这个标记并自行决定中断执行。
      • 打断 sleep,wait,join 的线程,会清空打断状态
      • 打断正常运行的线程,不会清空打断状态
    • join():主线程将等待这个线程结束再顺序执行代码(异步变同步了),可以设置等待millis毫秒。
    • setDaemon(true):设置守护线程;可以通过 Thread.isDaemon()来判断
      • 默认情况下创建的线程都是用户线程(普通线程),需要等待所有的线程执行完毕后,进程才会结束
      • 当所有的用户线程退出后,守护线程会立马结束
      • 应用:垃圾回收器线程属于守护线程,tomcat用来接受处理外部的请求的线程就是守护线程

线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?

  • Java 中线程状态是用 6个enum 表示
    • NEW:初始状态,线程被构建,但是还没有调用start()
    • RUNNABLE:可能正在运行(运行状态),也可能正在等待 CPU 时间片(可运行状态)(,还可能处在OS层面的阻塞状态,如正在进行IO操作)。Java线程将操作系统中的就绪和运行两种状态统称为“运行中”。
    • BLOCKED:阻塞状态,表示线程阻塞于锁。等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
    • WAITING:无限期等待,等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。如,等待一个死循环 t.join(),当前线程将无限期等待。
    • TIME_WAITING:限期等待,无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
      调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
      ??睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
    • TERMINATED:终止状态。可以是线程结束任务之后自己结束,或者产生了异常而结束。

通常线程有哪几种使用方式?

有三种使用线程的方法:实现 Runnable 接口,实现 Callable 接口,继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
实现接口 VS 继承 Thread:实现接口会更好一些。因为 Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;类可能只要求可执行就行,继承整个 Thread 类开销过大。

  1. 通过继承Thread类,重写run方法。
    Thread 类也实现了 Runable 接口。当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
    1
    2
    3
    4
    public class MyThread extend Thread {
    @Override
    public void run() { ...}
    }
    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    // 启动过程为: thread.start()->中间过程->thread.run() 即重写的方法
    MyThread mt = new MyThread();
    // 开启异步线程:异步,创建一个新线程 mt
    mt.start();
    }
  2. 通过实现Runnable接口
    需要实现 run() 方法。通过 Thread 调用 start() 方法来启动线程。
    通过 Runnable接口 把线程和任务分开了;用 Runnable 更容易与线程池等高级 API 配合;用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
    1
    2
    3
    4
    public class MyRunnable implements Runnable {
    @Override
    public void run() { ...}
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    // 通过实现Runnable接口。实例化Thread,传递一个Runable任务
    // 启动过程为: thread.start()->中间过程>thread.run() 默认逻辑->runable.run() 实际业务
    Thread thread = new Thread(new MyRunnable());
    thread.start();
    }
    // 主线程main、线程thread 异步执行
  3. 实现 Callable 接口
    与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
    1
    2
    3
    public class MyCallable implements Callable<Integer> {
    public Integer call() { ...}
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
    }

基础线程机制有哪些?

  • Executor:管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。
    这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。主要有三种 Executor:
    • CachedThreadPool: 一个任务创建一个线程;
    • FixedThreadPool: 所有任务只能使用固定大小的线程;
    • SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。
  • 1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
    executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
    }
  • Daemon:守护线程,是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
    当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。
    1
    2
    3
    4
    public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
    }

线程的中断方式有哪些?

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

  • InterruptedException:通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
  • interrupted():如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
    但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
  • Executor 的中断操作:调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
    ???
  • 过时方法:stop(), suspend()休眠, resume()??

线程的互斥同步方式有哪些? 如何比较和选择?

Java 提供两种锁机制来控制多个线程对共享资源的互斥访问:JVM 实现的 synchronized,JDK 实现的 ReentrantLock
???
https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html#%E7%BA%BF%E7%A8%8B%E4%BA%92%E6%96%A5%E5%90%8C%E6%AD%A5

线程之间有哪些协作方式?

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

  • join():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
    对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
  • wait(),notify(),notifyAll()
    调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
  • await(),signal(),signalAll()
    java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

Java锁

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。

  1. 乐观锁 VS 悲观锁
    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。Java和数据库中都有此概念对应的实际应用。
    • 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
    • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
      乐观锁在Java中是通过使用无锁编程来实现,最常用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
      乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  2. 独享锁(排他锁) VS 共享锁
    • 独享锁也叫排他锁(写锁),指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
    • 共享锁(读锁)是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

CAS

不加锁的情况下保持数据读写一致。
CAS在java中的底层实现: lockcmpxchg

对象的内存布局??

对象头、类型指针、实例数据、对齐


共享模型之管程

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源;多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的。阻塞式的解决方案:synchronized,Lock;非阻塞式的解决方案:原子变量。

synchronized 解决方案

  • synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
    synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,拥有锁的线程可以安全的执行临界区内的代码,不会被线程切换所打断。临界区中的代码只能被串行运行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static int counter = 0;
    // 同一时刻 t1,t2 中只有其一能持有对象锁 room 以执行临界区中的代码
    static final Object room = new Object();
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
    // 当线程 t1 执行到 synchronized(room) 时持有了对象锁,执行 count++ 代码
    // 这中间即使 t1 的 cpu 时间片不幸用完,其仍将持有对象锁
    synchronized (room) {
    counter++; // 获取了对象锁后,自增的四个原子操作便作为一个整体,不可打断
    }
    }
    }, "t1");

    Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
    // 如果 t2 运行到 synchronized(room) 时 t1 仍持有对象锁,t2 将发生上下文切换,阻塞
    // 当 t1 执行完 synchronized{} 块内的代码,这时才释放对象锁,唤醒 t2 线程并让 t2 持有锁
    synchronized (room) {
    counter--;
    }
    }
    }, "t2");
  • 面向对象改进
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Room {
    // 需要保护的共享变量作为类的成员变量
    int value = 0;
    public void increment() {
    // 自身实例化的对象作为锁
    synchronized (this) {
    value++;
    }
    }
    public void decrement() {
    synchronized (this) {
    value--;
    }
    }
    public int get() {
    synchronized (this) {
    return value;
    }
    }
    }
  • 方法上的 synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Test{
    public synchronized void test() {
    // 在这个方法内,只有一个线程能够访问 Resource
    Resource++;
    }
    }
    // 等价于代码块级别的:
    class Test{
    public void test() {
    // 该实例化对象作为锁
    synchronized(this) {
    Resource++;
    }
    }
    }
  • 静态方法上的 synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test{
    public synchronized static void test() {
    }
    }
    // 等价于
    class Test{
    public static void test() {
    // 该类作为锁
    synchronized(Test.class) {

    }
    }
    }
  • 所谓的“线程八锁”
    ,,,

线程安全分析

  • 成员变量和静态变量是否线程安全?
    • 如果它们没有共享,则线程安全;
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
  • 局部变量是否线程安全?
    • 局部变量是线程安全的
      线程调用 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份(线程私有局部变量表),因此不存在共享
    • 但局部变量引用的对象则未必:如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,需要考虑线程安全
  • 常见线程安全类
    String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包下的类
    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。它们的每个方法是原子的,但注意它们多个方法的组合不是原子的!!
    1
    2
    3
    4
    5
    // 下面代码是否线程安全?不安全
    Hashtable table = new Hashtable();
    if( table.get("key") == null) {
    table.put("key", value);
    }
  • 不可变类线程安全性
    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
    String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?生成并且指向新的类。
  • 实例分析
    https://www.bilibili.com/video/BV16J411h7Rd?p=69&vd_source=ff210768dfaee27c0d74f9c8c50d7274
  • 例题
    https://www.bilibili.com/video/BV16J411h7Rd?p=69&vd_source=ff210768dfaee27c0d74f9c8c50d7274

Monitor

,,

wait/notify

,,

线程状态转换

,,

活跃性

,,

Lock

,,


非共享模型

,,


JUC 类汇总

JUC是 java.util.concurrent包的缩写,说白了就是并发场景进行多线程编程的工具类。
总的来说就是在并发场景下,怎么让程序尽量通过有限的硬件,高效的处理请求,并且保证程序“线程安全”。
开发高并发、高性能系统(1)加快响应用户的时间(2)使代码模块化、异步化、简单化(3)充分利用CPU的多核资源

Java 线程池 ??

  • JDK 线程池
    • ExecutorService
    • ScheduledExecutorService(定时任务)
  • Spring 线程池
    • ThreadPoolTaskExecutor
    • ThreadPoolTaskScheduler(定时任务)
  • 分布式定时任务
    • Spring Quartz:不同服务器上的quartz线程依赖于同一个数据库
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // JDK普通线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(5);
    // JDK可执行定时任务的线程池
    private ScheduledExecutorService scheduedExecutorSevice = Executors.newScheduledThreadPool(5);
    // Spring普通线程池
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    // Spring可执行定时任务的线程池
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    // 在 AlphaService 中给方法加上 @Async,使该方法在多线程环境下被异步的调用(被主线程并发并发,异步执行)
    // 在 AlphaService 中给方法加上 @Scheduled,只要有程序再跑就会自动调用
    @Autowired
    private AlphaService alphaService;

    // 1.JDK普通线程池
    public void testExecutorService() {
    Runnable task = new Runnable() {};
    for (int i = 0; i < 10; i++) {
    executorService.submit(task);
    }
    }
    // 2.JDK定时任务线程池
    public void testScheduledExecutorService() {
    Runnable task = new Runnable() {};
    scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);
    }
    // 3.Spring普通线程池
    public void testThreadPoolTaskExecutor() {
    Runnable task = new Runnable() {};
    for (int i = 0; i < 10; i++) {
    taskExecutor.submit(task);
    }
    }
    // 4.Spring定时任务线程池
    public void testThreadPoolTaskScheduler() {
    Runnable task = new Runnable() {};
    Date startTime = new Date(System.currentTimeMillis() + 10000);
    taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
    }
    // 5.Spring普通线程池(简化)
    public void testThreadPoolTaskExecutorSimple() {
    for (int i = 0; i < 10; i++) {
    // 并发,异步执行 AlphaService 中的 加了@Async 的 execute1()
    alphaService.execute1();
    }
    }
    // 6.Spring定时任务线程池(简化)
    public void testThreadPoolTaskSchedulerSimple() {
    // 自动调用 AlphaService 中的加上 @Scheduled 的方法
    }

JVM

JVM (Java虚拟机)本质上是一个计算机程序,他的职责是运行 Java字节码文件,解释为对应OS的机器指令。
JVM 包含内存管理、解释执行虚拟机指令、即时编译三大功能。
常见的JVM有HotSpot、GraalVM、OpenJ9等,另外DragonWell龙井JDK也提供了一款功能增强版的JVM。其中使用最广泛的是HotSpot。

JVM 跨平台原理

  • 本质是不同操作系统上提供了不同的JVM。(下载JDK/JRE for Linux/Windows…)
  • Java 代码“一次编写,到处执行”,其实是指由.java文件编译来的.class字节码文件(高级语言java –> javac编译 –> 字节码(提升程序运行的效率,跨平台运行))能够通过不同操作系统上的 JVM 解释为该操作系统的机器指令。
  • JVM 也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。

JVM 知识要点

1. 类加载器:将class字节码文件中的内容加载到内存中。 2. 运行时数据区域:负责管理JVM 使用到的内存,比如创建对象和销毁对象。 3. 执行引擎:将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能,用gc回收内存。 4. 本地接口:调用本地已经编译好的方法(不在字节码文件中)比如虚拟机中提供的cpp方法。


类的生命周期

  • 类的生命周期:加载,连接,初始化,使用,卸载
  • 其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
    在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
  1. 类的加载:查找并加载类的二进制数据
    • 类加载器通过一个类的全限定名来获取其定义的二进制字节流。可以使用Java代码拓展的不同的渠道。
    • JVM 将字节码中的信息保存到内存的方法区(虚拟概念)
    • 生成一个InstanceKlass对象(c++编写),保存类的所有信息,里边还包含实现特定功能比如多态的信息。
    • 在Java堆中生成一个代表这个类的java.lang.Class对象(包含字段比InstanceKlass中的少,控制访问范围和保证安全性),作用是在Java代码中去获取类的信息(反射),以及存储静态字段的数据(JDK8后) 加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
    • 加载.class文件的方式:从本地系统中直接加载、通过网络下载.class文件、从zip,jar等归档文件中加载.class文件、从专有数据库中提取.class文件、将Java源文件动态编译为.class文件
  2. 连接
    • 2.1 验证: 确保被加载的类的正确性。验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
      • 文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内(不能高于环境jdk版本)、常量池中的常量是否有不被支持的类型。
      • 元数据验证: 对字节码描述的信息进行语义分析(注意: 对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object之外。
      • 字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
      • 符号引用验证: 确保解析动作能正确执行。
    • 2.2 准备: 为类的静态变量分配内存,并将其初始化为默认值。这些内存都将在方法区中分配。
      • 这时进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量在对象实例化时随着对象一块分配在Java堆
      • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。 假设一个类变量的定义为: public static int value = 3;那么变量value在准备阶段过后的初始值为0,而非3,因为这时候尚未开始执行任何Java方法,把value赋值为3的动作将在初始化阶段才会执行。
      • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,系统不会为其赋予默认零值。
      • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值.而局部变量使用前必须显式地为其赋值,否则编译时不通过。
      • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
    • 2.3 解析: 把类中的符号引用(在字节码文件中使用编号来访问常量池中内容)转换为直接引用(符号->内存地址)
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  3. 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
    • 在Java中对类变量进行初始值设定有两种方式: 声明类变量是指定初始值、使用静态代码块为类变量指定初始值
    • JVM初始化步骤
      • 假如这个类还没有被加载和连接,则程序先加载并连接该类
      • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
      • 假如类中有初始化语句,则系统依次执行这些初始化语句
    • 类初始化时机: 只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下:
      • 创建类的实例,也就是new的方式
      • 访问某个类或接口的静态变量(注意final修饰且等号右边是常量的变量不会触发初始化)或者对该静态变量赋值
      • 调用类的静态方法、执行Main方法的当前类
      • 反射(如Class.forName(“com.pdai.jvm.Test”))
      • 初始化某个类的子类,则其父类也会被初始化
      • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
  4. 使用
    (即程序员工作阶段)类访问方法区内的数据结构的接口, 对象是Heap区的数据。
  5. 卸载
    • Java虚拟机将结束生命周期的几种情况:
      • 执行了System.exit()方法
      • 程序正常执行结束
      • 程序在执行过程中遇到了异常或错误而异常终止
      • 由于操作系统出现错误而导致Java虚拟机进程终止

JVM 类加载

类的生命周期的加载,连接,初始化阶段

类字节码详解

源代码通过编译器编译为字节码,再通过类加载子系统进行加载到JVM中运行;
字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读;
常用工具:对于本地/服务器上的字节码文件:javap;jclasslib,有idea插件版本,在代码编译(构建项目)后实时看到字节码文件内容。
对于运行中的程序:Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。(jar包,使用java -jar启动 Arthas包)服务器上文件使用javap命令直接查看,也可以通过arthas的dump命令导出字节码文件再查看本地文件。还可以使用jad命令反编译出源代码。

  1. 基础信息:magic魔数(Java文件头 CAFEBABE,软件通过文件头来校验文件的类型,而非文件扩展名),主副版本号(指编译字节码文件的JDK版本号(如主版本号52对应JDK8,减44) 用于判断当前字节码的版本与运行时的JDK是否兼容(低版本JDK不能运行高版本字节码)冲突时建议降低第三方依赖的版本号),访问标识(public,final等等),父类和接口
  2. 常量池:避免相同内容重复定义,节省空间。保存了字符串常量、类或接口名、字段名主要在字节码指令中使用。
    常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
    字节码指令中通过编号引用到常量池的过程称之为符号引用。
  3. 字段:当前类或接口声明的字段信息
  4. 方法:当前类或接口声明的方法信息,为字节码指令(在操作数栈和局部变量表上执行 iconst,istore,iload,iinc等指令,i++和++i区别就在指令iinc和iload的顺序颠倒)
    main方法:Java类的main方法是程序的入口点。在字节码文件中,这通常表示为public static void main(String[] args)方法。Java虚拟机(JVM)将通过public static void main(String[] args)方法来执行程序。字节码指令会调用这个方法,并它将启动程序的执行。
    init方法:类的构造函数,用于创建类的实例。在字节码文件中,方法是类的构造函数,用于初始化新对象的状态。这个方法通常是由invokespecial指令调用的,用于初始化新对象。
    clinit方法:类的类构造函数,也称为静态初始化方法。这个方法用于执行类的静态初始化代码块。在字节码文件中,方法通常由clinit指令表示。这个方法用于执行类级别的静态初始化操作
    Code属性:每个方法都有一个Code属性,其中包含了该方法的字节码指令,也就是方法体的实际执行代码
  5. 属性:类的属性,比如源码的文件名,内部类的列表等

类加载器

类加载器(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成byte[](只负责这一部分),接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的,无法直接通过代码查看,程序员难以直接调试。
    它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其他的类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
  • 扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器: Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器
因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点: 1. 在执行非置信代码之前,自动验证数字签名。2. 动态地创建符合用户特定需要的定制化构建类。 3.从特定的场所取得java class,例如数据库中和网络中。
URLClassLoader: 这是一个常见的用户自定义类加载器,可以从指定的URL加载类。在很多情况下,当需要从外部加载一些类或JAR文件时,使用URLClassLoader是比较方便的。URLClassLoader是Application ClassLoader的子类加载器。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制 ,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
  • 双亲委派机制,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去
    完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
  • 在Java中如何使用代码的方式去主动加载一个类呢?
    方式1:使用Class.forName方法,使用当前类的类加载器去加载指定的类
    方式2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载

双亲委派机制

由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。
双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否(被这个类加载器)加载过,再由顶向下(如果位于这个类加载器的加载目录中)进行加载。
双亲委派机制的好处有两点: 第一是避免恶意代码替换JDK中的核心类库,确保的完整性和安全性;二是避免一个类重复地被加载

打破双亲委派机制:

  1. 自定义类加载器
    通常情况下,我们都是直接使用系统类加载器。但有时,我们也需要自定义类加载器。比如 应用通过网络传输 Java 类的字节码,为保证安全性经过了加密处理,这时系统类加载器就无法对其进行加载;Tomcat通过自定义类加载器实现应用之间类隔离;
    自定义类加载器一般都是继承自 ClassLoader 类,重写 findClass方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class MyClassLoader extends ClassLoader {
    ...
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData = loadClassData(name);
    if (classData == null) { throw new ClassNotFoundException();
    } else { return defineClass(name, classData, 0, classData.length); // 连接阶段
    }
    }
    private byte[] loadClassData(String className) {
    String fileName = root + File.separatorChar
    + className.replace('.', File.separatorChar) + ".class";
    try {
    InputStream ins = new FileInputStream(fileName);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int bufferSize = 1024;
    byte[] buffer = new byte[bufferSize];
    int length = 0;
    while ((length = ins.read(buffer)) != -1) {
    baos.write(buffer, 0, length);
    }
    return baos.toByteArray();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    ...
    public static void main(String[] args) {
    MyClassLoader classLoader = new MyClassLoader();
    classLoader.setRoot("D:\\temp");
    Class<?> testClass = null;
    try {
    testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
    Object object = testClass.newInstance();
    System.out.println(object.getClass().getClassLoader());
    } catch () {... }
    }
    }
  2. 线程上下文类加载器 ???实际上没打破。。?**?????????**
    大量应用在 java底层,比如JDBC和JNDI等
  3. Osgi框架的类加载器
    历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载;还使用类加载器实现热部署功能。

JDK8 后的类加载器

由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。

  1. 启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件.启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
  2. 扩展类加载器被替换成了平台类加载器(Platform Class Loader)
    平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑

JVM 内存结构

Java 虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。!!注意不要和Java内存模型混淆了,,

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。【 类的生命周期的使用,卸载阶段
线程私有:程序计数器、虚拟机栈、本地方法区
线程共享:堆、方法区,堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

程序计数器

程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的内存地址。
它是一块很小的固定大小的内存空间,不会发生内存溢出 "OutOfMemoryError"

  1. 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
  2. 在多线程执行情况下,CPU会不停的做任务切换,这导致经常中断或恢复。JVM 需要通过程序计数器记录 CPU 切换前解释执行到那一句指令并继续解释运行。每个线程都有它自己的程序计数器,独立计算,不会互相影响。

Java 虚拟机栈

  • 早期也叫 Java 栈。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。
    作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
  • JVM栈 采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧(Stack Frame)来保存。
    IDEA 在 debug 时候,可以在 debug 窗口看到 Frames 中各种方法的压栈和出栈情况
  • 栈帧的组成
    • 局部变量表:在运行过程中存放所以的局部变量,包括实例方法的this对象,方法的参数,方法体中声明的局部变量
      • 编译成字节码文件时就可以确定局部变量表的内容,包含局部变量的编号、生效范围等;
        局部变量表数据在 字节码文件 - 方法 - Code - LocalVariableTable 查看。
      • 实际在栈帧中,局部变量表是一个数组,数组中每一个位置为(slot),long和double类型占两个槽,其他类型占一个。
      • 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。
      • 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致局部变量表
    • 操作数栈:JVM在执行指令的过程中用来存放临时数据的一块区域。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
      • 它是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。
      • 在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小
    • 帧数据
      • 动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
      • 方法出口:方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
      • 异常表的引用:异常表存放代码中异常的处理信息,包含了try代码块和catch代码块执行后跳转到的字节码指令位置
  • 栈内存溢出:一个线程的Java 虚拟机栈中,如果栈帧过多,占用内存超过栈内存可分配的最大大小就会出现内存溢出,出现StackOverflowError错误
    • 如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构,windows(64位)下的JDK8测试最小值为180k,最大值为1024m
    • 要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss。一般情况下,工作中即便使用了递归进行操作,栈的深度最多也只能到几百,不会出现栈的溢出。所以此参数可以手动指定为 -Xss256k 节省内存。

本地方法栈

  • Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是(c++编写的)native本地方法(如文件IO操作)的栈帧.
  • 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数,同时方便出现异常时也把本地方法的栈信息打印出来

**栈是运行时的单位,而堆是存储的单位**

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。

一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上,并且在栈上保存一个对象的引用(在堆上的地址)。
达到上限之后,就会抛出 OutofMemory错误。
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。

  • 堆内存中有三个值,used 指的是当前已使用的堆内存,total 是JVM已分配的可用堆内存,max 是JVM可以分配的最大堆内存。total 不足时,JVM会继续分配内存给堆,total 值增大但最多只能与 max 相等。
  • max 默认是系统内存的 1/4,total 默认是系统内存的 1/64。实际使用中一般都需要设置这两个值。
    Java服务端程序开发时,建议将 -Xmx(max最大值)和 -Xms(初始的total)设置为相同的值,避免再次申请内存。

方法区

  • 方法区是存放基础信息的位置,线程共享,主要包括三部分;
    • 类的元信息:类的基本信息 (元信息),一般称之为InstanceKlass对象。在类的加载阶段完成.
    • 运行时常量池:放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池
    • 字符串常量池:字符串常量池存储在代码中定义的常量字符串内容。比如String s2 = “abc” 的 “abc”即 s2这个字符串常量就会被放入字符串常量池(而String s1 = new String(“abc”) 中的 s1这个对象会被放入堆内存)
  • 方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下
    • JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
    • JDK8及之后的版本将方法区存放在元空间中,元空间位于OS维护的直接内存中,默认在不超过OS承受的上限可以一直分配
    • !! JDK6中字符串常量池在方法区(永久代)中,永久代在堆中;JDK7中从字符串常量池永久代中移出,仍在堆中;而JDK8后方法区(永久代->元空间)从堆中移出,字符串常量池仍在堆中

直接内存

直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域.

  • 在JDK1.4中引入了NIO机制,使用了直接内存,主要为了解决以下两个问题:
    1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
    2、I0操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路.
  • 要创建直接内存上的数据,可以使用ByteBuffer。语法: ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
    注意事项: arthas的memory命令可以查看直接内存大小,属性名direct。

Java 内存模型

??


JVM GC 垃圾回收

在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出
Java中为了简化对象的释放,引入了自动的垃圾回收 (Garbage Collection 简称 GC) 机制。【 类的生命周期的卸载阶段 ??
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。所以只用考虑线程不共享部分。JVM GC主要负责对堆上的内存进行回收。

方法区的回收

方法区中能回收的内容主要就是不再使用的类.判定一个类可以被卸载。需要同时满足下面三个条件
1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
2、加载该类的类加载器已经被回收
3、该类对应的java.lang.Class 对象没有在任何地方被引用
开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中。

堆的回收

如何判断堆上的对象可以回收? Java中的对象是否能被回收,是根据对象是否被引用来决定的。引用说明该对象还在使用,不允许被回收。

常见的有两种判断方法:引用计数法和可达性分析法。

  • 引用计数法:会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1.
    • 优点是实现简单,C++中的智能指针就采用了引用计数法
    • 缺点主要有两点: 1.每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响 2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
  • 可达性分析算法:Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象GRoot)和普通对象,对象与对象之间存在引用关系。如果其(反向?引用)到某个GC Root对象是可达的,对象就不可被回收。
    • 四大类 GCRoot 对象
      • 线程Thread对象(放在堆中,指向对应的栈)
      • 系统类加载器加载的java.lang.Class对象,引用类中的静态变量 ???、,,??
      • 监视器对象,用来保存同步锁 synchronized关键字持有的对象
      • 本地方法调用时使用的全局对象

五种引用

  • 强引用:可达性算法中描述的一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要关系存在普通对象就不会被回收。
  • 软引用:相对于强引用是一种比较弱的引用关系
    • 如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。
    • 软引用常用于缓存中(提高效率,不影响可用性)
    • 在JDK 1.2版之后提供了sftReference类来实现软引用。软引用的执行过程如下:
      1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
      2.内存不足时,虚拟机尝试进行垃圾回收。
      3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象
      4.如果依然内存不足,抛出OutofMemory异常
      1
      2
      3
      4
      5
      6
      7
      byte[] bytes = new byte[1024 * 1024 * 100];    // 建立数组(强引用对象??                
      SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes); // 建立软引用
      bytes = null; // 释放强引用
      System.out.println(softReference.get()); // :[B@2503dbd3
      byte[] bytes2 = new byte[1024 * 1024 * 100]; // 内存不足,释放软引用对象
      System.out.printIn(softReference.get()); // :null
      byte[] bytes2 = new byte[1024 * 1024 * 100]; // :直接抛OutofMemory异常
    • ???软引用队列:软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些SoftReference对象需要回收呢? SoftReference提供了一套队列机制:1、软引用创建时,通过构造器传入引用队列2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列3通过代码遍历引用队列,将SoftReference的强引用删除
  • 弱引用:弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时不管内存够不够都会直接被回收
    在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。
    弱引用对象本身也可以使用引用队列进行回收。
  • 虚引用:虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
  • 终结器引用:指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。

垃圾回收算法

  • 垃圾回收要做的有两件事:1、找到内存中存活的对象; 2、释放不再存活对象的内存,使得程序能再次利用这部分空间
  • 三种评价标准:堆使用效率、吞吐量,以及最大停时间不可兼得。
    一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。不同的垃圾回收算法,适用不同的场景。
  • 标记清除算法
    • 核心思想:1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。 2.清除阶段,从内存中删除没有被标记也就是非存活对象
    • 优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
    • 缺点:1.碎片化问题由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配 2.分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
  • 复制算法
    • 核心思想: 1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间 (From空间) 2.在垃圾回收GC阶段,将From中存活对象复制到To空间。 3.将两块空间的From和To名字互换
    • 优点:吞吐量高、不会发生碎片化
    • 缺点:内存使用效率低,每次只能让一半的内存空间来为创建对象使用
  • 标记整理算法:也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案.
    • 核心思想:1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象 2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间
    • 优点:内存使用效率高,不会发生碎片化
    • 缺点:整理阶段的效率不高

分代垃圾回收算法

  • 分代垃圾回收算法(GenerationalGC):将上述描述的垃圾回收算法组合进行使用,
    • 它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
    • 一般将堆分为新生代和老年代。新生代使用: 复制算法;老年代使用: 标记清除 或者 标记整理 算法
    • 将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
    • HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间不够用,此时需要依赖于老年代进行分配担保,即借用老年代的空间存储放不下的对象。
    • 系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。 老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。
      在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
    • 分代GC算法将堆分成年轻代和老年代主要原因有:
      1、可以通过调整年轻代和老年代的比例来适应不同型的应用程序,提高内存的利用率和性能。 2、新生代和老年代使用不同的垃圾回收算法,新生一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。 3、分代的设计中允许只回收新生代 (minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(fullgc),STW时间就会减少
  • 内存分配策略
    1. 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC
      大对象直接进入老年代:大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
      -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免 Eden 和 Survivor 区之间的大量内存复制。
    2. Minor GC 会把需要 eden中和 From需要回收的对象回收,把没有回收的对象放入To区。接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入SO。 !!如果to区满了,会将没有回收的对象放入老年代。
      注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
    3. 长期存活的对象进入老年代:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold 用来定义年龄阈值。
      动态对象年龄判定:虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,,,
    4. 当老年代中空间不足,无法放入新的对象时,先尝试minor gc(因为存在直接从 eden区放如老年代的情况)。如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出OutofMemory异常。
    5. 空间分配担保:在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
      如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
  • Full GC 的触发条件
    对于 Minor GC,其触发条件非常简单:当 Eden 空间满时就将触发一次。而 Full GC 则相对复杂,有以下条件:
    1.调用 System.gc() 2. 老年代空间不足 3. 空间分配担保失败 4. JDK 1.7及以前的永久代空间不足在 5. Concurrent Mode Failure
  • 内存回收策略
    Minor GC、Major GC、Full GC
    JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代,方法区)区域一起回收的,大部分时候回收的都是指新生代。针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类: 部分收集 (Partial GC),整收集(Full GC)。
    • 部分收集: 不是完整收集整个 Java 堆的垃圾收集。其中又分为:
      1. 新生代收集 (Minor GC/Young GC) : 只是新生代的垃圾收集。
      2. 老年代收集 (Major GC/Old GC) : 只是老年代的垃圾收集
        目前,只有 CMS GC 会有单独收集老年代的行为。很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
      3. 混合收集 (Mixed GC) : 收集整个新生代以及部分老年代的垃圾收集;目前只有 G1 GC 会有这种行为
    • 整堆收集 (Full GC) : 收集整个 Java 堆和方法区的垃圾

垃圾回收器

垃圾回收器是垃圾回收算法的具体实现
由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。

  • Serial + Serial Old 收集器
    Serial Serial Old
    单线程串行回收年轻代的垃圾回收器 Serial垃圾回收器老年代版本,采用单线程串行回收
    年轻代,复制算法 老年代,标记-整理算法
    优点 单CPU处理器下吞吐量非常出色
    缺点 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
    适用场景 Java编写的客户端程序或者硬件配置有限的场景 与Serial垃圾回收器搭配使用或者在CMS特殊情况下使用
  • ParNew + CMS 收集器
    ParNew CMS
    本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收 关注的是系统的暂停时间。允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
    参数 -XX:+UseParNewGC 新生代使用ParNew,回收器老年代使用串行回收器 XX:+UseConcMarkSweepGC
    年轻代,复制算法 老年代,标记-清除算法
    优点 多CPU处理器下停顿时间较短 系统由于垃圾回收出现的停顿时间较短,用户体验好
    缺点 吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用 1、内存碎片问题 2、退化问题 3、浮动垃圾问题
    适用场景 Java编写的客户端程序或者硬件配置有限的场景 大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
  • ParNew + CMS 收集器
    Parallel Scavenge Parallel Old
    JDK8默认的年轻代垃圾回收器多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点 是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
    参数 -XX:+UseParallelGC 或-XX:+UseParallelOldGC,可以使用Parallel Scavenge + Parallel Old这种组合
    年轻代,复制算法 老年代,标记-整理算法
    优点 吞吐量高,而目手动可控为了提高吞吐量,虚拟机会动态调整堆的参数 并发收集,在多核CPU下效率较高
    缺点 不能保证单次的停顿时间 暂停时间会比较长
    适用场景 后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理,大文件导出 与Parallel Scavenge配套使用
  • 垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选择如下:
    JDK8及之前:ParNew + CMS (关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、不建议 G1(较大堆并且关注暂停时间)
    JDK9之后:G1 (默认)

G1收集器

  • 它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。使命是未来可以替换掉 CMS 收集器。
    G1 将 Parallel Scavenge与 CMS的优点融合:1.支持巨大的堆空间回收,并有较高的吞吐量。2.支持多CPU并行垃圾回收 3.允许用户设置最大暂停时间。
  • G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,可以直接对新生代和老年代一起回收。
  • G1垃圾回收有两种方式:1、年轻代回收 (Young GC) 2、混合回收(Mixed GC)
    • 年轻代回收:回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地XX:MaxGCPauseMillis=n (默认200) 保证暂停时间。
    • 混合回收分为:初始标记 (initial mark) 、并发标记 (concurrent mark)、最终标记(remark或者FinalizeMarking)、并发清理 (cleanup) ;G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbagefirst)名称的由来。
    • 注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
  • G1 执行流程
    1、新创建的对象会存放在Eden区。当G1判新年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC
    2、标记出Eden和Survivor区域中的存活对象
    3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域
    4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区
    5、当某个存活对象的年龄到达值 (默认15),将被放入老年代
    6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humonous区,如果对象过大会横跨多个Region。
    7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到闻值时(-XX:InitiatingHeap0ccupancyPercent默认45%)
    会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成???不会产生内存碎片

JVM 排错调优

??


JVM 知识体系