后端技术记录

nginx

nginx的作用

反向代理

  1. 用户 ===> 反向代理地址 <=== (服务地址1,服务地址2,服务地址3)

    server {
    listen 80; #监听的端口
    server_name localhost; # 域名地址

    location /api/ {
    proxy_pass http://localhost:8080/admin/; #路径中包含api则将该请求转发到proxy_pass指向的地址
    }
    }

负载均衡

  1. 当我们部署多台服务,每次请求的时候我们需要指定该请求应该去访问哪台服务,尽量做到请求分配的合理。

  2. 负载均衡的配置

    http {
    include mime.types;
    default_type application/octet-stream;

    #新增的 配置集群地址
    upstream nacos-cluster {
    server 127.0.0.1:8843;
    server 127.0.0.1:8845;
    server 127.0.0.1:8847;
    }
    server {
    listen 80;
    server_name localhost;

    #新增的 使用/nacos代理集群地址,并负责负载均衡
    location /nacos {
    proxy_pass http://nacos-cluster;
    }
    }
    }

swagger

  1. 通过使用swagger注解可以生成接口文档,常用注解如下所示,对类和属性进行描述,在接口文档可以看到该描述

    swagger常用注解

对象属性拷贝

  1. 在开发过程中,对于前端传递过来的属性进行操作时,如果传递过来的只是部分属性,其他未传递的字段为null或者默认值,数量太多时更新也会非常麻烦,所以接受前端传递的属性一般再写一个DTO类。

  2. DTO(Data Transfer Object)数据传输对象的缩写,一般用来接受前端传递过来的参数来代替实体类。

  3. 但是在调用mappe层的数据库方法时,我们还是需要使用实体类,此时需要将DTO对象中的方法赋值到实体类中,可以直接调用实体类的set方法,也可以使用对象属性拷贝进行赋值。

       
    public void add(DTO dTO) {
    Entity entity = new Entity();
    // 对象属性拷贝,将dTO中的属性拷贝到entity中,前提是两个对象中的属性名相同,第一个参数是源,第二个参数是目标
    BeanUtils.copyProperties(dTO, entity);
    }

异常处理

  1. 异常处理类中,方法传递的参数为需要捕获的异常,可以是已存在的异常类,也可以是自定义的异常类,或者也可以在@ExceptionHandler后添加需要捕获的异常类,例如@ExceptionHandler(SQLIntegrityConstraintViolationException.class)

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
    return Result.error("SQL异常")
    }

线程局部变量

  1. 每次请求都属于一个线程,当我们可以该线程的一个地方得到一个值时,另一个地方需要用到这个值,此时就可以使用ThreadLocal

  2. ThreadLocal为每个线程提供单独的存储空间,具有线程隔离的效果,只有在该线程内才能获取到该值。

  3. ThreadLocal方法

    public void set(T value)
    public T get()
    public void remove()
  4. 方法封装

    public class ThreadContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
    threadLocal.set(id);
    }
    public static Long getCurrentId() {
    return threadLocal.get();
    }
    public static void removeCurrentId() {
    threadLocal.remove();
    }
    }

