Java 校招面试大全

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011669700/article/details/84887892

面试准备


HBase

HBase 基础结构

HBase基础架构.png-183.1kB

1. HMaster

  • HMaster 是 HBase 主/从集群架构的的中央节点;
  • HMaster 将 Region 分配给 RegionServer,协调 RegionServer 的负载并维护集群状态;
  • 维护表和 Region 的元数据,不参与数据的输入/输出过程

2. RegionServer

  • 维护 HMaster 分配的 Region,并且处理对应 Region 的 I/O 请求
  • 负责切分正在运行过程中变的过大的 Region

3. Zookeeper

  • Zookeeper 是集群的协调器
  • HMaster 在启动的时候把系统表加载在 Zookeeper 上
  • 维护 RegionServer 的状态,提供 HBase RegionServer 的状态信息

HBase 写流程

HBase写流程.png-156.4kB

  1. Client 先访问 Zookeeper,获取表相关的信息,并得到对应的 RegionServer 地址,根据要插入的 Rowkey 获取指定的 RegionServer 的信息(如果为批量提交,会把 Rowkey 根据 HRegionLocation 进行分组);
  2. Client 对 RegionServer 发起写请求(RegionServer 会进入检查状态,比如当前的 Region 是否处于只读状态,MemoStore 的大小是否超过了 BlockingMemoStoreSize 等等),RegionServer 接受数据写入内存(会依次写入 MemoStore 和 HLog);
  3. 当 MemStore 大小达到一定的值后,flush 到 StoreFile 并存储到 HDFS 中。如果 StoreFile 达到一定的阈值,会触发 Split 机制,将 Region 一分为二,然后 HMaster 给两个 Region 分配相应的 RegionServer 进行管理,从而分担压力。

HBase写

  • RegionServer:RegionServer上有一个或者多个Region。我们读写的数据就存储在Region上。如果你的HBase是基于HDFS的,那么Region所有数据存取操作都是调用了HDFS的客户端接口来实现的。
  • Region:表的一部分数据。HBase是一个会自动分片的数据库。一个Region就相当于关系型数据库中分区表的一个分区,或者MongoDB的一个分片。每一个Region都有起始rowkey和结束rowkey,代表了它所存储的row范围。
  • HDFS:Hadoop的一部分。HBase并不直接跟服务器的硬盘交互,而是跟HDFS交互,所以HDFS是真正承载数据的载体。
  • HLog:每一个 RegionServer都会有一个HLog示例,并且将操作写在里面。 HLog是WAL(Write-Ahead Log,预写日志)的一个实现实例。WAL是一个保险机制,数据在写到Memstore之前,先被写到WAL了。这样当故障恢复的时候可以从WAL中恢复数据。

HBase 读流程

HBase读流程.png-167.9kB

  1. Client 先访问 Zookeeper,得到对应的 RegionServer 地址;
  2. Client 对 RegionServer 发起读请求;
  3. 当 RegionServer 收到 Client 的读请求的时候,先扫描自己的 Memstore,再扫描 BlockCache(加速读内容缓冲区),如果还没有找到相应的数据,则从 StoreFile 中读取数据,然后将数据返回给 Client。

为什么 Client 只需要访问 Zookeeper?

HMaster 启动的时候会把 Meta 的信息表记录在 Zookeeper 中。这个元数据信息表存储了 HBase 中所有的表,以及 Region 的详细信息。如 Region 开始和结束的 Key,所在的 RegionServer 的地址。HBASE 的 Meta 表相当于一个目录。通过访问 Meta 表可以快速的定位到数据的实际位置,所以读写操作只需要与 Zookeeper 和对应的 RegionServer 进行交互,而 HMaster 只需要负责维护 table 和 Region 的元数据信息,协调各个 RegionServer,也因此减少了 HMaster 的负载。

参考资料

  1. HBase工作原理学习

面向对象设计原则

S.O.L.I.D

简写 全拼 中文翻译
SRP The Single Responsibility Principle 单一责任原则
OCP The Open Closed Principle 开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
ISP The Interface Segregation Principle 接口分离原则
DIP The Dependency Inversion Principle 依赖倒置原则

1. 单一责任原则

修改一个类的原因应该只有一个。

换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。

如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。

2. 开放封闭原则

类应该对扩展开放,对修改关闭。

扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。

符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。

3. 里氏替换原则

子类对象必须能够替换掉所有父类对象。

继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。

如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。

4. 接口分离原则

不应该强迫客户依赖于它们不用的方法。

因此使用多个专门的接口比使用单一的总接口要好。

5. 依赖倒置原则

高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。

高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。

依赖于抽象意味着:

  • 任何变量都不应该持有一个指向具体类的指针或者引用;
  • 任何类都不应该从具体类派生;
  • 任何方法都不应该覆写它的任何基类中的已经实现的方法。

Spring MVC 请求流程

Spring MVC请求流程.png-86.7kB

  1. 发起请求到前端控制器 (DispatcherServlet);
  2. 前端控制器请求 HandlerMapping 查找 Handler (可以根据 xml 配置、注解进行查找);
  3. 处理器映射器 HandlerMapping 向前端控制器返回 Handler,HandlerMapping 会把请求映射为 HandlerExecutionChain 对象(包含一个 Handler 处理器(页面控制器)对象,多个 HandlerInterceptor 拦截器对象),通过这种策略模式,很容易添加新的映射策略;
  4. 前端控制器调用处理器适配器去执行 Handler;
  5. 处理器适配器 HandlerAdapter 将会根据适配的结果去执行 Handler;
  6. Handler 执行完成给适配器返回 ModelAndView;
  7. 处理器适配器向前端控制器返回 ModelAndView (ModelAndView 是 Spring MVC 框架的一个底层对象,包括 Model 和 view);
  8. 前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图 (jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可;
  9. 视图解析器向前端控制器返回 View;
  10. 前端控制器进行视图渲染 (视图渲染将模型数据 (在 ModelAndView 对象中) 填充到 request 域);
  11. 前端控制器向用户响应结果。

参考资料

  1. springMVC请求流程详解

Spring IOC

IOC 是 Inversion of Control 的缩写,多数书籍翻译成“控制反转”。

IOC 理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。

软件系统在没有引入 IOC 容器之前,如图 1 所示,对象 A 依赖于对象 B,那么对象 A 在初始化或者运行到某一点的时候,自己必须主动去创建对象 B 或者使用已经创建的对象 B。无论是创建还是使用对象 B,控制权都在自己手上。

此处输入图片的描述

软件系统在引入 IOC 容器之后,这种情形就完全改变了,如图所示,由于 IOC 容器的加入,对象 A 与对象 B 之间失去了直接联系,所以,当对象 A 运行到需要对象 B 的时候,IOC 容器会主动创建一个对象 B 注入到对象 A 需要的地方。
此处输入图片的描述

通过前后的对比,我们不难看出来:对象 A 获得依赖对象 B 的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

DI 依赖注入

2004 年,Martin Fowler 探讨了同一个问题,既然 IOC 是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现 IOC 的方法:注入。所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。

IOC 和 DI 的区别

理解以上概念需要搞清以下问题:

  1. 参与者都有谁?
      一般有三个参与者。 1)是某个对象;2)是 IOC/DI 的容器;3)某个对象的外部资源。
      其中 1)某个对象指的是任意的,普通的 Java 对象;2)IOC/DI 容器指的是指用于实现 IOC/DI 功能的框架程序;3)对象的完毕资源指的是对象所需要的,但是需要从外部获取的统称为资源;比如一个对象的属性为另外一个对象,或者是对象需要的是一个文件资源等等。
  2. 依赖: 谁依赖于谁?为什么需要依赖?
      对象依赖于 IOC/DI 的容器。 因为对象需要 IOC 来提供对象所需要的外部资源。
  3. 注入:谁注入谁?到底注入什么?
      IOC/DI 容器注入某个对象。 注入某个对象所需要的外部资源。
  4. 控制反转:谁控制谁?控制了什么?为什么叫反转(有反转就应该有正转)?
      是 IOC 容器控制对象,主要是控制了对象实例的创建。
      反转是针对正向而言,正向是针对常规下的应用程序而言的。正规应用程序下,如果要在 A 里面使用 C,则会直接创建 C 的对象。,也就是说,是在 A 类中主动去获取所需要的外部资源 C,这种情况被称为正向的。
      那么什么是反向呢?就是 A 类不再主动去获取 C,而是被动等待,等待 IoC/DI 的容器获取一个 C 的实例,然后反向的注入到 A 类中。
  5. 依赖注入和控制反转是同一概念么?
      依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。
      依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;
      而控制反转是从容器的角度在描述。描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

