面试总结一

1. HashMap底层

HashMap由数组和链表组合构成的数据结构

2. 加锁方式

自动锁Synchronized、手动锁Lock、安全类Java.util.concurrent

3. 线程池有几种

3.1 newCachedThreadPool:这是第一种可缓存线程池,线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程
3.2 newFixedThreadPool:这是个可重用固定个数的线程池,当当前线程数大于总数则会进行等待,等待线程池内的线程执行完,相对来说比较少占内存,如果等待线程过多也是相对消耗资源的
3.3 ScheduledThreadPool:这是个可重用固定个数的线程池,当前线程数大于总数则会进行等待,并且可以设置线程延迟执行时间
3.4 newSingleThreadExecutor:单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。缺点就是单线程执行,当线程多的时候执行速度比较慢
3.5 自定义线程池(ThreadPoolExecutor和BlockingQueue连用)
BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储。,一个取出操作。在保证并发安全的同时,提高了队列的存取效率

4. Spring Ioc

Inversion of Control,即“控制反转”,对于Spring 框架来说,就是有Spring来负责控制对象的声明周期和对象间的关系。简单来说,控制指的是当前对象对内部成员的控制权;控制反转指的是,这种控制权不由当前对象管理了,由其他(类、
第三方容器)来管理

5. AOP 原理、使用场景

AOP是面向切面编程,通过预编译方式和运行时期动态代理实现程序功能的统一维护的一种技术,简单来说就是统一处理某一“切面”(类)的问题的编程思想,比如统一处理日志、异常、幂等性、事务、各种各样的 xxxTemplate等

6. DispatcherServlet

在这里插入图片描述

7. 有哪些容器

Java容器类类库的用途是“持有对象”,并将其划分为两个不同的概念:

7.1 Collection:一个独立元素的序列,这些元素都服从一条或者多条规则。 List必须按照插入的顺序保存元素,而set不能有重复的元素Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)
7.2 Map:一组成对的“键值对”对象,允许你使用键来查找值

8. Spring 容器 Vs SpringMVC

父子容器关系,Spring是根容器,SpringMVC是其子容器,并且在Spring根容器中对于SpringMVC容器中的Bean是不可见的,而在SpringMVC容器中对于Spring根容器中的Bean是可见的,也就是子容器可以看见父容器中的注册的Bean,反之就不行

9. MyBatis 分页插件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10. SpringBoot 自动化配置原理

11. Shiro VS Spring Security

Spring Security 是一个比 Shiro 优秀很多的权限管理框架,但是重量级、配置繁琐、门槛高,安全性更高

12. SpringBoot的启动流程

1、new了一个SpringApplication对象,使用SPI技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例

2、调用SpringApplication.run() 方法

3、调用createApplicationContext()方法创建上下文对象,创建上下文对象同时会注册spring的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等)

4、调用refreshContext() 方法启动Spring容器和内置的Servlet容器

13. Spring模块

Spring 框架是一个分层架构,由 7 个模块组成
在这里插入图片描述

每个模块的作用如下:
核心容器(Spring Core):核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开
Spring 上下文(Spring Context):Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI(Java命名和目录接口)、EJB(Enterprise Java Beans称为Java 企业Bean)、电子邮件、国际化、校验和调度功能
Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中
Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写 的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构
Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map
所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构
Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作
Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP

14. 索引是什么?怎么知道有没有索引

索引:索引就是对数据库中的一列或者多列的值排序( 进行标记)的一种结构
索引的作用:可以快速的查询数据库中的特定信息(加速检索数据库中表的数据)
mysql中有以下几种索引
1.普通索引
2.唯一索引
3.主键索引
4.组合索引
5.全文索引

15. Spring Cloud的组件

Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务
在这里插入图片描述

16. MyBatis是不是最适用Java的框架

Mybaits 的优点:
(1) 基于 SQL 语句编程,相当灵活,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句, 并可重用
(2) 与 JDBC 相比,减少了 50%以上的代码量,消除了大量冗余的代码,不需要手动开关连接
(3) 很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC
支持的数据库 MyBatis都支持)
(4) 能够与 Spring 很好的集成
(5) 提供映射标签,支持对象与数据库的 ORM 字段关系映射
3、MyBatis 框架的缺点:
(1) SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求
(2) SQL 语句依赖于数据库,导致数据库移植性差
4、MyBatis 框架适用场合:
(1) MyBatis是一个足够灵活的 DAO 层解决方案
(2) 对性能的要求很高,或者需求变化较多的项目, MyBatis 将是不错的选择