动态SQL

  1. 一些SQL语句不能简单通过注解的方式来完成,所以需要通过配置文件的方式来写SQL

    1. 首先在resources文件夹下新建一个mapper文件夹,文件夹下新建一个xml文件

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
      <mapper namespace="com.qr.mapper.EmployeeMapper">
      <!-- id对应方法名称,返回值类型对应的返回的泛型 -->
      <select id="queryPage" resultType="com.sky.entity.Employee">
      select * from employee
      <where>
      <if test="name != null and name != ''">
      and name like concat('%', #{name}, '%')
      </if>
      </where>
      order by create_time desc
      </select>
      </mapper>

    2. mapper中的namespace为mapper文件的地址,mapper标签内通过标签的形式书写SQL语句,id对应mapper中的方法名。

    3. 在application.yml中配置扫描该xml文件

      mybatis:
      #mapper配置文件
      mapper-locations: classpath:mapper/*.xml
    4. 文件目录

      配置文件目录

PageHelper

  1. PageHelper是一个帮助分页的插件,首先,引入坐标

    <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>
  2. 在service的实现层中,使用startPage接收页码和每页的数量

    // PageHelper底层使用的是拦截器,拦截器会拦截到我们的sql语句,然后在sql语句后面拼接limit语句,
    // 将传进去的页码每页数量存到ThreadLocal中,然后在sql中取出来拼到limit后
    PageHelper.startPage(dto.getPage(), dto.getPageSize());
    // 返回page类型是使用PageHelper后必须返回这个类型,固定格式
    Page<Entity> page = mapper.queryPage(dto);
  3. 结果

    SQL语句

日期返回

  1. 在返回前端日期时,返回的是类似于20230303122345格式,所以我们需要对日期格式进行处理

注解方式配置:

  1. 通过在实体类的属性上添加如下注解就可以实现

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
  2. 但是该方法在设置属性多的情况下有点繁琐,所以可以采用全局配置的方式来实现

全局配置消息转换器

创建FastJsonHttpMessageConverter的消息转换器对象

  1. 在全局配置类中,新增配置消息转换器

    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("开始配置消息转换器...");

    //创建消息转换器对象,FastJsonHttpMessageConverter
    FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
    //创建fastJsonConfig配置对象
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    //添加配置
    fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
    //添加到自己定义的转换器中
    converter.setFastJsonConfig(fastJsonConfig);
    // 将转换器添加到SpringMVC框架的转换器中,第一个参数代表优先使用咱们自己的转换器
    converters.add(0,converter);
    }

创建MappingJackson2HttpMessageConverter的消息转换器对象

  1. 也在全局配置类中进行添加

    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("开始配置消息转换器...");
    // 添加MappingJackson2HttpMessageConverter
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    //设置对象转换器,JacksonObjectMapper为自定义转换器
    converter.setObjectMapper(new JacksonObjectMapper());
    //将转换器添加到SpringMVC框架的转换器中
    converters.add(0,converter);
    }

区别

  1. FastJsonHttpMessageConverter和MappingJackson2HttpMessageConverter都是Spring框架中用于处理JSON数据的HttpMessageConverter接口的实现类。

  2. FastJsonHttpMessageConverter使用阿里巴巴的FastJson库来处理JSON数据,它能够快速地将Java对象转换为JSON格式的数据。

  3. MappingJackson2HttpMessageConverter则使用了Jackson库来处理JSON数据,它同样能够将Java对象转换为JSON格式的数据。

自定义注解

  1. 在开发过程中,可以自定义注解来完成相应的功能,比如对于一些数据库中公共字段的填充,我们可以使用自定义注解+AOP的方式实现

自定义注解的实现方式

  1. 新建package:annotation

    annotation

  2. 新建Java Class选择Annotation

    新建注解

  3. 注解文件中代码如下所示,其中Retention用来指定注解的生命周期,共有三种:SOURCE,CLASS和RUNTIME,SOURCE表示注解只在源代码中存在,编译时会被丢弃。CLASS表示注解在编译时会被保留到class文件中,但是在运行中会被丢弃。RUNTIME表示注解在运行时会被保留,可以通过反射机制读取。由于我们需要通过反射读取区分注解的value值,所以需要使用RUNTIME。OperationType value();代表注解的value值必须是一个OperationType枚举值。

    @Target(ElementType.METHOD) // 用于方法
    @Retention(RetentionPolicy.RUNTIME) // 编译时注解
    public @interface AutoFill {
    OperationType value(); // 数据库操作类型
    }
    public enum OperationType {
    UPDATE,
    INSERT
    }

  4. 注解注意事项

    1. 注解里面的参数只能用 public 或 默认(default) 这两个访问权修饰, OperationType value()这里设置为了default
    2. 参数成员只能用基本类型 byte,short,char,int,long,float,double,boolean八种基本数据类型 和 String,Enum,Class,annotations 等数据类型,以及这一些类型的数组.例如,String value();这里的OperationType value()的参数成员就为枚举类型
    3. 如果只有一个参数成员,最好把参数名称设为”value”,后加小括号
  5. 之后我们就能在方法上使用该注解了

反射

  1. 什么是反射?反射它允许程序在运行时检查和操作类、方法和属性。通过反射,程序可以动态地创建对象、调用方法、访问属性等,

  2. 对于一个实体对象,我们可以获取他的类,并通过调用类中的方法来实现赋值

    Object entity = args[0]; //AOP中参数第一个为一个entity对象,通过args[0]取出来
    //通过entity对象获取到他所属的类,再获取到声明的方法setAttribute,parameterTypes代表参数的类型,可以传int.class等等,然后会返回一个Method对象
    Method method = entity.getClass().getDeclaredMethod("setAttribute", [parameterTypes]);
    //通过invoke来执行指定对象的方法,第一个参数为调用该方法的实例,第二个参数为对该方法传递的参数值,这些参数必须在类型和顺序上与方法的参数类型相匹配。
    method.invoke(entity, [value])

Mysql重设密码

引用链接

注解式事务

  1. 实现类方法上添加@Transactional注解
  2. 主函数中添加@EnableTransactionManagement ,开启注解方式的事务管理

对于自增主键id在插入操作后获取id

  1. 当我们在实现类中对两个数据库进行操作时,在一个数据中插入了一条数据,另一个数据库操作需要使用该插入的数据的ID,由于这ID设置的是自增的,所以我们不能直接获取,需要通过一些方法进行获取。

  2. 书写动态SQL语句时,通过useGeneratedKeys=”true”可以将主键返回,该语句表示如果插入的表id以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。 keyProperty=”id”代表要返回的主键名称

    <insert id="[Metgid]" useGeneratedKeys="true" keyProperty="id">
    .........
    </insert>
  3. 返回后,我们可以实例化与数据库相对应的实体类,然后通过get方法获取该ID

批量插入操作

  1. 对于有多条数据需要一起插入导数据库中,除了循环执行SQL外还可以进行批量插入操作

  2. 直接将需要插入的数据结合传到Mapper层,通过动态SQL中的foreach执行动态插入,collection为传到mapper层的参数名称,item和index就是循环的值和索引,separator每个SQL使用什么分隔符隔开

    <insert id="insertBatch">
    insert into table (id, name,value)
    values
    <foreach collection="param" item="item" index="index" separator=",">
    (#{item.id}, #{item.name}, #{item.value})
    </foreach>
    </insert>

Redis

  1. 是一个基于内存的key-value结构数据库,而MySQL是基于磁盘存储的。基于内存存储所以速度快,适合存放一些访问量较高的数据。

  2. 下载redis后,在redis文件夹中打开命令行窗口,然后运行以下命令启动服务

    .\redis-server.exe .\redis.windows.conf
  3. 之后就可以通过命令行或者可视化界面进行连接

    1. 命令行方式

      #-h代表连接的地址
      #-p代表redis服务的端口
      #-a代表redis服务的密码
      .\redis-cli.exe -h localhost -p 6379 -a 123456
    2. 可视化界面,直接在AnotherRedisDesktopManager下载地址下载,然后点击新建

      新建连接

Redis中的数据类型

  1. 这里的数据类型指value的数据类型,key的数据类型一般都是字符串类型

字符串string

  • 操作命令

    SET key value
    GET key
    SETEX keys seconds value #可以设置过期时间 setex code 30 1234
    SETNX key value #当key不存在的时候设置key的值

哈希hash

  • hash结构也类似于key-value,key — value(field — value),适合存储对象

  • 操作命令

    HSET key field value  # HSET data name tom 存储
    HGET key field # hget data name 获取
    HDEL key field # hdel data name 删除
    HKEYS key # hkeys data 获取所有字段
    HVALS key # hvals data 获取所有值

列表list

  • 按插入顺序排序,可存在重复

  • 操作命令

    LPUSH key value1 value2  # left头部插入数据
    LRANGE key start stop #获取指定范围的元素,索引从0开始,start指定为0,stop指定为-1返回所有元素
    RPOP key #移除并获取列表最后一下元素
    LLEN key #获取列表长度

集合set

  • 无序集合,不允许重复

  • 操作命令

    SADD key member1 member2 # 向集合添加一个或者多个成员
    SMEMBERS key # 返回集合中所有成员
    SCARD key # 获取集合成员数
    SINTER key1 key2 # 获取给定所有集合的交集
    SUNION key1 key2 # 获取所有给定集合的并集
    SREM key member1 mermber2 # 删除集合中的一个或者多个成员

有序集合sorted set/zset

  • 有序集合,集合中每个元素关联一个分数,根据分数升序排序,不允许重复

  • 操作命令

    ZADD key score1 member1 score2 member2 # 向有序集合添加一个或者多个成员
    ZRANGE key start stop # 通过索引区间返回有序集合中指定区间内的成员
    ZINCRBY key increment member # 有序集合中对指定成员的分数上加上增量increment
    ZREM key member1 mermber2 # 删除有序集合中的一个或者多个成员

通用命令

KEYS * # 查询所有key
KEYS set* # 查询以set开头的key
EXISTS key # 检查key是否存在
TYPE key # 返回key所存储的值的类型
DEL key # key存在时删除key

Java中使用redis

  1. 导入Spring Boot Starter Data Redis坐标,继承了spring-boot的配置,这里引入spring-boot-starter-parent的好处是在添加启动器时不用申明版本号

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置redis数据源(地址,端口号,密码等等)

    spring:
    redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0
  3. 编写配置类,创建RedisTemplate对象

    /**
    * Redis配置类
    */
    @Configuration
    @Slf4j
    public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
    log.info("开始配置RedisTemplate模板对象");
    // 1.创建RedisTemplate对象
    RedisTemplate redisTemplate = new RedisTemplate();
    // 2.设置RedisTemplate的连接工厂
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    // 3.设置RedisTemplate的序列化器
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    return redisTemplate;
    }
    }

  4. 通过RedisTemplate对象操作Redis

    // 1.获取操作字符串的ValueOperations对象
    ValueOperations valueOperations = redisTemplate.opsForValue();
    // 2、获取操作hash
    HashOperations hashOperations = redisTemplate.opsForHash();
    // 3、获取操作list
    ListOperations listOperations = redisTemplate.opsForList();
    // 4、获取操作set
    SetOperations setOperations = redisTemplate.opsForSet();
    // 5、获取操作zset
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
  5. 通用命令

    redisTemplate.keys(*); //查询符合要求的ley
    redisTemplate.delete(key); //删除指定的key或者集合,该命令不能使用通配符
  6. 使用场景,在Controller中,对于用户发送过来的请求,比如查询请求,可以先查询redis缓存,如果有,直接返回,如果没有,查询数据库后存储到redis中,这里存进去的是什么类型,在取的时候就取什么类型。比如存进去的是自定义VO类型,取的时候也是这个类型。注意:在数据发生更新/删除/新增的时候,都要清除redis缓存。