Spring AOP

AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充。它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。

所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

AOP 常见的使用场景

  1. Authentication 权限
  2. Caching 缓存
  3. Context passing 内容传递
  4. Error handling 错误处理
  5. Lazy loading 懒加载
  6. Debugging  调试
  7. logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  8. Performance optimization 性能优化
  9. Persistence  持久化
  10. Resource pooling 资源池
  11. Synchronization 同步
  12. Transactions 事务

实现原理

Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP 中的动态代理方法主要有两种:

  1. JDK 动态代理:JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。
  2. CGLIB 动态代理:如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。

JDK 与 Cglib 代理对比

  1. JDK 只能针对有接口的类的接口方法进行动态代理。
  2. Cglib 基于继承实现代理,无法对 static 或者 final 类进行代理。
  3. Cglib 基于继承实现代理,也无法对 private 或者 static 方法进行代理。

Spring AOP 对两种方法的选择

  1. 如果目标对象实现了接口,则默认采用 JDK 动态代理。

  2. 如果目标对象没有实现接口,则默认采用 Cglib 进行动态代理。

  3. 如果目标对象实现了接口,则强制 Cglib 代理,则采用 Cglib 进行代理。

    // 采用注解开启强制代理
    @EnableAspectJAutoProxy(proxyTragetClass = true)
    

参考资料

  1. Spring AOP 的实现原理

Spring Bean 的初始化流程

SpringBean 初始化.png-379.2kB

  1. Spring 对 bean 进行实例化;
  2. Spring 将值和 bean 的引用注入到 bean 对应的属性中;
  3. 如果 bean 实现了 BeanNameAware 接口,Spring 将 bean 的 ID 传递给 setBeanName() 方法;
  4. 如果 bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanFactory() 方法,将 BeanFactory 容器实例传入;
  5. 如果 bean 实现了 ApplicationContextAware 接口,Spring 将调用 setApplicationContext() 方法,将 bean 所在的应用上下文的引用传入进来;
  6. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 postProcessBeforeInitialization() 方法;
  7. 如果 bean 实现了 InitializingBean 接口,Spring 将调用它们的 after-PropertiesSet() 方法。类似地,如果 bean 使用 init-method 声明了初始化方法,该方法也会被调用;
  8. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 post-ProcessAfterInitialization() 方法;
  9. 此时,bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
  10. 如果 bean 实现了 DisposableBean 接口,Spring 将调用它的 destroy() 接口方法。同样,如果 bean 使用 destroy-method 声明了销毁方法,该方法也会被调用。
/**
 * bean的生命周期:
 * 		bean创建---初始化----销毁的过程
 * 容器管理bean的生命周期;
 * 我们可以自定义初始化和销毁方法;容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁方法
 * 
 * 构造(对象创建)
 * 		单实例:在容器启动的时候创建对象
 * 		多实例:在每次获取的时候创建对象
 * 
 * BeanPostProcessor.postProcessBeforeInitialization
 * 初始化:
 * 		对象创建完成,并赋值好,调用初始化方法。。。
 * BeanPostProcessor.postProcessAfterInitialization
 * 销毁:
 * 		单实例:容器关闭的时候
 * 		多实例:容器不会管理这个bean;容器不会调用销毁方法;
 * 
 * 
 * 遍历得到容器中所有的BeanPostProcessor;挨个执行beforeInitialization,
 * 一但返回null,跳出for循环,不会执行后面的BeanPostProcessor.postProcessorsBeforeInitialization
 * 
 * BeanPostProcessor原理
 * populateBean(beanName, mbd, instanceWrapper);给bean进行属性赋值
 * initializeBean
 * {
 * applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
 * invokeInitMethods(beanName, wrappedBean, mbd);执行自定义初始化
 * applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
 *}
 * 
 * 
 * 
 * 1)、指定初始化和销毁方法;
 * 		通过@Bean指定init-method和destroy-method;
 * 2)、通过让Bean实现InitializingBean(定义初始化逻辑),
 * 				DisposableBean(定义销毁逻辑);
 * 3)、可以使用JSR250;
 * 		@PostConstruct:在bean创建完成并且属性赋值完成;来执行初始化方法
 * 		@PreDestroy:在容器销毁bean之前通知我们进行清理工作
 * 4)、BeanPostProcessor【interface】:bean的后置处理器;
 * 		在bean初始化前后进行一些处理工作;
 * 		postProcessBeforeInitialization:在初始化之前工作
 * 		postProcessAfterInitialization:在初始化之后工作
 * 
 * Spring底层对 BeanPostProcessor 的使用;
 * 		bean赋值,注入其他组件,@Autowired,生命周期注解功能,@Async,xxx BeanPostProcessor;
 * 
 * @author lfy
 *
 */

Spring 组件注册

Spring 组件注册主要有以下三种方式:

  1. 包扫描 + 组件标注注解(@Controller/@Service/@Repository/@Component)
    缺点: 只能对自定义的类进行注解标注,无法对三方类库中的属性进行组件注册
  2. @Bean[主要用于三方类库中的组件注册 ]
  3. @Import[可以快速给容器中导入一个组件,其 Bean 的 id 默认为组件的全类名]

Spring 自动装配

@Autowired

  1. 默认优先按照类型去容器中找对应的组件:applicationContext.getBean(BookDao.class);找到就赋值
  2. 如果找到多个相同类型的组件,再将属性的名称作为组件的 id 去容器中查找,如applicationContext.getBean("bookDao")
  3. @Qualifier(“bookDao”):使用@Qualifier 指定需要装配的组件的 id,而不是使用属性名
  4. 自动装配默认一定要将属性赋值好,没有就会报错;可以使用@Autowired(required=false);
  5. @Primary:让 Spring 进行自动装配的时候,默认使用首选的 bean;(也可以继续使用@Qualifier 指定需要装配的 bean 的名字)
BookService{
	@Autowired
	BookDao  bookDao;
}

@Autowired 构造器,参数,方法,属性;都是从容器中获取参数组件的值。

  1. 构造器:如果组件只有一个有参构造方法,则该有参构造器的 @Autowired 可以省略,有参构造器参数的值自动从容器中获取。
  2. 方法:如果只是 @Bean 注解 + 方法参数,参数的值也会自动从容器中获取。默认不写 @Autowired 效果是一样的;都能自动装配。
  3. 属性:

@Resource [JSR250]

可以和@Autowired 一样实现自动装配功能;默认是按照组件名称进行装配的;
没有支持@Primary 功能,没有支持 @Autowired(reqiured=false)

@Inject [JSR300]

需要导入 javax.inject 的依赖,和 @Autowired 的功能一样。

<!-- https://mvnrepository.com/artifact/javax.inject/javax.inject -->
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

没有支持 @Autowired(reqiured=false) 的功能;

自动注入流程

AutowiredAnnotationBeanPostProcessor:解析完成自动装配功能;

容器组件注入

自定义组件想要使用Spring容器底层的一些组件(ApplicationContext,BeanFactory,xxx);
自定义组件实现xxxAware;在创建对象的时候,会调用接口规定的方法注入相关组件;均实现 `` 接口;
把Spring底层一些组件注入到自定义的Bean中;
xxAware 均有对应的 xxxProcessor 进行值的注入;
ApplicationContextAware==》ApplicationContextAwareProcessor;

Java 读取一个文件, 有哪些方法, 考虑性能, 用哪一个类

文件读写主要有以下集中常用的方法:

  1. 字节读写(InputStream/OutputStream)
  2. 字符读取(FileReader/FileWriter)
  3. 行读取(BufferedReader/BufferedWriter)

通过测试 ,可以发现,就写数据而言,BufferedOutputStream耗时是最短的,而性能FileWriter最差;读数据方面,BufferedInputStream性能最好,而FileReader性能最差劲。

Java OOM

为什么为发生 OOM

原因一般出现为以下两点:

  1. 分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的 VM 参数指定)太少。
  2. 应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

其对应的两个术语为:

  1. 内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
  2. 内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。

解决方法

  1. 修改内存引用,常用的有软引用、强化引用、弱引用
  2. 在内存中加载图片时直接在内存中作处理,如边界压缩
  3. 动态回收内存
  4. 自定义堆内存大小

参考资料

  1. https://blog.csdn.net/osle123/article/details/52756433
  2. Java 中常见 OOM 的场景及解决方法
  3. 什么是 java OOM?如何分析及解决 oom 问题?

HashMap 是怎么扩容的,为什么是 2 的幂

HashMap 中,length 为 2 的幂次方,h &(length-1)等同于求模运算 h%length

HashMap 采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap 定位哈希桶索引位置时,也加入了高位参与运算的过程。

操作流程图

此处输入图片的描述

参考资料

  1. HashMap 的扩容是怎样扩容的,为什么是 2 的 N 次幂的大小?

Java 垃圾回收

垃圾收集主要是针对堆和方法区进行。

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。

判断一个对象是否可回收

1.可达性分析算法

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

可达性分析.png-19kB

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

  1. 虚拟机栈中引用的对象
  2. 本地方法栈中引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中的常量引用的对象

2.方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

Minor GC 和 Full GC

  • Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

1. 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

2. 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3. 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4. 动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5. 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

垃圾回收算法

CMS 收集器

CMS.jpg-39kB

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

分为以下四个流程:

  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  • 并发清除:不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

具有以下缺点:

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

7. G1 收集器

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

4cf711a8-7ab2-4152-b85c-d5c226733807.png-28kB

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png-29kB

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

f99ee771-c56f-47fb-9148-c0036695b5fe.jpg-37kB

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

具备如下特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

Java 为什么获取不到函数参数名称

在 Java 1.7 以前,编译生成的字节码中并不会包含方法的参数信息。因此无法获取到方法的参数名称信息。

但是在 Java 1.8 以后,开始在 class 中保存参数名,并且增加了对应的类Parameter。使用的示例代码如下:

 public static List<String> getParameterNameJava8(Class clazz, String methodName){
    List<String> paramterList = new ArrayList<>();
    Method[] methods = clazz.getDeclaredMethods();
    for (Method method : methods) {
        if (methodName.equals(method.getName())) {
            Parameter[] params = method.getParameters();
            for(Parameter parameter : params){
                paramterList.add(parameter.getName());
            }

        }
    }

    return paramterList;
}

如果编译等级低于 1.8,则得到的参数名依旧为无效的参数名,例如 arg0、arg1……

同时,想要保留参数名也需要通过修改编译选项 javac -parameters 进行打开,默认是关闭的。

idea 设置保留参数名:

在 preferences-》Java Compiler-> 设置模块字节码版本 1.8,Javac Options 中的 Additional command line parameters: -parameters

参考资料

  1. Java——通过反射获取函数参数名称

JVM 常用命令

  1. jinfo:可以输出并修改运行时的 java 进程的 opts。
  2. jps:与 unix 上的 ps 类似,用来显示本地的 java 进程,可以查看本地运行着几个 java 程序,并显示他们的进程号。
  3. jstat:一个极强的监视 VM 内存工具。可以用来监视 VM 内存内的各种堆和非堆的大小及其内存使用量。
  4. jmap:打印出某个 java 进程(使用 pid)内存内的所有 ’ 对象 ’ 的情况(如:产生那些对象,及其数量)。
  5. jconsole:一个 java GUI 监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器 VM。

详细:在使用这些工具前,先用 JPS 命令获取当前的每个 JVM 进程号,然后选择要查看的 JVM。

参考资料

  1. 查看 jvm 常用命令

类加载 - 双亲委派模型

Java 运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找。

  1. 如果是 class 文件的根目录,则直接查看是否有对应的子目录及文件;
  2. 如果是 jar 文件,则首先在内存中解压文件,然后再查看是否有对应的类。

负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是 Class 对象。类加载器不是只有一个,一般程序运行时,都会有三个(适用于 Java 9 之前)类加载器。

  1. 启动类加载器(Bootstrap ClassLoader):这个加载器是 Java 虚拟机实现的一部分,不是 Java 语言实现的,一般是 C++实现的,它负责加载 Java 的基础类,主要是 <JAVA_HOME>/lib/rt.jar,我们日常用的 Java 类库比如 String、ArrayList 等都位于该包内。
  2. 扩展类加载器(Extension ClassLoader):这个加载器的实现类是 sun.misc.Laun-cher$ExtClassLoader,它负责加载 Java 的一些扩展类,一般是 <JAVA_HOME>/lib/ext 目录中的 jar 包。
  3. 应用程序类加载器(Application ClassLoader):这个加载器的实现类是 sun.misc.Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方类库,即所有在类路径中指定的类。

4236553-c65e628b05bddb2c.png-180.7kB

这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader 的父亲是 Extension ClassLoader,Extension 的父亲是 Bootstrap ClassLoader。注意不是父子继承关系,而是父子委派关系,即子 ClassLoader 有一个变量 parent 指向父 ClassLoader,在子 ClassLoader 加载类时,一般会首先通过父 ClassLoader 加载,具体来说,在加载一个类时,基本过程是:

  1. 判断是否已经加载过了,加载过了,直接返回 Class 对象,一个类只会被一个 ClassLoader 加载一次。
  2. 如果没有被加载,先让父 ClassLoader 去加载(父类加载器为空后就使用启动类加载器加载),如果加载成功,返回得到的 Class 对象。
  3. 在父 ClassLoader 没有加载成功的前提下,自己尝试加载类。

这个过程一般被称为“双亲委派”模型,即优先让父 ClassLoader 去加载。

  1. 这样,每一个类都只会被加载一次,可以避免Java类库被覆盖的问题。比如,用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。
  2. 每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
  3. 有效避免了某些恶意类的加载(比如自定义了java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)