17. 谈谈对Spring Cloud的看法

SpringCloud是Spring为微服务架构思想做的一个一站式实现
  从某种程度可以理解为,微服务是一个概念、一个项目开发的架构思想。SpringCloud是微服务架构的一种java实现

SpringCloud是基于SpringBoot的一套实现微服务的框架
  为微服务体系开发中的架构问题,提供了一整套的解决方案,它提供了微服务开发所需要的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等组件。最重要的是,跟SpringBoot框架一起使用的话,会让开发微服务架构的云服务非常方便
  Spring Cloud是一个基于Spring Boot实现的云应用开发工具;Spring boot专注于快速、方便集成的单个个体,Spring Cloud是关注全局的服务治理框架;spring boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring boot来实现
  
SpringCloud五大核心组件

服务注册发现-Netflix Eureka
配置中心 - spring cloud config
负载均衡-Netflix Ribbon
断路器 - Netflix Hystrix
路由(网关) - Netflix Zuul
Spring Boot的哲学就是约定大于配置。既然很多东西都是一样的,为什么还要去配置

  1. 通过starter和依赖管理解决依赖问题
  2. 通过自动配置,解决配置复杂问题
  3. 通过内嵌web容器,由应用启动tomcat,而不是tomcat启动应用,来解决部署运行问题
    Spring Cloud体系就比较复杂了。基本可以理解为通过Spring Boot的三大魔法,将各种组件整合在一起,非常简单易用

通过上面SpringCloud架构图,我们看一下 Spring Cloud主要的组件,以及它的访间流程

1、外部或者内部的非 Spring Cloud目都统一通过API网关(Zuul)来访可内部服务
2、网关接收到请求后,从注册中心( Eureka)获取可用服务
3、由 Ribbon进行均负载后,分发到后端的具体实例
4、微服务之间通过 Feign进行通信处理业务
5、 Hystrix负责处理服务超时熔断
6、 Turbine监控服务间的调用和焠断相关指标
总结一句:Spring boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring boot,属于依赖的关系

18. Spring Cloud Alibaba的几个组件

Sentinel: 把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性

Nacos: 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台

RocketMQ: 一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务

Dubbo: Apache Dubbo™ 是一款高性能 Java RPC 框架

Seata: 阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案

Alibaba Cloud ACM: 一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品

Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提
供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据

Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务

Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道

19. ES的搜索流程

索引过程

  1. 创建一个IndexWriter 用来写索引文件,它有几个参数,INDEX_DIR 就是索引文件所存放的位置,Analyzer 便是用来对文档进行词法分析和语言处理的
  2. 创建一个Document 代表我们要索引的文档
  3. 将不同的Field 加入到文档中。我们知道,一篇文档有多种信息,如题目,作者,修改时间,内容等。不同类型的信息用不同的Field 来表示
  4. IndexWriter 调用函数addDocument 将索引写到索引文件夹中
    检索过程
  5. IndexReader 将磁盘上的索引信息读入到内存,INDEX_DIR 就是索引文件存放的位置
  6. 创建IndexSearcher 准备进行搜索
  7. 创建Analyer 用来对查询语句进行词法分析和语言处理
  8. 创建QueryParser 用来对查询语句进行语法分析
  9. QueryParser 调用parser 进行语法分析,形成查询语法树,放到Query 中
  10. IndexSearcher 调用search 对查询语法树Query 进行搜索, 得到结果TopScoreDocCollector

20. Redis用在哪

什么是Redis?
Redis全称(Remote Dictionary Server)
Redis本质上是一个Key-Value类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB
Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上

Redis有哪些适合的场景
1、会话缓存(Session Cache)
最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件

2、全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC
再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端
此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面

3、队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看

4、排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到

5、发布/订阅
最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统

21. 下单的流程 具体的业务