httpClient

  1. 导入httpclient坐标

  2. 使用

    //创建httpclient对象
    CloseableHttpClient httpClient = HttpClients.createDefault();
    //创建get方式请求对象
    HttpGet httpGet = new HttpGet("http://baidu.com");
    //发送请求,接收响应
    CloseableHttpResponse response = httpClient.execute(httpGet);

    // 获取服务器返回的状态码
    int statusCode = response.getStatusLine().getStatusCode();
    System.out.println(statusCode);
    // 获取服务器返回的数据
    HttpEntity entity = response.getEntity();
    String body = EntityUtils.toString(entity);
    System.out.println(body);
    // 关闭连接
    response.close();
    httpClient.close();
  3. 在SpringCloud中学的RestTemplate和Fegin都是在httpClient基础上进行封装的,也都可以使用

  4. 目的:在java程序内部进行发送请求

Spring Cache

  1. 是一个框架,实现了基于注解的缓存功能,只需要简单的加一个注解,就能实现缓存功能
  2. Spring Cache可以切换底层缓存实现,主要支持EHCache,Caffeine,Redis
  3. 常用注解
    • @EnableCaching 开启缓存注解功能,通常加在启动类上
    • @Cacheable 在方法执行前先查询缓存中是否有数据,如果有直接返回,如果没有,调用方法并将返回值放入缓存
    • @CachePut 将方法的返回值放到缓存中
    • @CacheEvict 将一条或多条数据从缓存中删除

使用

  1. 导入Spring Cache坐标

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
  2. 导入实现底层缓存方式的包(切换底层实现方式直接切换引入的包即可)

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  3. 在启动类上添加注解@EnableCaching

  4. 使用

    • 在Controller方法上添加@CachePut 注解,比如,要新增一条数据,插入到数据库中后,也需要在缓存中存储一份

      @PostMapping
      @CachePut(cacheNames = "userCache", key = "#user.id") //生成的key就是userCache::1 value就是方法返回的数据
      public User add(@RequestBody User user) {
      //操作数据库插入数据,并将自增主键返回
      return user;
      }
    • @Cacheable 注解,查询数据时,去缓存先查询,有直接返回,没有执行方法后再存到缓存中

      @GetMapping
      @Cacheable(cacheNames = "userCache", key = "#id") //key的名称保持和上面一致
      public User getById(Long id) {
      User user = userService.getById(id);
      return user;
      }
    • @CacheEvict

      @DeleteMapping
      @CacheEvict(cacheNames = "userCache", key = "#id") // @CacheEvict(cacheNames = "userCache", allEntries = true)
      public void deleteById(Long id) {
      userService.deleteById(id);
      }
      @DeleteMapping
      @CacheEvict(cacheNames = "userCache", allEntries = true) //allEntries,删除所有的userCache键值对
      public void deleteAll() {
      userService.deleteAll();
      }

Spring Task

  1. Spring框架提供的任务调度工具,可以按照约定时间自动执行某个代码逻辑,是一个定时任务框架。

cron表达式

  1. 通过cron表达式可以定义任务触发的时间
  2. 构成规则:分为6-7个区域,由空格分开,每个域从左到右代表:秒,分钟,小时,日,月,周,年(可选)
  3. 示例:2024年1月25日上午10点整cron表达式为 0 0 9 25 1 ? 2024