打破双亲委派

继承 ClassLoader 重写 loadClass 方法使得自己先加载之后尝试父类进行数据的加载。

参考资料

  1. 深入理解 Tomcat(四)Tomcat 类加载器之为何违背双亲委派模型
  2. 破坏双亲委派模型和自定义自己的类加载器
  3. 【JVM】浅谈双亲委派和破坏双亲委派

线程池

线程池的构造函数如下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
     long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
     long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory, RejectedExecutionHandler handler)

线程池参数

  1. corePoolSize:核心线程个数,可能处于等待状态。
  2. maximumPoolSize:线程池允许的最大线程个数。
  3. keepAliveTimeunit :空闲线程存活时间。超出 corePoolSize 部分线程如果等待超过指定时间,将会被回收。如果 keepAliveTime 值为 0,则表示所有线程都不会超时终止。

其中对于核心线程数,还有以下几点需要注意:

  • 核心线程会一直存活,即使没有任务需要执行
  • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
  • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

任务进队流程

  1. 如果当前线程个数小于 corePoolSize,则会创建一个新的线程来执行该任务。
  2. 如果当前线程个数不小于 corePoolSize,则尝试加入阻塞队列。
  3. 如果因为队列满了或者其他原因不能入队,则检查线程个数是否达到 maximumPoolSize,如果没有则继续创建线程直到 maximumPoolSize 个。
  4. 如果超过了 maximumPoolSize,则触发拒绝策略。
step 1: <ExecutorService>
Future<?> submit(Runnable task); 
 
step 2:<AbstractExecutorService>
    public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
 
step 3:<Executor>
void execute(Runnable command);
 
step 4:<ThreadPoolExecutor>
 public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) { //提交我们的额任务到workQueue
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false)) //使用maximumPoolSize作为边界
        reject(command); //还不行?拒绝提交的任务
}
 
step 5:<ThreadPoolExecutor>
private boolean addWorker(Runnable firstTask, boolean core)
 
 
step 6:<ThreadPoolExecutor>
w = new Worker(firstTask); //包装任务
final Thread t = w.thread; //获取线程(包含任务)
workers.add(w);   // 任务被放到works中
t.start(); //执行任务

线程池阻塞队列

ThreadPoolExecutor 要求的队列类型是阻塞队列 BlockingQueue。

  1. LinkedBlockingQueue:基于链表的阻塞队列,可以指定最大长度,但默认是无界的。
  2. ArrayBlockingQueue:基于数组的有界阻塞队列。
  3. PriorityBlockingQueue:基于堆的无界阻塞优先级队列。
  4. SynchronousQueue:没有实际存储空间的同步阻塞队列。对于 SynchronousQueue,我们知道,它没有实际存储元素的空间,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到 maximumPoolSize

任务拒绝策略

如果队列有界,且 maximumPoolSize 有限,则当队列排满,线程个数也达到了 maximumPoolSize。此时,新任务会触发线程池的任务拒绝策略。

默认情况下,提交任务的方法(如 execute/submit/invokeAll 等)会抛出异常,类型为 RejectedExecutionException。其中拒绝策略是可以自定义的,需要实现 RejectedExecutionHandler 接口。ThreadPoolExecutor 实现了四种方式。

  1. ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常。
  2. ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行。
  3. ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉(丢弃workQueue的头部任务),然后自己排队。
  4. ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行。

拒绝策略只有在队列有界,且 maximumPoolSize 有限的情况下才会触发。

  1. 如果队列无界,服务不了的任务总是会排队,但这不一定是期望的结果,因为请求处理队列可能会消耗非常大的内存,甚至引发内存不够的异常。
  2. 如果队列有界但 maximumPoolSize 无限,可能会创建过多的线程,占满 CPU 和内存,使得任何任务都难以完成。

所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。

线程池大小设计

IO密集型

线=(+)×CPU 线程个数 = \frac{(运行时间 + 等待时间)}{运行时间} \times {CPU核数}

例如运行时间如果为 1s,等待时间为 5s,cpu 核数为 4,那么应该设置的线程池大小为 24

关于公式的理解,因为就是一个将等待时间用来运行线程的思想,比如刚才那个例子:运行时间 1s,等待时间 5s,那么等待的这段时间就可以多运行 5 / 1 个线程

CPU 密集型

线=CPU+1 线程个数 = CPU 核数 + 1

参考资料

  1. 如何合理地估算线程池大小?

工厂类Executors

类 Executors 提供了一些静态工厂方法,可以方便地创建一些预配置的线程池,主要方法有:

public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()

newSingleThreadExecutor

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

只使用一个线程,使用无界队列 LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L,
                TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用固定数目的 n 个线程,使用无界队列 LinkedBlockingQueue,线程创建后不会超时终止。和 newSingleThreadExecutor 一样,由于是无界队列,如果排队任务过多,可能会消耗过多的内存。

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L,
               TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

它的 corePoolSize 为 0,maximumPoolSize 为 Integer.MAX_VALUE,keepAliveTime 是 60 秒,队列为 SynchronousQueue。它的含义是:当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果 60 秒内没有新任务,就终止。

总结

实际中,应该使用 newFixedThreadPool 还是 newCachedThreadPool 呢?

在系统负载很高的情况下,newFixedThreadPool 可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而 newCachedThreadPool 会为每个任务创建一个线程,导致创建过多的线程竞争 CPU 和内存资源,使得任何实际任务都难以完成,这时,newFixedThreadPool 更为适用。

不过,如果系统负载不太高,单个任务的执行时间也比较短,newCachedThreadPool 的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。

在系统负载可能极高的情况下,两者都不是好的选择,newFixedThreadPool 的问题是队列过长,而 newCachedThreadPool 的问题是线程过多,这时,应根据具体情况自定义 ThreadPoolExecutor,传递合适的参数。

线程死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁防止

  1. 破除互斥等待条件 -> 一般无法破除

  2. 破除 hold and wait -> 一次性获取所有资源

    编程语言方面可能不支持,但是我们可以在尝试获取第一个锁之后再去尝试获取第二个锁,增加超时等待。如果超时则把两个锁全部释放。再去重新获取。
    **缺点:**因为两个锁不能同时获取到就要舍弃,进行再次尝试。其中尝试的次数,每次尝试之间的等待时间。都会给用户带来不好的体验

  3. 破除循环等待 -> 按顺序获取资源

    比如业务对象具有本身的 ID,根据一定的规则,比如按照 ID 的大小进行锁的获取。这样大家都是先获取 ID 值较小的对象的锁,这样就不会产生循环等待

  4. 破除无法剥夺的等待 -> 加入超时

LRU - Java实现

LRU 是 Least Recently Used 的缩写,翻译过来就是“最近最少使用”,LRU 缓存就是使用这种原理实现,简单的说就是缓存一定量的数据,当超过设定的阈值时就把一些过期的数据删除掉

比如我们缓存 10000 条数据,当数据小于 10000 时可以随意添加,当超过 10000 时就需要把新的数据添加进来,同时要把过期数据删除,以确保我们最大缓存 10000 条,那怎么确定删除哪条过期数据呢,采用 LRU 算法实现的话就是将最老的数据删掉。

Java 里面实现 LRU 缓存通常有两种选择,一种是使用 LinkedHashMap,一种是自己设计数据结构,使用链表+HashMap。

LinkedHashMap