1 下单过程
预订者浏览某个已发布的会议
进入会议的详情页面,该页面显示了所有可预订的座位分类信息
预订者选择好要预订的座位分类,录入每个分类的预定数量
预订者点击提交按钮,提交下单请求到Server端
2 Server端订单处理过程
Server端Controller提交处理订单的命令到分布式消息队列,然后后台的Command Processor就可以消费该命令并异步处理订单了。核心处理步骤
生成订单(初始状态)
扣减库存(内部有预扣逻辑)
更新订单状态
Server端Controller发送命令后,立即重定向页面到查单订单处理结果页面,该页面会以轮训的方式查看订单处理结果
3 用户等待订单处理结果
如果下单成功(库存足够),预订者被导航到支付页面进行支付;预订者可以选择支付,也可以放弃支付
如果下单失败(库存不足),则提示用户下单失败,因为库存不足
如果轮训等待超时,则告诉用户暂时无法知道订单处理状态,然后当前页面继续定时(5s)轮训订单处理结果
4 用户支付订单
如果支付成功,则提示预订者订单处理完成,交易完成
如果拒绝支付,则关闭订单
如果超过规定时间(15分钟)未支付,则视作订单已过期,系统自动回收订单所预定的座位
5 流程结束

22. SQL优化 索引 索引用在哪

一般在什么字段上建立索引?

1.表的主键外键必须有

2.经常与其他表进行连接的表,在连接字段上建立索引

3.where后面的判断条件

4.选择性高的字段上

5.建立在小字段上,对于大字段甚至超长字段不要建立索引

使用索引字段的时候应该注意什么?

下面是不走索引的情况

1.select *可能导致不走索引

2.在索引字段上使用!=、<>、not in 、 not exits

3.在字段上使用函数不会走索引,可以建立函数索引

4.空值导致不走索引

5.like ‘%a’ ,百分号在前面不走索引

6.字符型索引列会导致优化器认为需要扫描索引大部分数据,导致放弃索引用全表扫描

23. 缓存击穿 缓存穿透 缓存雪崩

一、缓存处理流程
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果
二、缓存穿透
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
三、缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
设置热点数据永远不过期
加互斥锁,互斥锁参考代码如下:
说明:

      1)缓存中有数据,直接走上述代码13行后就返回结果了
     2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现
      3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点

四、缓存雪崩
描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中
设置热点数据永远不过期

24. RabbitMQ应用场景

1.背景
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue)的开源实现

2.应用场景
2.1异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种1.串行的方式;2.并行的方式
(1)串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西
在这里插入图片描述

(2)并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间
在这里插入图片描述

假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并性已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,英爱是写入数据库后就返回
(3)消息队列
引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
在这里插入图片描述

由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍

2.2 应用解耦
场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口
在这里插入图片描述

这种做法有一个缺点:

当库存系统出现故障时,订单就会失败。(这样马云将少赚好多好多钱^ ^)
订单系统和库存系统高耦合
引入消息队列
在这里插入图片描述

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功

库存系统:订阅下单的消息,获取下单消息,进行库操作
就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失(马云这下高兴了)
流量削峰
流量削峰一般在秒杀活动中应用广泛
场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
1.可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
在这里插入图片描述

1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面
2.秒杀业务根据消息队列中的请求信息,再做后续处理

3.系统架构
在这里插入图片描述

几个概念说明:
Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列
Queue:消息的载体,每个消息都会被投到一个或多个队列
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key:路由关键字,exchange根据这个关键字进行消息投递
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离
Producer:消息生产者,就是投递消息的程序
Consumer:消息消费者,就是接受消息的程序
Channel:消息通道,在客户端的每个连接里,可建立多个channel

4.任务分发机制
4.1Round-robin dispathching循环分发
RabbbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的,如果现在load加重,那么只需要创建更多的Consumer来进行任务处理。

4.2Message acknowledgment消息确认
为了保证数据不被丢失,RabbitMQ支持消息确认机制,为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack,而应该是在处理完数据之后发送ack
在处理完数据之后发送ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以安全的删除它了
如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer,这样就保证在Consumer异常退出情况下数据也不会丢失
RabbitMQ它没有用到超时机制.RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有正确处理,也就是说RabbitMQ给了Consumer足够长的时间做数据处理
如果忘记ack,那么当Consumer退出时,Mesage会重新分发,然后RabbitMQ会占用越来越多的内存