使用

  1. 导包,由于SpringTask包很小,直接包含在spring-context中,所以不用手动导入

  2. 启动类添加注解@EnableScheduling开启任务调度

  3. 自定义定时任务类

    @Component
    @Slf4j
    public class Task {
    // 每5s执行一次
    @Scheduled(cron = "0/5 * * * * ?")
    public void run() {
    log.info("Task.run()");
    }
    }

WebSocket

  1. WebSocket是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工通信——-浏览器和服务器只需完成一次握手,两者之间就可以创建持久性连接,并进行双向数据传输.

使用

  1. 导入坐标

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
  2. 新建websocketServer文件

    /**
    * WebSocket服务
    */
    @Component
    @ServerEndpoint("/ws/{sid}") //前端请求的websocket地址
    public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
    * 连接建立成功调用的方法
    */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
    System.out.println("客户端:" + sid + "建立连接");
    sessionMap.put(sid, session);
    }

    /**
    * 收到客户端消息后调用的方法
    *
    * @param message 客户端发送过来的消息
    */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
    System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
    * 连接关闭调用的方法
    *
    * @param sid
    */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
    System.out.println("连接断开:" + sid);
    sessionMap.remove(sid);
    }

    /**
    * 群发
    *
    * @param message
    */
    public void sendToAllClient(String message) {
    Collection<Session> sessions = sessionMap.values();
    System.out.println("发送websocket消息");
    for (Session session : sessions) {
    try {
    //服务器向客户端发送消息
    session.getBasicRemote().sendText(message);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    }
  3. 新建websocket配置文件

    /**
    * WebSocket配置类,用于注册WebSocket的Bean
    */
    @Configuration
    public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
    }

    }

  4. 在需要使用的地方注入WebSocketServerd调用其中的方法

Apache POI

  1. 是一个处理微软各种文件格式的开源项目,可以使用POI在Java程序中对文件进行操作,一般都是用来操作excel文件

使用

  1. 导入坐标

    <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.16</version>
    </dependency>
    <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.16</version>
    </dependency>
  2. 写操作

    //在内存中创建一个excel文件
    XSSFWorkbook workbook = new XSSFWorkbook();
    //在excel表中创建一个sheet页
    XSSFSheet sheet = workbook.createSheet("info");
    //在sheet中创建一个行对象,索引从0开始
    XSSFRow row = sheet.createRow(1);
    //在该行创建一个单元格并设置该单元格的值
    row.createCell(1).setCellValue("hello");
    //new一个file文件输出流,将内存文件写到磁盘D上
    FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\test.xlsx"));
    excel.write(fileOutputStream);
    //关闭资源
    fileOutputStream.close();
    excel.close();
  3. 读操作

    FileInputStream fileInputStream = new FileInputStream(new File("D:\\info.xlsx"));
    XSSFWorkbook excel = new XSSFWorkbook(fileInputStream);
    // 获取sheet页
    XSSFSheet sheet = excel.getSheetAt(0);
    // 获取有文字的最后一行
    int lastRowNum = sheet.getLastRowNum();
    // 获取第一行
    XSSFRow row = sheet.getRow(0);
    // 获取有文字的最后一列
    short lastCellNum = row.getLastCellNum();
    // 获取第一列
    row.getCell(0);
    // 获取某一行某一列的值
    row.getCell(0).getStringCellValue();
    //关闭资源
    fileInputStream.close();
    excel.close();
  4. 通过输出流在浏览器中下载excel

    ServletOutputStream outputStream = response.getOutputStream();
    excel.write(outputStream);

JSR303校验前端传递的数据

简单校验

  1. JSR 303 是 Java Specification Request (JSR) 的一部分,它定义了 Java Bean Validation API,这是一个用于验证 Java 应用程序中的对象属性的标准。JSR 303 的目标是提供一种简单的方式来验证 Java Bean 的属性值,而无需编写大量的验证逻辑。

  2. 添加依赖

    <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
    </dependency>
  3. 在实体类属性上添加注解,可以添加@NotBlank, @URL, @Pattern等(可以写一个正则)

public class BrandEntity implements Serializable {
@NotBlank(message="品牌名不能为空)
private String name;
}

  1. controller中添加@Valid注解
@RequestMapping("/save")
public Result save(@Valid @RequestBody BrandEntity entity){
entityService.save(entity);
return Result.ok();
}
  1. 如果不符合校验,就会返回400错误码,但是返回的信息就会不统一,相当于抛错了,所以进行处理
  2. 在第五步的基础上,可以在参数中添加一个BindingResult来接收校验异常。然后统一用封装的Result进行返回
@RequestMapping("/save")
public Result save(@Valid @RequestBody BrandEntity entity, BindingResult result){
// 错误处理
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
result.getFieldErrors().forEach((item) -> {
map.put(item.getField(), item.getDefaultMessage());
});
return R.error(400, "提交的数据不合法").put("data", map);
}
entityService.save(entity);

return Result.ok();
}
  1. 在上一步的基础上进行优化,不在controller中进行处理,写一个公共的类,统一进行处理

    @Slf4j
    @RestControllerAdvice(basePackages = "com.qr.mall.product.controller")
    public class ExceptionControllerAdvice {
    /**
    * 处理字段校验异常
    * @param e
    * @return
    */
    @ExceptionHandler(value=Exception.class)
    public R handleVaildException(MethodArgumentNotValidException e){
    log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
    Map<String, Object> errorMap = new HashMap<>();
    e.getBindingResult().getFieldErrors().forEach((fieldError) -> {
    errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
    });
    return R.error(400, "数据校验出现问题").put("data", errorMap);
    }
    }

分组校验

  1. 分组校验是指在不同的情况下,对某个字段的要求不一样,比如,在新增时,ID是自增的,可以不用传,但是修改的时候,就必须要传,

  2. @NotBlank, @URL, @Pattern等注解中新加groups参数

    public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
    * 品牌id
    */
    @NotNull(message = "修改必须指定品牌名", groups = {UpdateGroup.class})
    @Null(message = "新增不能指定品牌名", groups = {AddGroup.class})
    @TableId
    private Long brandId;
    /**
    * 品牌名
    */
    @NotBlank(message="品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private String name;
    /**
    * 品牌logo地址
    */
    @URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class})
    @NotNull(groups = {AddGroup.class})
    private String logo;
    }
  3. 将之前Controller中的@Valid注解换成@Validated注解, 并添加上指定的操作类型

     /**
    * 保存
    */
    @RequestMapping("/save")
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
    brandService.save(brand);

    return R.ok();
    }
  4. 使用了@Validated注解后,没有标注分组的字段校验都不会生效

  5. 上面的AddGroup.classUpdateGroup.class只是简单的声明接口类型,用于区分添加还是更新

    public interface AddGroup {
    }
    public interface UpdateGroup {
    }