LinkedHashMap 自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回 false,即不删除数据,我们使用 LinkedHashMap 实现 LRU 缓存的方法就是对 LinkedHashMap 实现简单的扩展,扩展方式有两种,一种是 inheritance(继承),一种是 delegation(委派)。

//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

//LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
//我们要做的就是重写这个方法,当满足一定条件时删除老数据
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
}

LRU 缓存 LinkedHashMap(inheritance) 实现

采用 inheritance 方式实现比较简单,而且实现了 Map 接口,在多线程环境使用时可以使用 Collections.synchronizedMap() 方法实现线程安全操作

package lru;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Created by Joe on 2018/8/21.
 */
public class LRUCacheInheritance<K, V> extends LinkedHashMap {
    private final int MAX_CACHE_SIZE;

    public LRUCacheInheritance(int cacheSize) {
        // 根据cacheSize和加载因子计算hashmap的capactiy,+1确保当达到cacheSize上限时不会触发hashmap的扩容
        super((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true);
        MAX_CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_CACHE_SIZE;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<K, V>> set = entrySet();
        for (Map.Entry<K,V> entry : set) {
            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
        }
        return sb.toString();
    }
}

在实际使用中还可以有如下的实现:

final int cacheSize = 100;
Map<String, String> map = new LinkedHashMap<String, String>((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
    return size() > cacheSize;
    }
};

LRU 缓存 LinkedHashMap(delegation) 实现

delegation 方式实现更加优雅一些,但是由于没有实现 Map 接口,所以线程同步就需要自己搞定了

package lru;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Created by Joe on 2018/8/21.
 */
public class LRUCacheDelegation<K, V> {
    private final int MAX_CACHE_SIZE;
    private final float DEFAULT_LOAD_FACTOR = 0.75f;
    private LinkedHashMap<K, V> map;

    public LRUCacheDelegation(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;

        //根据cacheSize和加载因子计算hashmap的capactiy,+1确保当达到cacheSize上限时不会触发hashmap的扩容
        int capacity = (int) Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTOR) + 1;
        map = new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTOR, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > MAX_CACHE_SIZE;
            }
        };
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized void remove(K key) {
        map.remove(key);
    }

    public synchronized Set<Map.Entry<K, V>> entrySet() {
        return map.entrySet();
    }

    public synchronized int size() {
        return map.size();
    }

    public synchronized void clear() {
        map.clear();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<K, V> entry : map.entrySet()) {
            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
        }
        return sb.toString();
    }
}

LRU Cache 的链表+HashMap 实现

package lru;

import java.util.HashMap;

/**
 * Created by Joe on 2018/8/21.
 */
public class LRUCache<K, V> {
    private final int MAX_CACHE_SIZE;

    private Entry first;
    private Entry last;

    private HashMap<K, Entry<K, V>> hashMap;

    public LRUCache(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        hashMap = new HashMap<>();
    }

    public void put(K key, V value) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
            if (hashMap.size() > MAX_CACHE_SIZE) {
                hashMap.remove(last);
                removeLast();
            }
            entry = new Entry<>();
            entry.key = key;
        }

        entry.value = value;
        moveToFirst(entry);
        hashMap.put(key, entry);
    }

    public V get(K key) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) return null;
        moveToFirst(entry);
        return entry.value;
    }

    public void remove(K key) {
        Entry entry = getEntry(key);
        if (entry != null) {
            if (entry.pre != null) entry.pre.next = entry.next;
            if (entry.next != null) entry.next.pre = entry.pre;
            if (entry == first) first = entry.next;
            if (entry == last) last = entry.pre;
        }
        hashMap.remove(key);
    }

    private Entry<K, V> getEntry(K key) {
        return hashMap.get(key);
    }

    private void removeLast() {
        if (last != null) {
            last = last.pre;
            if (last == null) {
                first = null;
            } else {
                last.next = null;
            }
        }
    }

    private void moveToFirst(Entry<K, V> entry) {
        // 如果本就是第一个,直接返回
        if (entry == first) {
            return;
        }
        // 修改当前节点的前驱和后继节点,把当前节点独立出来
        if (entry.pre != null) {
            entry.pre.next = entry.next;
        }
        if (entry.next != null) {
            entry.next.pre = entry.pre;
        }
        // 如果entry为最后一个节点,则把last置前
        if (entry == last) {
            last = last.pre;
        }

        // 如果插入的为第一个节点
        if (first == null && last == null) {
            first = last = entry;
            return;
        }

        // 移动到first
        entry.next = first;
        first.pre = entry;
        first = entry;
        entry.pre = null;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Entry entry = first;
        while (entry != null) {
            sb.append(String.format("%s:%s ", entry.key, entry.value));
            entry = entry.next;
        }
        return sb.toString();
    }

    class Entry<K, V> {
        public Entry pre;
        public Entry next;
        public K key;
        public V value;
    }
}

LinkedHashMap 的 FIFO 实现

FIFO 是 First Input First Output 的缩写,也就是常说的先入先出,默认情况下 LinkedHashMap 就是按照添加顺序保存,我们只需重写下 removeEldestEntry 方法即可轻松实现一个 FIFO 缓存,简化版的实现代码如下

final int cacheSize = 5;
LinkedHashMap<Integer, String> lru = new LinkedHashMap<Integer, String>() {
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
    return size() > cacheSize;
    }
};

参考资料

  1. LRU缓存实现(Java)

二叉树有什么优点

二叉树 -> 二叉排序树,二叉排序树是一种比较有用的折衷方案。

数据的搜索比较方便,可以直接使用下标,但是删除或者插入元素比较麻烦。
而链表插入和删除元素很快,但是查找很慢。

而二叉排序树会兼有上面两个的好处,有序二叉树天然具有对数查找效率;二叉树天然具有链表特征。在处理大批量的动态数据比较有用。但是删除节点方法相对复杂。

参考资料:

  1. 二叉树的优点和缺点
  2. 分析线性表、二叉平衡树和哈希表存储数据时各自的优劣。
  3. 二叉树的优势

排序

排序复杂度.gif-56kB

快速排序

public void quickSort(int[] array, int _left, int _right) {
    int left = _left;
    int right = _right;
    int pivot;

    if (left < right) {
        // 获取基准线
        pivot = array[left];
        while (left != right) {
            //从右往左找到比基准线小的数
            while (left < right && pivot < array[right]) {
                right--;
            }
            //将右边比基准线小的数换到左边
            array[right] = array[left];


            //从左往右找到比基准线大的数
            while (left < right && pivot > array[left]) {
                left++;
            }
            //将左边比基准线大的数换到右边
            array[left] = array[right];
        }
        //此时left和right指向同一位置
        array[left] = pivot;
        quickSort(array, _left, left - 1);
        quickSort(array, left + 1, _right);
    }
}

堆排序

package other;

import java.util.Arrays;

/**
 * Created by Joe on 2018/8/21.
 */
public class HeapSort {
    public static void main(String args[]) {
        int []a = new int[] {16,25,34,27,30,5,7,4,41,55};
        heapSort(a);
        System.out.println(Arrays.toString(a));
    }

    static int parent(int i) {
        return (i - 1) / 2;
    }

    static int left(int i) {
        return 2 * i + 1;
    }

    static int right(int i) {
        return 2 * i + 2;
    }