5.Message durability消息持久化
要持久化队列queue的持久化需要在声明时指定durable=True
这里要注意,队列的名字一定要是Broker中不存在的,不然不能改变此队列的任何属性
队列和交换机有一个创建时候指定的标志durable,durable的唯一含义就是具有这个标志的队列和交换机会在重启之后重新建立,它不表示说在队列中的消息会在重启后恢复
消息持久化包括3部分

  1. exchange持久化,在声明时指定durable => true

hannel.ExchangeDeclare(ExchangeName, “direct”, durable: true, autoDelete: false, arguments: null);//声明消息队列,且为可持久化的

2.queue持久化,在声明时指定durable => true

channel.QueueDeclare(QueueName, durable: true, exclusive: false, autoDelete: false, arguments: null);//声明消息队列,且为可持久化的

3.消息持久化,在投递时指定delivery_mode => 2(1是非持久化)

channel.basicPublish(“”, queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes())

如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的,如果exchange和queue两者之间有一个持久化,一个非持久化,则不允许建立绑定
注意:一旦创建了队列和交换机,就不能修改其标志了,例如,创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重现创建

6.Fair dispath 公平分发
你可能也注意到了,分发机制不是那么优雅,默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。n是取余后的,它不管Consumer是否还有unacked Message,只是按照这个默认的机制进行分发
那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却毫无休息的机会,那么,Rabbit是如何处理这种问题呢?
在这里插入图片描述

通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message,换句话说,在接收到该Consumer的ack前,它不会将新的Message分发给它

channel.basic_qos(prefetch_count=1)
注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计

7.分发到多个Consumer
7.1Exchange
先来温习以下交换机路由的几种类型:
Direct Exchange:直接匹配,通过Exchange名称+RountingKey来发送与接收消息
Fanout Exchange:广播订阅,向所有的消费者发布消息,但是只有消费者将队列绑定到该路由器才能收到消息,忽略Routing Key
Topic Exchange:主题匹配订阅,这里的主题指的是RoutingKey,RoutingKey可以采用通配符,如:*或#,RoutingKey命名采用.来分隔多个词,只有消息这将队列绑定到该路由器且指定RoutingKey符合匹配规则时才能收到消息
Headers Exchange:消息头订阅,消息发布前,为消息定义一个或多个键值对的消息头,然后消费者接收消息同时需要定义类似的键值对请求头:(如:x-mactch=all或者x_match=any),只有请求头与消息头匹配,才能接收消息,忽略RoutingKey
默认的exchange:如果用空字符串去声明一个exchange,那么系统就会使用”amq.direct”这个exchange,我们创建一个queue时,默认的都会有一个和新建queue同名的routingKey绑定到这个默认的exchange上去
channel.BasicPublish(“”, “TaskQueue”, properties, bytes)
因为在第一个参数选择了默认的exchange,而我们申明的队列叫TaskQueue,所以默认的,它在新建一个也叫TaskQueue的routingKey,并绑定在默认的exchange上,导致了我们可以在第二个参数routingKey中写TaskQueue,这样它就会找到定义的同名的queue,并把消息放进去
如果有两个接收程序都是用了同一个的queue和相同的routingKey去绑定direct exchange的话,分发的行为是负载均衡的,也就是说第一个是程序1收到,第二个是程序2收到,以此类推
如果有两个接收程序用了各自的queue,但使用相同的routingKey去绑定direct exchange的话,分发的行为是复制的,也就是说每个程序都会收到这个消息的副本。行为相当于fanout类型的exchange
下面详细来说:

7.2 Bindings 绑定
绑定其实就是关联了exchange和queue,或者这么说:queue对exchange的内容感兴趣,exchange要把它的Message deliver到queue

7.3Direct exchange
Driect exchange的路由算法非常简单:通过bindingkey的完全匹配,可以用下图来说明
在这里插入图片描述

Exchange和两个队列绑定在一起,Q1的bindingkey是orange,Q2的binding key是black和green
当Producer publish key是orange时,exchange会把它放到Q1上,如果是black或green就会到Q2上,其余的Message被丢弃

7.4 Multiple bindings
多个queue绑定同一个key也是可以的,对于下图的例子,Q1和Q2都绑定了black,对于routing key是black的Message,会被deliver到Q1和Q2,其余的Message都会被丢弃

7.5 Topic exchange
对于Message的routing_key是有限制的,不能使任意的。格式是以点号“.”分割的字符表。比如:”stock.usd.nyse”, “nyse.vmw”,“quick.orange.rabbit”。你可以放任意的key在routing_key中,当然最长不能超过255 bytes
对于routing_key,有两个特殊字符