自定义注解

  1. 自定义注解,实现在注解中声明需要的值,传过来的值不在声明中,则校验不通过

  2. 在字段上添加自定义注解

     /**
    * 显示状态[0-不显示;1-显示]
    */
    @ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateGroup.class}, message = "值必须是0或1")
    private Integer showStatus;
  3. ListValue的实现

    1. **@Documented**:
      • 表示这个注解应该被 javadoc 或类似的工具记录。默认情况下,注解不会出现在 javadoc 中,但使用 @Documented 可以使其出现在文档中。
    2. **@Constraint(validatedBy = {})**:
      • 指定了这个注解是一个约束注解,并且需要一个或多个验证器来实现验证逻辑。validatedBy 属性通常指定一个实现 ConstraintValidator 接口的类数组。需要提供一个验证器类
    3. **@Target**:
      • 指定了这个注解可以应用的目标。这里的目标包括方法、字段、注解类型、构造函数、参数和类型使用。
    4. **@Retention**:
      • 指定了这个注解的保留策略。RetentionPolicy.RUNTIME 表示注解将保留在运行时,因此可以通过反射访问。
    @Documented
    @Constraint(validatedBy = {ListValueValidator.class})
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ListValue {

    String message() default "{必须提交指定的值}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int[] vals() default {};
    }
  4. 验证器类, 判断传过来的值是否在vals中, 两个方法都是重写ConstraintValidator接口中的方法

    public class ListValueValidator implements ConstraintValidator<ListValue, Integer> {

    private Set<Integer> set = new HashSet<>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
    int[] vals = constraintAnnotation.vals();
    for (int val : vals) {
    set.add(val);
    }
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
    return set.contains(value);
    }
    }

xml文件中使用mapper中的参数

  1. 不使用@Param对参数进行标注

    1. mapper文件如下所示

      @Mapper
      public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

      void updateCategory(String name, Long catId);
      }
    2. 对应的xml

    <update id="updateCategory">
    update pms_category_brand_relation
    set catelog_name = #{param2}
    where catelog_id = #{param1}
    </update>
  2. 使用@Param参数

    1. mapper文件

      @Mapper
      public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

      void updateCategory(@Param("name") String name,@Param("id") Long catId);
      }
    2. xml文件

      <update id="updateCategory">
      update pms_category_brand_relation
      set catelog_name = #{name}
      where catelog_id = #{id}
      </update>
  3. 但是在gym项目中,没添加@Param注解, 直接使用参数名也没报错,,具体原因目前未知., 参照网上的说法,,都不太符合.。等以后遇见了再看。

Elastic Search

倒排索引表

Java中使用

  1. 引入依赖

    <dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
    </dependency>
  2. i编写配置类,给容器中注入一个RestHighLevelClient

    @Configuration
    public class ElasticSearchConfig {

    public RestHighLevelClient esRestClient() {
    RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(
    new HttpHost("192.168.202.1", 9200, "http")
    )
    );
    return client;
    }
    }

JVM

知识点

  1. jps控制台命令,展示所有java进程的ID

  2. 使用HSDB查看Java虚拟机内存信息

    1. 运行到安装的JDK的lib目录下,运行以下命令,

      • -cp .\sa-jdi.jar:这是设置 Java 类路径的选项。类路径是 JVM 查找类文件的位置。这个选项设置类路径为当前目录下的 sa-jdi.jar 文件。sa-jdi.jar 是 Serviceability Agent 的一部分,Serviceability Agent 是一个可以用来调试、监控和故障排查 JVM 的工具。

      • sun.jvm.hotspot.HSDB:这是要运行的主类的全名。主类是程序的入口点,main 方法就在这个类中。

    //使用当前目录下的 `sa-jdi.jar` 作为类路径,运行 `sun.jvm.hotspot.HSDB` 类的 `main` 方法,也就是启动 HSDB。
    java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

类的生命周期

加载

  1. 第一阶段类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
  2. 类加载器加载完类后,Java虚拟机会将字节码中的信息保存到方法区(虚拟概念,并不是堆栈)中,生成一个InstanceKlass保存类的所有信息,主要包含基本信息,常量池,字段,方法,虚方法表(实现多态的基础)等
  3. Java虚拟机会在堆中生成一份与方法区中数据类似的java.lang.Class文件,作用就是在Java代码中去获取类的信息以及存储静态字段的数据(存储静态字段的数据在JDK8及之后才存在这里)
  4. InstanceKlass与堆中的Class相互关联,从一个都可以找到另一个
  5. 开发者一般访问的是堆中的Class对象,InstanceKlass是使用C++编写的对象,一般不能直接访问,并且方法区中的一些方法开发者用不到,比如之前说的虚拟方法表,实现多态并不需要程序员参与,所以为了安全访问和简洁,InstanceKlass是不允许访问的。

连接

校验

准备

  1. 为静态变量赋初始值,而不会赋值你写的值,如果是被final修饰的,在准备阶段就会直接赋你写的那个值,因为虚拟机认为被fina修饰的不会在变化了

解析

  1. 将常量池中的符号引用转变为直接内存引用