    /**
     * 最大堆调整
     * @param a 待排序数据
     * @param i 第 i 个节点
     * @param heapSize 数组中实际排序元素的长度
     */
    static void maxHeapfy(int[] a, int i, int heapSize) {
        int left = left(i);
        int right = right(i);
        int largest = i;

        if (left < heapSize && a[left] > a[largest]) {
            largest = left;
        }
        if (right < heapSize && a[right] > a[largest]) {
            largest = right;
        }

        if (largest != i) {
            a[largest] ^= a[i];
            a[i] ^= a[largest];
            a[largest] ^= a[i];

            // 交换之后依旧需要保证最大根堆的性质
            maxHeapfy(a, largest, heapSize);
        }
    }

    static void buildMaxHeap(int[] a, int heapSize) {
        // heapSize是长度,而heapSize-1是最后一个节点的索引,或者叫下标
        // 再得到他的父节点应该是(heapSize-1 -1)/2
        for (int i = parent(heapSize - 1); i >= 0; i--) {
            maxHeapfy(a, i, heapSize);
        }
    }

    static void heapSort(int[] a) {
        int len = a.length;
        // 建立初始堆
        buildMaxHeap(a, len);
        // 之后进行交换
        a[len - 1] ^= a[0];
        a[0] ^= a[len - 1];
        a[len - 1] ^= a[0];

        // 初始建堆之后还要排a.length-2次
        for (int i = 0; i < len - 2; i++) {
            maxHeapfy(a, 0, len - 1 - i);
            a[len - 2 - i] ^= a[0];
            a[0] ^= a[len - 2 - i];
            a[len - 2 - i] ^= a[0];
        }
    }
}

归并排序

package other;

import java.util.Arrays;

/**
 * Created by Joe on 2018/8/21.
 */
public class MergeSort {
    public static void merge(int[] a, int low, int mid, int high) {
        int[] temp = new int[high - low + 1];
        int i = low;// 左指针
        int j = mid + 1;// 右指针
        int k = 0;
        // 把较小的数先移到新数组中
        while (i <= mid && j <= high) {
            if (a[i] < a[j]) {
                temp[k++] = a[i++];
            } else {
                temp[k++] = a[j++];
            }
        }
        // 把左边剩余的数移入数组
        while (i <= mid) {
            temp[k++] = a[i++];
        }
        // 把右边边剩余的数移入数组
        while (j <= high) {
            temp[k++] = a[j++];
        }
        // 把新数组中的数覆盖nums数组
        for (int k2 = 0; k2 < temp.length; k2++) {
            a[k2 + low] = temp[k2];
        }
    }

    public static void mergeSort(int[] a, int low, int high) {
        int mid = (low + high) / 2;
        if (low < high) {
            // 左边
            mergeSort(a, low, mid);
            // 右边
            mergeSort(a, mid + 1, high);
            // 左右归并
            merge(a, low, mid, high);
            System.out.println(Arrays.toString(a));
        }

    }

    public static void main(String[] args) {
        int a[] = { 51, 46, 20, 18, 65, 97, 82, 30, 77, 50 };
        mergeSort(a, 0, a.length - 1);
        System.out.println("排序结果:" + Arrays.toString(a));
    }
}

参考资料

  1. 排序算法时间复杂度、空间复杂度、稳定性比较
  2. 堆排序算法及其Java实现(以大根堆为例)

计算机网络

计算机网络体系架构

x

网络协议族

七层协议族

OSI七层模型 对应网络协议
应用层 HTTP、TFTP、FTP、NFS、WAIS、SMTP
表示层 Telnet、Rlogin、SNMP、Gopher
会话层 SMTP、DNS
传输层 TCP、UDP
网络层 IP、ICMP、ARP、RARP、AKP、UUCP
数据链路层 FDDI、Ethernet、Arpanet、PDN、SLIP、PPP
物理层 IEEE 802.1A、IEEE 802.2到IEEE 802.11

四层协议族

四层协议族.png-127kB

常用协议端口

应用程序 FTP TFTP TELNET SMTP DNS HTTP SSH MYSQL
熟知端口 21,20 69 23 25 53 80 22 3306
传输层协议 TCP UDP TCP TCP UDP TCP

HTTP 0.9/1.0/1.1/2

HTTP/0.9 只支持客户端发送 Get 请求,且不支持请求头。HTTP 具有典型的无状态性。

无状态是指协议对于事务处理没有记忆功能。缺少状态意味着,假如后面的处理需要前面的信息,则前面的信息必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要前面信息时,应答就较快。直观地说,就是每个请求都是独立的,与前面的请求和后面的请求都是没有直接联系的。

HTTP/1.0 在 HTTP/0.9 的基础上支持客户端发送 POST、HEAD。HTTP 1.0 需要使用 keep-alive 参数来告知服务器端要建立一个长连接,但默认是短连接。

HTTP/2 相比 HTTP/1.0 更简单,高效,强大。HTTP/2 的首要目标是通过完全的请求,响应多路复用,头部的压缩头部域来减小头部的体积,添加了请求优先级,服务端推送等。

参考资料

  1. HTTP/2 新特性总结
  2. 为什么说http协议是无状态协议

HTTP 首部字段

有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。

各种首部字段及其含义如下(不需要全记,仅供查阅):

通用首部字段

首部字段名 说明
Cache-Control 控制缓存的行为
Connection 控制不再转发给代理的首部字段、管理持久连接
Date 创建报文的日期时间
Pragma 报文指令
Trailer 报文末端的首部一览
Transfer-Encoding 指定报文主体的传输编码方式
Upgrade 升级为其他协议
Via 代理服务器的相关信息
Warning 错误通知

请求头

首部字段名 说明
Accept 用户代理可处理的媒体类型
Accept-Charset 优先的字符集
Accept-Encoding 优先的内容编码
Accept-Language 优先的语言(自然语言)
Authorization Web 认证信息
Expect 期待服务器的特定行为
From 用户的电子邮箱地址
Host 请求资源所在服务器
If-Match 比较实体标记(ETag)
If-Modified-Since 比较资源的更新时间
If-None-Match 比较实体标记(与 If-Match 相反)
If-Range 资源未更新时发送实体 Byte 的范围请求
If-Unmodified-Since 比较资源的更新时间(与 If-Modified-Since 相反)
Max-Forwards 最大传输逐跳数
Proxy-Authorization 代理服务器要求客户端的认证信息
Range 实体的字节范围请求
Referer 对请求中 URI 的原始获取方
TE 传输编码的优先级
User-Agent HTTP 客户端程序的信息

响应头

首部字段名 说明
Accept-Ranges 是否接受字节范围请求
Age 推算资源创建经过时间
ETag 资源的匹配信息
Location 令客户端重定向至指定 URI
Proxy-Authenticate 代理服务器对客户端的认证信息
Retry-After 对再次发起请求的时机要求
Server HTTP 服务器的安装信息
Vary 代理服务器缓存的管理信息
WWW-Authenticate 服务器对客户端的认证信息

实体首部字段

首部字段名 说明
Allow 资源可支持的 HTTP 方法
Content-Encoding 实体主体适用的编码方式
Content-Language 实体主体的自然语言
Content-Length 实体主体的大小
Content-Location 替代对应资源的 URI
Content-MD5 实体主体的报文摘要
Content-Range 实体主体的位置范围
Content-Type 实体主体的媒体类型
Expires 实体主体过期的日期时间
Last-Modified 资源的最后修改日期时间

参考资料

  1. Http请求头和响应头

HTTP 状态码

服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。

状态码 类别 原因短语
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出错

1XX 信息

  • 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

2XX 成功

  • 200 OK

  • 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。

  • 206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。