*(星号)代表任意一个单词
#(hash)0个或多个单词
在这里插入图片描述

Producer发送消息时需要设置routing_key,routing_key包含三个单词和连个点号o,第一个key描述了celerity(灵巧),第二个是color(色彩),第三个是物种:
在这里我们创建了两个绑定: Q1 的binding key 是”.orange.“; Q2 是 “…rabbit” 和 “lazy.#”:

Q1感兴趣所有orange颜色的动物
Q2感兴趣所有rabbits和所有的lazy的.
例子:rounting_key 为 “quick.orange.rabbit”将会发送到Q1和Q2中
rounting_key 为”lazy.orange.rabbit.hujj.ddd”会被投递到Q2中,#匹配0个或多个单词
8.消息序列化
RabbitMQ使用ProtoBuf序列化消息,它可作为RabbitMQ的Message的数据格式进行传输,由于是结构化的数据,这样就极大的方便了Consumer的数据高效处理,当然也可以使用XML,与XML相比,ProtoBuf有以下优势:
1.简单
2.size小了3-10倍
3.速度快了20-100倍
4.易于编程
6.减少了语义的歧义.
ProtoBuf具有速度和空间的优势,使得它现在应用非常广

25. Redis最常用的数据类型是哪一个

字符串string
列表list
散列hash
集合set
有序集合sorted set

26. JVM

Java内存区域
说一下 JVM 的主要组成部分及其作用?
在这里插入图片描述

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)

Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area

Execution engine(执行引擎):执行classes中的指令

Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口

Runtime data area(运行时数据区域):这就是我们常说的JVM的内存

作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能

下面是Java程序运行机制详细说明

Java程序运行机制步骤

首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java
再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class
运行字节码的工作是由解释器(java命令)来完成的

在这里插入图片描述

从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构

说一下 JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
在这里插入图片描述

不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:

程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据

深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变

深复制:在计算机中开辟一块新的内存地址用于存放复制的对象

说一下堆栈的区别?
物理地址

堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)

栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快

内存分别

堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈

栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的

存放的内容

堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行

PS:

静态变量放在方法区
静态的对象还是放在堆
程序的可见度

堆对于整个应用程序都是共享、可见的

栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同

队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的

操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈
可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作
操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除
HotSpot虚拟机对象探秘
对象的创建
说到对象的创建,首先让我们看看 Java 中提供的几种对象创建方式:

Header 解释
使用new关键字 调用了构造函数
使用Class的newInstance方法 调用了构造函数
使用Constructor类的newInstance方法 调用了构造函数
使用clone方法 没有调用构造函数
使用反序列化 没有调用构造函数
下面是对象创建的主要流程:

在这里插入图片描述

虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行方法

为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:

指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

在这里插入图片描述

处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB
在这里插入图片描述

对象的访问定位
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式

指针: 指向对象,代表一个对象在内存中的起始地址
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址

句柄访问
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:

在这里插入图片描述

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改

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

在这里插入图片描述

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式

内存溢出异常
Java会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除

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

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

GC是什么?为什么要GC
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存

回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动

回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法

垃圾回收的优点和原理。并考虑2种回收机制
java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题

由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”

垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存

垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收

程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收

垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收

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

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

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

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

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

一般有两种方法来判断:

引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题
可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的
在Java中,对象什么时候可以被垃圾回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因