初始化

  1. 为上面赋了初始值的静态变量赋值

  2. 执行顺序:

    • 构造代码块会优先于构造方法执行,一个类被加载并初始化只会执行一次,而构造方法可以创建多个对象,每次创建都会执行一次。
    • 静态代码块(也称为静态初始化块)会在构造代码块(也称为实例初始化块)之前执行。这是因为静态代码块在类加载时就会被执行,而构造代码块在创建对象时才会被执行。
    • 静态变量和静态代码块:静态变量和静态代码块的执行顺序与它们在代码中的顺序相同。它们只在类加载时执行一次。
    • 实例变量和构造代码块:实例变量和构造代码块的执行顺序与它们在代码中的顺序相同。它们在每次创建对象时都会被执行。
    • 构造方法:构造方法最后执行,每次创建对象时都会被执行。
    //输出DACBCB
    public class Test1() {
    public static void main(String[] args) {
    System.out.println("A");
    new Test1();
    new Test1();
    }

    public Test1() {
    System.out.println("B");
    }

    {
    System.out.println("C");
    }
    static {
    System.out.println("D")
    }

    }

类加载器

  1. 类加载器(ClassLoader)的主要作用是将类的字节码文件加载到内存中,使得 JVM 可以使用这些类。类加载器的工作过程主要包括以下三个步骤:

    1. 加载(Loading):类加载器负责从文件系统、网络或者其他来源加载 Java 类的字节码文件,然后将这些字节码文件转换为 JVM 中的内部表示。

    2. 链接(Linking):链接过程包括验证类文件的正确性、为类的静态字段分配存储空间以及将符号引用转换为直接引用等步骤

    3. 初始化(Initialization):初始化阶段主要是执行类的静态初始化代码,包括静态字段的初始化以及静态块的执行。

  1. 类加载器还有一个重要的特性,那就是双亲委派模型。当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有当父类加载器无法完成此加载任务时,才由自己来处理这个加载任务。这种模型可以确保 Java 核心库的类型安全,所有的 Java 应用都至少引用 java.lang.Object 类,也就是说在运行期,java.lang.Object 这个类会被加载到 Java 虚拟机中;如果这个加载过程由 Java 应用自己的类加载器来完成的话,那么很可能就会在 JVM 中存在多个版本的 java.lang.Object 类,而且这些类之间还是不兼容的,相互不可见的。

Java虚拟机底层类加载器

  1. JDK8之前,底层类加载器主要为启动类加载器,通过代码获取类加载器,如果输出为null代表为启动类加载器,因为上层是访问不到下层的类加载器的

    ClassLoader classLoader = Arrays.class.getClassLoader();

JDK提供或者默认的类加载器

  1. JDK8之前,Java类加载器主要为扩展类加载器和应用类加载器(自己定义的类)

  2. 使用arthas可以看到对应的类加载器都加载了那些类,在查看应用类加载器加载的类时,可以看到还加载了启动类加载器和扩展类加载器所加载的类,这个主要和类的双亲委派机制有关。

    # 启动arthas
    java -jar arthas-boot.jar
    # 找到对应启动的程序,选择对应数字,进入该类
    # 之后输入以下代码,获取对应类加载器的hash
    classloader -l
    # 输入以下代码,获取对应类加载器加载的类
    classloader -c <hash值>

双亲委派机制

  1. 双亲委派机制是 Java 类加载器的一个重要特性。工作原理如下:

    1. 当一个类加载器被调用来加载类时,它首先不会自己去加载,而是把这个请求委派给父类加载器。
    2. 这个过程是递归的,也就是说,首先会判断最顶层的启动类加载器(Bootstrap ClassLoader)能否加载这个类,如果能加载,就由它完成加载,如果不能,就由它的子加载器(Extension ClassLoader)尝试加载。
    3. 如果所有的父类加载器都不能加载这个类,那么再由自己尝试加载。
  2. 这种机制可以确保 Java 核心库的类型安全,以及避免一个类被重复的加载。借助于双亲委派机制,Java 类加载器可以确保所有的 Java 应用都是由同一个类加载器加载的,这个类加载器就是启动类加载器(Bootstrap ClassLoader)。

  3. 启动类加载器 是扩展类加载器的父类,扩展类加载器是应用程序类加载器的父类

打破双亲委派机制

自定义类加载器

  1. 在classLoader源码中,双亲委派机制主要是以下代码

    if (parent != null) {
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    // 自定义
    public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 首先,检查这个类是否已经被加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
    try {
    // 尝试使用自定义的方式加载这个类
    c = findClass(name);
    } catch (ClassNotFoundException e) {
    // 如果自定义的方式加载失败,就尝试使用父类加载器来加载这个类
    if (getParent() != null) {
    c = getParent().loadClass(name);
    } else {
    // 如果没有父类加载器,就尝试在引导类加载器中查找这个类
    c = getSystemClassLoader().loadClass(name);
    }
    }
    }
    return c;
    }

    @Override
    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 name) {
    // 在这里实现你的类数据加载逻辑
    return null;
    }
    }

线程上下文类加载器

  1. JDBC使用的该技术(SPI机制)(其实是没有打破双亲委派机制的)

    • DriverManager

      image-20240603085123185

OSGI模块化

JDK9及之后类加载器

  1. 将扩展类加载器替换成了平台类加载器(由于引入了模块的概念,所以这个并无实用,只是为了和JDK9之前保持一致)

运行时内存

线程不共享区域(线程内部)

  1. 程序计数器(不会内存溢出):通过将类加载器加载后的字节码文件传到虚拟机中,虚拟机将该线程的字节码地址存储到程序计数器中,程序计数器中保存的永远是下一次要执行的指令地址

  2. Java虚拟机栈(会内存溢出):每个方法执行时入栈,称为栈帧,执行完毕后出栈

    • 栈帧的组成

      1. 局部变量表:本质为数组,数组中每个位置称为槽(slot),long和double占两个,其他类型包括引用类型占1个,主要保存方法的this实例,方法的参数,方法体中声明的局部变量(分前后顺序)

      2. 操作数栈,存放指令执行过程中的中间数据,编译期间就已经确定了操作数栈的深度

      3. 帧数据:

        1. 动态链接:当前类的字节码指令引用了其他类的属性或方法时,需要将符号引用转换成对应的运行时常量池中的内存地址,动态链接就保存了运行时常量池的内存地址映射关系

        2. 方法出口

        3. 异常表

      4. int i = 0字节码指令解析

        1. iconst_0,将常量0放到操作数栈中
        2. 字节码指令istore_1,将这个操作数栈中的数据弹出,存到1的位置,
        3. iload_1则是将局部变量表执行索引1的位置的数据(复制一份)放到操作数栈中
      //最后结果为0,因为顺序是,先将0存到操作数栈中,再取到局部变量表中,i++字节码指令是在局部变量表中进行加1,之后将操作数栈中的0赋值到局部变量表中,1就会被覆盖,所以结果就为0
      int i = 0;
      i = i++;
  3. 本地方法栈:存储native本地方法栈帧,和Java虚拟机栈在实现上使用了同一栈空间