3XX 重定向

  • 301 Moved Permanently :永久性重定向

  • 302 Found :临时性重定向

  • 303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。

  • 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。

  • 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。

  • 307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。

4XX 客户端错误

  • 400 Bad Request :请求报文中存在语法错误。

  • 401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。

  • 403 Forbidden :请求被拒绝。

  • 404 Not Found

5XX 服务器错误

  • 500 Internal Server Error :服务器正在执行请求时发生错误。

  • 503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

HTTP 的服务器端和客户端能双向通信吗

HTTP 协议是基于TCP/IP的,从协议角度来说是和服务器端具有双向的通信能力。

但是使用 HTTP 协议的 WEB 服务器不能主动推送信息给客户浏览器。现在可以使用WebSocket协议进行替代。或者使用SSE协议让服务器端给客户端进行信息的推送。

原因是:HTTP主要是给web 服务使用的协议,对于 web 服务来说,并发的数量最为重要。如果采用 TCP 的有状态协议,那么用户只要打开浏览器,则与服务器的HTTP连接就不会切断。但是服务器所能同时开启的连接数是有限制的。因此为了服务器的性能着想,基于HTTP的服务器端并不能和客户端进行双向通信。

参考资料:

  1. http 是基于 tcp/ip 的,那么它也是可以具有和服务器双向通信的能力的。我们为什么不升级 http,搞个 http x.1
  2. HTTP 是否可以实现双向通讯?

TCP 三次握手和四次挥手

网络连接总结

网络图总结.jpg-50.9kB

三次握手

此处输入图片的描述

  1. 第一次握手: Client 将标志位 SYN 置为 1,随机产生一个值 seq=J,并将该数据包发送给 Server,此时 Client 进入 SYN_SENT 状态,等待 Server 确认。
  2. 第二次握手: Server 收到数据包后,由标志位 SYN=1 知道 Client 要请求建立连接,Server 将标志位 SYN 和 ACK 置 1,ack=J+1,并且随机产生一个 seq=K,并且将该数据包发送给 Client 以确认连接请求,Server 进入 SYN_RCVD 状态。
  3. 第三次握手: Client 收到确认后,检查 ack 是否为 J+1,ACK 是否为 1,如果正确则将标志位 ACK 置为 1,ack=K+1,并将该数据包发送给 Server,Server 检查 ack 是否为 K+1,ACK 是否为 1,如果正确则连接建立成功,Client 和 Server 进入 ESTABLISHED 状态,完成三次握手,随后 Client 与 Server 之间可以开始传输数据了。

TCP 建立连接为什么需要三次握手

主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

TCP 四次挥手

TCP四次挥手.png-48.6kB

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为 seq=u(等于前面已经传送过来的数据的最后一个字节的序号加 1),此时,客户端进入 FIN-WAIT-1(终止等待 1)状态。 TCP 规定,FIN 报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号 seq=v,此时,服务端就进入了 CLOSE-WAIT(关闭等待)状态。**TCP 服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。**这个状态还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入 FIN-WAIT-2(终止等待 2)状态,等待服务器发送连接释放报文**(在这之前还需要接受服务器发送的最后的数据)。**
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为 seq=w,此时,服务器就进入了 LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是 seq=u+1,此时,客户端就进入了 TIME-WAIT(时间等待)状态。注意此时 TCP 连接还没有释放,必须经过 2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的 TCB 后,才进入 CLOSED 状态。
  6. 服务器只要收到了客户端发出的确认,立即进入 CLOSED 状态。同样,撤销 TCB 后,就结束了这次的 TCP 连接。可以看到,服务器结束 TCP 连接的时间要比客户端早一些。

为什么TCP 挥手需要四次

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。

而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

其对应的 Client 端的 FIN_WAIT_2 状态。实际上 FIN_WAIT_2 状态下的 SOCKET,表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你 (ACK 信息),稍后再关闭连接。(主动方)。

为什么客户端最后需要等待 2MSL

MSL 即 Maximum Segment Lifetime,就是最大报文生存时间,是任何报文在网络上的存在的最长时间,超过这个时间报文将被丢弃。TCP 允许不同的实现可以设置不同的 MSL 值。(《TCP/IP 详解》中是这样描述的:MSL 是任何报文段被丢弃前在网络内的最长时间。RFC 793 中规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟、2 分钟等。)

  1. 保证客户端发送的最后一个 ACK 报文能够到达服务器,因为这个 ACK 报文可能丢失,站在服务器的角度看来,我已经发送了 FIN+ACK 报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个 2MSL 时间段内收到这个重传的报文,接着给出回应报文,并且会重启 2MSL 计时器。
  2. 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个 2MSL 时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

参考资料

  1. TCP的三次握手与四次挥手(详解+动图)
  2. TCP四次挥手(图解)-为何要四次挥手
  3. TCP三次握手及四次挥手详解及常见面试题
  4. 网络知识总结

GET 和 POST 的区别

  GET POST
后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。 书签 可收藏为书签 不可收藏为书签 缓存 能被缓存 不能缓存 编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded 或 multipart/form-data。为二进制数据使用多重编码。 历史 参数保留在浏览器历史中。 参数不会保存在浏览器历史中。 对数据长度的限制 是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 个字符)。 无限制。 对数据类型的限制 只允许 ASCII 字符。 没有限制。也允许二进制数据。 安全性

与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。

在发送密码或其他敏感信息时绝不要使用 GET !

POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。 可见性 数据在 URL 中对所有人都是可见的。 数据不会显示在 URL 中。

参考链接

  1. HTTP 方法:GET 对比 POST

Session 和 Cookie 的区别

  1. Session: Session 是用于唯一标志客户信息的对象,容器通过浏览器端传送的 cookie 中的 jsessionid 来区分不同的客户对象,所以 Session 依赖于 cookie。是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。
  2. Cookie: cookie 是在服务器端创建,然后服务器发送到浏览器端,存储在浏览器边,当再次请求服务器时候,浏览器会将该域中的相关 cookie 发送到服务器端。是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。

区别

  1. cookie 数据存储在客户端上, session 数据存储在服务器上。
  2. cookie 数据由于存储在客户端上并不是十分安全,别人可以通过分析存放在本地的 cookie 进行 cookie 欺骗。因此考虑到安全问题应当使用 session。
  3. session 会在一定时间上保存在服务器中。当访问增多时,会大量占用服务器的性能。如果为了考虑减少服务器的压力,应当使用 cookie。
  4. 单个 cookie 的保存数据大小不能超过 4k,很多浏览器限制一个站点最多保存 20 个 cookie。

因此,可以考虑将登陆等重要的信息保存在 session 中,其他相对不中要的数据可以存储在 cookie 中。

参考链接:

  1. cookie 和 session 的区别详解
  2. Session 和 Cookie 的关系

MySQL 索引类型

MySQL 主要有以下几种索引类型: 普通索引,唯一索引,主键索引,组合索引,全文索引。

创建索引的语句如下:

CREATE TABLE table_name[col_name data type]
[unique|fulltext][index|key][index_name](col_name[length])[asc|desc]
  1. unique|fulltext 为可选参数,分别表示唯一索引、全文索引
  2. indexkey 为同义词,两者作用相同,用来指定创建索引
  3. col_name 为需要创建索引的字段列,该列必须从数据表中该定义的多个列中选择
  4. index_name 指定索引的名称,为可选参数,如果不指定,默认 col_name 为索引值
  5. length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度
  6. ascdesc 指定升序或降序的索引值存储