JVM中的永久代中会发生垃圾回收吗?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
(译者注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

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

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

标记阶段:标记出可以回收的对象
清除阶段:回收被标记的对象所占用的空间
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的

优点:实现简单,不需要对象进行移动

缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率

标记-清除算法的执行的过程如下图所示

在这里插入图片描述

复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收

优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制

复制算法的执行过程如下图所示

在这里插入图片描述

标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边

优点:解决了标记-清理算法存在的内存碎片问题

缺点:仍需要进行局部对象移动,一定程度上降低了效率

标记-整理算法的执行过程如下图所示
在这里插入图片描述

分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:

在这里插入图片描述

说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用

在这里插入图片描述

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代
详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器

CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低

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

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

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

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

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

内存分配策略
简述java内存分配与回收策率以及Minor GC和Major GC
所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配

对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则:

对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存

这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC

Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快
Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象

前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配

长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代

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

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

类装载方式,有两种 :

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

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

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

什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器

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

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

说一下类装载的执行过程?

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

什么是双亲委派模型?
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象

在这里插入图片描述

类加载器分类:

启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库
其他类加载器:
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类

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

JVM调优
说一下 JVM 调优的工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具

jconsole:用于对 JVM 中的内存、线程和类等进行监控
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等
常用的 JVM 调优的参数都有哪些?
-Xms2g:初始化推大小为 2g
-Xmx2g:堆最大内存为 2g
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合
-XX:+PrintGC:开启打印 gc 信息
-XX:+PrintGCDetails:打印 gc 详细信息

27. Redis的持久化

1、故障发生的时候会怎么样

2、如何应对故障的发生

redis的持久化,RDB,AOF,区别、工作机制,各自的特点是什么,适合什么场景。如何抉择

redis的企业级的持久化方案是什么,是用来跟哪些企业级的场景结合起来使用的?

如果想redis仅作为纯内存的缓存来用,可禁止RDB和AOF所有的持久化机制

Redis持久化的作用:
Redis所有的数据都保存在内存中,对数据的更新将异步地保存在磁盘上

Redis持久化与高可能有很大关系

Redis持久化的意义:
在于数据备份和故障恢复。持久化主要是做灾难恢复,数据恢复

部署一个redis作为cache缓存,或保存一些较为重要的数据

如果没有持久化,redis遇到灾难性故障时,由于redis数据存储在内存中,就会丢失所有的数据

如果通过持久化将数据搞一份儿在磁盘上去,然后定期比如说同步和备份到一些云存储服务上去,就可以保证数据不丢失全部,还是可以恢复一部分数据回来的

对于企业级的redis架构来说,持久化是不可减少的

企业级redis集群架构:海量数据、高并发、高可用

Redis持久化的重要性-缓存雪崩
如redis不可用了,要做的事是让redis尽快变得可用,重启redis,尽快让它对外提供服务

如果没做数据备份,这时redis启动了也不可用,因为数据都没了

有可能大量的请求过来,缓存全部无法命中,在redis里找不到数据,这时缓存雪崩问题,所有请求,没有在redis命中,就会去mysql数据库这种数据源头中去找,一下子mysql承接高并发,然后就挂了

mysql挂掉,数据也就无法恢复到redis里面去。redis的数据从mysql来

如果把redis的持久化做好,备份和恢复方案做到企业级的程度,即使redis故障了,也可通过备份数据,快速恢复,一旦恢复立即对外提供服务

缓存雪崩解决方案:
具体的完整的缓存雪崩的场景,还有企业级的解决方案,到后面讲

Redis持久化和高可用是有关系的。企业级redis架构中去讲解

持久化的方式:
快照(先把数据拷贝出来,做个备份):Mysql Dump 和 Redis RDB
日志(某时某点的日志记录):MySQL Binlog和Hbase HLog和Redis AOF
PDB(快照)和AOF(日志)

28. JWT

JWT也就是JSON Web Token(JWT),是目前最流行的跨域身份验证解决方案

JWT的组成:
一个JWT实际上就是一个字符串,它由三部分组成:头部(Header)、载荷(Payload)与签名(signature)
在这里插入图片描述

三个部分组成,用.号隔开
1, Header

{“typ”:“JWT”,“alg”:“HS256”}

这个json中的typ属性,用来标识整个token字符串是一个JWT字符串;它的alg属性,用来说明这个JWT签发的时候所使用的签名和摘要算法

typ跟alg属性的全称其实是type跟algorithm,分别是类型跟算法的意思。之所以都用三个字母来表示,也是基于JWT最终字串大小的考虑,

同时也是跟JWT这个名称保持一致,这样就都是三个字符了…typ跟alg是JWT中标准中规定的属性名称

2, Payload(负荷)

{“sub”:“123”,“name”:“Tom”,“admin”:true}

payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims

它的一个“属性值对”其实就是一个claim(要求)

每一个claim的都代表特定的含义和作用

3 signature

签名是把header和payload对应的json结构进行base64url编码之后得到的两个串用英文句点号拼接起来,然后根据header里面alg指定的签名算法生成出来的

算法不同,签名结果不同。以alg: HS256为例来说明前面的签名如何来得到

按照前面alg可用值的说明,HS256其实包含的是两种算法:HMAC算法和SHA256算法,前者用于生成摘要,后者用于对摘要进行数字签名。这两个算法也可以用HMACSHA256来统称

JWT的验证过程
它验证的方法其实很简单,只要把header做base64url解码,就能知道JWT用的什么算法做的签名,然后用这个算法,再次用同样的逻辑对header和payload做一次签名

并比较这个签名是否与JWT本身包含的第三个部分的串是否完全相同,只要不同,就可以认为这个JWT是一个被篡改过的串,自然就属于验证失败了

接收方生成签名的时候必须使用跟JWT发送方相同的密钥

注1:在验证一个JWT的时候,签名认证是每个实现库都会自动做的,但是payload的认证是由使用者来决定的。因为JWT里面可能会包含一个自定义claim

所以它不会自动去验证这些claim,以jjwt-0.7.0.jar为例

A 如果签名认证失败会抛出如下的异常

io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

即签名错误,JWT的签名与本地计算机的签名不匹配

B JWT过期异常

io.jsonwebtoken.ExpiredJwtException: JWT expired at 2017-06-13T11:55:56Z. Current time: 2017-06-13T11:55:57Z, a difference of 1608 milliseconds. Allowed

注2:认证失败,返回401 Unauthorized响应

注3:认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找

39. 谈谈你对ArrayList,HashMap的看法

相同点:
1.都是线程不安全,不同步
2.都可以储存null值
3.获取元素个数方法一样,都是用size()方法获取
4.都是可扩容的容器

区别:
1.实现的接口
ArrayList实现了List接口(Collection(接口)->List(接口)->ArrayList(类)),底层使用的是数组;HashMap实现了Map接口,底层使用的是数组+链表结构
2.添加元素的方法
ArrayList用add(Object object)方法添加元素,而HashMap用put(Object key,Object value)添加元素
3.默认的大小和扩容
ArrayList扩容后的长度为扩容前的1.5倍,不存在负载因子这一概念;HashMap扩容是原容量的2倍

30. 你对Sring的理解

1、spring为什么出现

spring的出现是为了解耦,我们在学习java初期,相信很多小伙伴都使用的是JSP+Servlet+MySQL+JDBC技术,在操作dao层的时候,每次都需要实例化一次,这样是不是就很繁琐;有的人可能使用单例模式来解决这个问题,但业务代码与单例模式的模板代码放在一个类里而且也会出现大量重复的单例模式的模板代码,耦合性较高,要知道java语言可是高内聚,低耦合的;所以伟大的Spring就出现了,也就是类似于数据库连接池的东西。下面着重介绍这个框架

2、理解Sping

传统Java SE程序设计,我们直接在对象内部通过new进行创建对象或者getInstance等直接或者间接调用构造方法创建一个对象;而在Spring开发模式中,Spring容器使用了工厂模式为我们创建了所需要的对象(这个过程就是DI通过setter方法在配置中注入对象),我们使用时不需要自己去创建,直接调用Spring为我们提供的对象即可,这就是控制反转的思想。实例化一个java对象有三种方式:使用类构造器,使用静态工厂方法,使用实例工厂方法,当使用spring时我们就不需要关心通过何种方式实例化一个对象,spring通过控制反转机制自动为我们实例化一个对象

3、面向切面AOP

在面向对象编程(OOP)思想中,我们将事物纵向抽象成一个个的对象。而在面向切面编程中,我们将一个个对象某些类似的方面横向抽象成一个切面,对这个切面进行一些如权限验证,事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想

4、Spring支持的几种bean的作用域

Spring框架支持以下五种bean的作用域: singleton : bean在每个Spring ioc 容器中只有一个实例。 prototype:一个bean的定义可以有多个实例。 request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。 session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效

5、使用Spring框架的好处是什么

轻量:Spring 是轻量的,基本的版本大约2MB。 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。 容器:Spring 包含并管理应用中对象的生命周期和配置。 MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。 事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。 异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常

6、spring配置bean实例化有哪些方式

1)使用类构造器实例化(默认无参数)