线程共享

  1. 方法区:JDK8存在元空间中,元空间位于操作系统维护的直接内存中,JDK8之前,方法区位于堆区域的一个叫永久代空间内

    1. 类的元信息

    2. 运行时常量池:每一个加载到JVM中的类或接口,都有一个对应的运行时常量池

    3. 字符串常量池:字符串常量池是运行时常量池的一个特殊部分

      • 存储代码中定义的常量字符串内容
      String a = "1";
      String b = "2";
      String c = "12";
      String d = "1" + "2";// 位于方法区中
      System.out.println(d == c);// true
      String e = a + b;
      System.out.println(e == c);// false 因为变量连接使用StringBuilder,创建出来的对象位于堆上,
      • intern()方法:String.intern() 方法会将字符串对象放入字符串常量池,如果池中已有相同的字符串,它会返回池中已存在的字符串引用

        StringBuilder s1 = new StringBuilder("abc");
        System.out.println(s1.toString().intern().equals(str1));
  2. 堆区(会溢出):创建出来的对象都位于堆上,栈上的局部变量表存放对象的引用,静态变量也可以存放对象的引用,这样通过静态变量就可以实现在线程之间的共享

image-20240606084633468

  • used:已使用的堆内存
  • total:Java虚拟机已经分配的可用堆内存
  • max:Java虚拟机可以分配的最大堆内存

垃圾回收

  1. 垃圾回收主要负责对上的内存进行回收

方法区回收

  1. 方法区中主要回收不再使用的类
  2. 一个类可以被卸载主要满足三个条件
    1. 此类所有的实例对象已经被回收,在堆中不存在任何该类的实例对象以及子类的对象
    2. 加载该类的类加载器已经被回收
    3. 该类对应的java.lang.Class对象没有在任何地方被引用

堆回收

判断堆上的对象有没有被引用

引用计数法
  1. 每个对象维护一个引用计数器,当对象被引用时加一,取消引用时减一
  2. 缺点:
    1. 每次取消和引用都需要维护计数器,对性能有影响
    2. 存在循环引用的问题,A引用B,B引用A会出现对象无法回收的问题
可达性分析
  1. 主要分析垃圾回收的根对象(GC Root)和普通对象的引用关系

  2. 根对象(GC Root)主要包括

    1. 线程Thread对象,引用线程栈帧中的方法参数,局部变量等
    2. 系统类加载器加载的java.lang.Class对象
    3. 监视器对象,用来保存同步锁synchronized关键字持有的对象
    4. 本地方法调用时使用的全局对象(虚拟机调用的)
  3. 引用方式

    1. 强引用:一般GC root对象是不能被回收的,所以只需要分析通过GC root对象可达的对象就是不可回收的,不可达的就是可回收的对象,被GC root关联的就是强引用

    2. 软引用:一个对象只有软引用对象关联到他,当程序内存不足,就会将软引用的引用的数据进行回收,软引用使用场景一般是缓存,软引用对象必须被GC root强引用,不然软引用对象就会被回收,并且SoftReference对象本身也需要被回收(使用引用队列进行回收)。

      // 通过new关键字创建软引用对象
      new SoftReference<对象类型>(需要被软引用的对象)

      byte[] bytes = new byte[1024 * 1024 * 100];
      SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
      bytes = null;//解除bytes的强引用
      System.out.println(softReference.get());//输出软引用的地址

      byte[] byte2 = new byte[1024 * 1024 * 100];
      System.out.println(softReference.get());//输出软引用的地址,当虚拟机内存为100M时,由于内存不足,软引用就会被释放,这个就会输出为null
    3. 弱引用:与软引用不同的是,垃圾回收时,无论内存够不够都会被回收,通过WeakReference类来实现

    4. 虚引用:不能通过虚引用对象获取到到包含的对象,唯一用途是当对象被垃圾回收的时候会收到通知

    5. 终结器引用(先不了解)

垃圾回收算法

  1. Java垃圾回收过程会通过单独GC线程来完成,无论哪一种GC算法,都会有部分阶段需要停止所有用户的线程,这个过程被称为Stop The World(STW),所以STW的时间是评判垃圾回收算法的一个标准
  2. 最大暂停时间,每次暂停时间的最大值
  3. 堆使用的效率:不同的垃圾回收算法,对堆内存的使用方式不同
  4. 以上三点不可兼得,根据不同场景选择不同的算法

标记清除算法

  1. 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析,从GC root开始通过引用链遍历所有对象
  2. 清除阶段:从内存中删除没有被标记的对象
  3. 缺点:
    1. 碎片化问题
    2. 分配速度慢

复制算法

  1. 将堆内存分成两块:From空间和To空间,在对象分配阶段,只使用From空间。
  2. 在垃圾回收阶段,将From中存活的对象(根据GC root引用链判断,先将GC root移动到To,在根据引用链将被引用的对象也移动到To中)复制到To空间
  3. 将两块空间的名字互换
  4. 优点:
    1. 不会产生碎片空间:因为复制之后就会将对象按顺序摆放
    2. 吞吐量高:只需要遍历一次存活对象复制到To空间,比标记整理算法少一次遍历,但是不如标记清除
  5. 缺点:内存使用效率低,每次只能使用一半的空间

标记整理算法

  1. 对标记清除算法的碎片化空间进行处理
  2. 标记阶段:和标记清除一样
  3. 标记整理:将存活的对象移动到堆的另一端,清理掉存活对象的空间
  4. 缺点:效率不高