1. 普通索引

是最基本的索引,它没有任何限制。

CREATE INDEX index_name ON table(column(length))

2. 唯一索引

与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。

CREATE UNIQUE INDEX indexName ON table(column(length))

3. 主键索引

是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:

CREATE TABLE `table` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` char(255) NOT NULL ,
    PRIMARY KEY (`id`)
);

4. 组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合:

ALTER TABLE `table` ADD INDEX name_city_age (name,city,age); 

5. 全文索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext 索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句的参数匹配。fulltext 索引配合 match against 操作使用,而不是一般的 where 语句加 like。它可以在 create table,alter table ,create index 使用,不过目前只有 char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用 CREATE index 创建 fulltext 索引,要比先为一张表建立 fulltext 然后再将数据写入的速度快很多。

CREATE FULLTEXT INDEX index_content ON article(content)

参考资料

  1. 7.1.2 索引的分类
  2. MySQL 索引类型

MySQL 建索引的原则

  1. 选择唯一性索引
    唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。
  2. 为经常排序、分组和联合操作的字段建立索引
    经常需要 ORDER BY、GROUP BY、DISTINCT 和 UNION 等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。
  3. 为常作为查询条件的字段建立索引
    如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。
  4. 限制索引的数目
    索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
  5. 尽量使用数据量少的索引
    如果索引的值很长,那么查询的速度会受到影响。例如,对一个 CHAR(100) 类型的字段进行全文检索需要的时间肯定要比对 CHAR(10) 类型的字段需要的时间要多。
  6. 尽量使用前缀来索引
    如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT 和 BLOG 类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。
  7. 删除不再使用或者很少使用的索引
    表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
  8. 最左前缀匹配原则,非常重要的原则。
    mysql 会一直向右匹配直到遇到范围查询 (>、<、between、like) 就停止匹配,比如a 1 and b=2 and c>3 and d=4 如果建立 (a,b,c,d) 顺序的索引,d 是用不到索引的,如果建立 (a,b,d,c) 的索引则都可以用到,a,b,d 的顺序可以任意调整。
  9. =和 in 可以乱序。
    比如a = 1 and b = 2 and c = 3 建立 (a,b,c) 索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式
  10. 尽量选择区分度高的列作为索引
    区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就 是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条 记录
  11. 索引列不能参与计算,保持列“干净”。
    比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本 太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
  12. 尽量的扩展索引,不要新建索引。
    比如表中已经有 a 的索引,现在要加 (a,b) 的索引,那么只需要修改原来的索引即可

注意:选择索引的最终目的是为了使查询的速度变快。上面给出的原则是最基本的准则,但不能拘泥于上面的准则。读者要在以后的学习和工作中进行不断的实践。根据应用的实际情况进行分析和判断,选择最合适的索引方式。

MySQL 不用索引情况

  1. MySQL 估计使用索引扫描比全表扫描慢,则不使用索引。
// 列 key_part1 均匀的分布在 1-100 之间。下面的 sql 则不会使用索引
// 或者数据库表数量极少
 select * from table_name where key_part1 > 1 and key_part1 <90
  1. 如果查询条件中有 or ,则不会使用索引查询。如果使用 or,且想让索引生效。则每一列都需要加上索引。

    // 如果在 key1 上有索引而在 key2 上没有索引,则该查询也不会走索引
    select * from table_name where key1='a' or key2='b';
    
  2. 复合索引,如果索引列不是复合索引的第一部分,则不使用索引(即不符合最左前缀)

    // 复合索引为 (key1,key2) ,以下 sql 将不会使用索引
    select * from table_name where key2='b';
    
  3. 如果 like 是以‘%’开始的,则该列上的索引不会被使用。

select * from table_name where key1 like '%a';
  1. 存在索引列的数据类型隐形转换,则用不上索引,比如列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。

    // 如果 key1 列保存的是字符串,即使 key1 上有索引,也不会被使用。
    select * from table_name where key1=1;
    
  2. where 子句里对索引列上有数学运算,用不上索引
    20161129171115358.jpg-15.4kB

  3. where 子句里对有索引列使用函数,用不上索引
    20161129171116027.jpg-15.8kB

MySQL 不推荐使用索引情况

  1. 数据唯一性差的时候不推荐使用索引(比如一个字段取值只有几种)。

    比如性别,只有两种可能数据。意味着索引的二叉树级别少,多是平级。这样的二叉树查找无异于全表扫描。

  2. 频繁更新的字段不推荐使用索引。

    比如 login_count 登录次数,频繁变化导致索引也频繁变化,增大数据库工作量,降低效率。

  3. 字段不在 where 语句出现的时候不推荐使用索引;如果 where 包含:IS NULL, IS NOT NULL, LIKE '%判断字符%' 等条件也不推荐使用索引。

    只有在 where 语句出现,mysql 才会去使用索引

  4. WHERE 子句中对索引列使用不等于(<>),使用索引效果也一般。

参考资料

  1. MYSQL 索引类型、什么情况下用不上索引、什么情况下不推荐使用索引

MYSQL SQL优化

获取有性能的SQL

  • 通过用户反馈获取存在性能问题的SQL
  • 通过慢查询日志获取存在性能问题的SQL
  • 书写脚本实时查询存在性能问题的SQL

慢查日志

慢查日志常用参数

  1. show_query_log:用于启动和停止记录慢查询日志,通过设置 show_query_log=ON 来启动记录慢查日志;
    开启慢查日志.png-94.2kB
  2. show_query_log_file:指定查询慢查日志的存储路径及文件,默认情况下保存在 MySQL 的数据目录中,建议将日志存储和数据存储分开存储;
  3. long_query_time:指定记录慢查日志 SQL 查询时间的阈值,参数默认值为 10 秒,通常对于繁忙的系统来说,改为 0.001 秒(即 1 毫秒)更为合适;
  4. log_queries_not_using_indexes:是否记录未使用索引的 SQL,这个开启之后,就算该 SQL 执行实现小于前面所设定的long_query_time阈值,也会被记录下来,可以帮助提前发现由于数据库条数比较小,但是未使用索引的 SQL,可以提前对 SQL 进行优化,避免数据量增长之后出现 SQL 运行速度过缓的问题。

慢查询日志内容

慢查询日志内容.png-315.3kB

  1. 第一行保存的是 SQL 语句执行者的用户,域名以及线程 ID;
  2. 第二行保存的是 SQL 的查询时间,精确到毫秒;
  3. 第三行保存的是数据库使用锁的时间;
  4. 第四行保存的是 SQL 数据执行之后,所返回的数据条数;
  5. 第五行保存的是数据库执行该 SQL 时,所扫描的数据库的行数;
  6. 第六行保存的是执行该 SQL 语句时的数据库时间,采用的是 UNIX 时间戳的形式;
  7. 第七行保存的是所执行 SQL 的具体语句内容。

慢查询日志常用分析工具

1. mysqldumpslow

为MySQL官方所带的慢查询日志分析工具,汇总除查询条件外,完全相同的SQL,并将分析结果按照参数中所指定的顺序进行输出。

使用示例如下:

mysqldumpslow -s r -t 10 slow-mysql.log

其中 -s order(c,t,l,r,at,al,ar) 参数表示慢查询日志的排序方式。

mysqldumpslow参数.png-138.6kB

其中-t top,表示指定取前 top 条作为结束输出。

2. pt-query-digest

阅读更多

没有更多推荐了,返回首页