<bean id="bean1" class="cn.itcast.spring.b_instance.Bean1"></bean>

2)使用静态工厂方法实例化(简单工厂模式)
//下面这段配置的含义:调用Bean2Factory的getBean2方法得到bean2

<bean id="bean2" class="cn.itcast.spring.b_instance.Bean2Factory" factory-method="getBean2"></bean>

3)使用实例工厂方法实例化(工厂方法模式)

//先创建工厂实例bean3Facory,再通过工厂实例创建目标bean实例

<bean id="bean3Factory" class="cn.itcast.spring.b_instance.Bean3Factory"></bean> <bean id="bean3" factory-bean="bean3Factory" factory-method="getBean3"></bean>

31. Spring Cloud Alibaba有哪几个组件

8个:

  1. Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
  2. Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
  3. RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务
  4. Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架
  5. Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案
  6. Alibaba Cloud OSS:阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据
  7. Alibaba Cloud SchedulerX:阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务
  8. Alibaba Cloud SMS:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道

32. string的底层是什么

String底层是一个不可变字符串,使用连接符的时候,实际上是经过了StringBuilder的优化处理的。并不是在原来的String对象中做追加

33. .java中什么是容器?

什么是容器?
容器可以管理对象的生命周期、对象与对象之间的依赖关系
直白点说容器就是一段Java程序,能够帮助你管理对象间的关系,而不需要你自行编写程序处理