分代GC

  1. 将上述的垃圾回收算法进行结合
  2. 将内存区域划分成年轻代和老年代,年轻代存放存活时间比较短的对象,老年代存放存活时间比较长的对象,老年代的内存默认大于新生代(年轻代),年轻代中还包括伊甸区,幸存者区(有两块S1(From区域),S2(To区域))
  3. 步骤:
    1. 创建出来的对象,首先会被放到Eden伊甸园区
    2. 如果伊甸园区满了,新创建对象无法放入,就会触发年轻代GC,称为Minor GC,或者Young GC,使用复制算法,将伊甸区和S1From区域中需要回收的对象回收,没有回收的放入S2 To区域中
    3. 接下来,S1变为To区,S2变为From区,当伊甸区满时,依然会触发Minor GC,只不过接下来是将S2和伊甸区中需要回收的对象回收,没有回收的放入S1中
    4. 每次Minor GC都会为存活的对象记录年龄,初始值为0,每Minor GC一次,存活下来就加一。
    5. 当年龄大于15时,默认其会永久存在,就把该对象放入老年代区。
    6. 当老年代空间不足时,会先尝试Minor GC,如果还是不足就会触发Full GC,对整个堆进行回收

垃圾回收器(都为分代算法)

  1. 垃圾回收器分为老年代和年轻代,所以除了G1这个垃圾回收器,其他垃圾回收器必须组合使用

image-20240618083702309

Serial垃圾回收器(年轻代)

  1. Serial:一种单线程回收年轻代的垃圾回收器
  2. 回收算法:复制算法
  3. 优点:单CPU下处理器吞吐量(用户线程的执行时间/总时间)非常出色
  4. 缺点:多CPU下吞吐量不如其他垃圾回收,年轻代空间如果偏大会让用户线程处于长时间等待
  5. 单线程的垃圾回收算法在单核 CPU 下吞吐量高,是因为在单核 CPU 下,所有的计算任务都是串行执行的,包括垃圾回收任务。这意味着当垃圾回收任务在执行时,其他的计算任务都会暂停,直到垃圾回收任务完成。这种方式虽然会导致计算任务的暂停,但是由于没有并发执行的任务,所以不会出现资源竞争和上下文切换的开销,因此在单核 CPU 下,单线程的垃圾回收算法的吞吐量会比较高。然而,在多核 CPU 下,多个计算任务可以并行执行。如果垃圾回收算法仍然是单线程的,那么在垃圾回收任务执行时,其他的 CPU 核心可能会空闲,因为它们不能帮助执行垃圾回收任务。这就浪费了多核 CPU 的并行计算能力,导致吞吐量降低。此外,如果在多核 CPU 下使用单线程的垃圾回收算法,那么垃圾回收任务可能需要在不同的 CPU 核心之间移动,每次移动都会产生上下文切换的开销,这也会降低吞吐量。

SerialOld垃圾回收器(老年代)

  1. SerialOld:Serial回收期的老年代版本,也是单线程回收
  2. 回收算法:标记整理算法
  3. 优缺点同Serial

ParNew垃圾回收器(年轻代)

  1. ParNew:使用多线程进行垃圾回收
  2. 回收算法:复制算法
  3. 优点:多CPU处理器下停顿时间较短
  4. 缺点:不如G1

CMS(老年代)

  1. CMS:关注系统的暂停时间,允许用户线程和垃圾回收线程在某些情况下同时执行,减少用户等待时间
  2. 回收算法:标记清除算法
  3. 缺点:
    1. 内存碎片,因为使用了标记清除算法
    2. 浮动垃圾问题:由于有些步骤是并发执行,无法清理并行过程中的产生的浮动垃圾,不能做到完全的垃圾回收
    3. 退化问题,第二步导致老年代的内存不足,CMS就会退化成Serial Old单线程回收老年代
  4. 执行步骤:
    1. 初始标记:极短的时间标记GC root直接关联的对象
    2. 并发标记:标记所有对象,用户线程不需要暂停
    3. 重新标记:并发标记阶段有些对象可能发生变化,存在错标,漏标等情况,需要重新标记
    4. 并发清理:清理死亡对象,用户线程不需要暂停

Parallel Scavenge(年轻代)

  1. PS:JDK8默认的垃圾回收器,多线程并行回收,关注的是系统吞吐量,具备自动调整堆大小的能力
  2. 回收算法:复制算法
  3. 缺点:不能保证单次停顿时间
  4. 可以设置虚拟机参数:最大暂停时间,吞吐量,最大暂停时间和吞吐量是冲突的,可能某一个属性不会达到所设置的预期

Parallel Old(老年代)

  1. PO:为Parallel Scavenge设计的老年代版本
  2. 回收算法:标记整理算法

Garbage First(简称G1)

  1. G1垃圾回收器垃圾回收有两种方式
    1. 年轻代回收:回收Eden,Survivor区
    2. 混合回收:回收所有年轻代和部分老年代
  2. 回收算法:复制算法
  3. JDK9之后建议使用G1垃圾回收器
  4. G1之前的垃圾回收器,内存结构一般是连续的,但是G1的整个堆会被划分多个大小相等的区域,称之为Region,分为Eden,Survivor区,Old区
  5. 执行流程:
    1. 年轻代回收:
      1. 新创建的对象放在Eden区,当G1判断年轻代不足时,无法分配对象时执行年轻代回收
      2. 标记Eden,Survivor区存活的对象
      3. 根据配置的最大暂停时间选择某些区域将存活对象复制到新的Survivor区中(年龄 + 1),并清除选择的区域。因为年轻代回收过程中会去记录每次垃圾回收时每个Survivor和Eden区的平均耗时,以作为下次回收时的参考依据。
      4. 某个存活对象的年轻达到阈值15,放到老年代中
      5. 对于部分对象大小超过Region的一半,会直接放入老年代
    2. 混合回收
      1. 对堆内存容量达到45%时,会触发混和GC,回收所有年轻代和部分老年代,采用复制算法来完成
      2. 初始标记:标记GC root直接关联的对象,串行执行
      3. 并发标记:将第一步中标记的对象引用的对象标记为存活,并行执行
      4. 最终标记:标记引用改变的对象,不管新创建的,不再关联的对象,串行执行
      5. 并发清理:将存活对象复制到别的Region,不会产生碎片
Author: Yang Wa
Link: https://blog.wxywxy.cn/2024/01/04/后端知识总结/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.