维基百科定义:
在计算机科学中,容器是指实例为其他类的对象的集合的类、数据结构、[1][2]或者抽象数据类型。换言之,它们以一种遵循特定访问规则的系统的方法来存储对象。容器的大小取决于其包含的对象(或元素)的数目
潜在的不同容器类型的实现可能在空间和时间复杂度上有所差别,这使得在给定应用场景中选择合适的某种实现具有灵活性

Java内部的容器类
Java内部的容器类主要分为两类:Collection(集合)与Map(图)

Collection

Set
HashSet
基于哈希表实现,底层使用HashMap来保存所有元素
不能保证迭代顺序
允许使用null元素

LinkedHashSet
LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承于HashSet
内部使用双向链表维护插入顺序

TreeSet
基于(TreeMap)红黑树实现
TreeSet非同步,线程不安全
TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序

List
ArrayList
实现 List 接口、底层使用数组保存所有元素
相当于动态数组,支持动态扩容
不同步

vector
Vector 可以实现可增长的对象数组。
Vector 实现 List 接口,继承 AbstractList 类,同时还实现RandmoAccess 接口,Cloneable 接口
Vector 是线程安全的

LinkedList
LinkedList 是基于链表实现的(通过名字也能区分开来)
所以它的插入和删除操作比 ArrayList 更加高效。但也是由于其为基于链表的,所以随机访问的效率要比 ArrayList 差

Queue
LinkedList
可以用于实现双向队列

PriorityQueue
通过二叉小顶堆实现,可以用一棵完全二叉树表示
可以用于实现优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C++的优先队列每次取最大元素)

Map(用于映射(键值对)问题处理)

HashMap
HashMap根据键的HashCode来实现,访问速度较快,遍历顺序并不确定
HashMap最多只允许一条记录的键为null,允许多条记录的值为null
HashMap线程不安全,也就是说任意时刻可以有多个线程同时写HashMap,所以可能会导致数据的不一致
如何确保线程安全?可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap

HashTable
HashTable是遗留类,多数功能与HashMap类似,继承自Dictionary类
HashTable是线程安全的。也就是说任意时刻只有一个线程能够写HashTable
HashTable的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁

LinkedHashMap
基于哈希表和链表实现,借助双向链表确保迭代顺序是插入的顺序

TreeMap
基于红黑树实现
默认按照键值得升序进行排序
在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator
否则会在运行时抛出java.lang.ClassCastException类型的异常

34. HashMap底层,如何插入数据的

Hash表 = 数组 + 线性链表 + 红黑树
HashMap 插入数据JDK1.7之前是从头部插入,JDK1.8及之后是从尾部插入的,在出现Hash相同时JDK1.7是单链表存储的,1.8及之后在同一个Hash处超过8个会使用红黑树存储,所以会从尾部插入数据

35. 微服务有哪些组件

Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里

Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台

Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求

Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题

Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

36. jkd8新特性

Java8 (又称 JKD1.8) 是 Java 语言开发的一个主要版本
Oracle公司于2014年3月18日发布Java8

  • 支持Lambda表达式
  • 函数式接口
  • 新的Stream API
  • 新的日期 API
  • 其他特性
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值