java知识点汇总

文章目录

面试前的工作

自我介绍

面试官好:

很荣幸有机会参加xxx公司面试。我此次应聘的职位是Java后端开发工程师。下面我请允许我自我介绍一下。

我叫张天扬,今年21,来自浙江省桐乡市,目前是陕西理工大学计算机科学与技术专业大四学生,对软件开发怀有浓烈的兴趣,对Java语言尤其熟悉,有良好的Java编程知识和面向对象编程思想,熟悉常用的数据结构和算法,熟悉 MVC 开发模式和前后端分离框架,熟练使用 Maven、Git 管理工具,熟悉 Web 框架和 J2EE 开发技术,熟悉 Linux 操作系统和常用的操作指令、熟悉 JVM 内存模型、类加载 。目前已经编写了几个项目,拥有一定的项目经验,其中技术咖论坛在全国大学生计算机设计大赛中,获得西北赛区三等奖。

我觉得自己为人踏实、勤奋、好学,严格要求自己,工作积极,一丝不苟,对新事物有较强的接受能力。对自己已掌握的技术敢于创新,不畏难题,勇于迎接新挑战!在平时生活中喜欢将自己学到的一些新技术写成博客与他人分享.也会经常阅读他人的技术性文章.来提升自己的能力,丰富自己的想法。能够妥善的处理周围的人际关系,团结同事,并具有极强的团队合作精神。我能够通过自己的努力在为企业服务的过程中实现自身价值。

专业书籍:深入理解JVM、鸟哥的Linux私房菜、大话设计模式

充足的知识储备

操作系统

计算机网络

数据结构算法

java语言(javaSE,javaEE,框架,JVM)

数据库

项目

linux

javaSE

javaSE内容作为java开发方向的基础知识,一定要扎实,对知识点要有自己深入的理解.一旦有磕绊对面试官的印象将大打折扣.

JMM内存模型

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

jdk1.8新特性

  • Lambda表达式

    本质上是一段匿名内部类,也可以是一段可以传递的代码。

  • 函数式接口

    定义了一个抽象方法的接口,就是函数式接口,并且还提供了注解:@Functionallnterface

  • *方法引用和构造器调用

  • Stream API

  • 接口中的默认方法和静态方法

    在接口中可以使用default和static关键字来修饰接口中定义的普通方法

    在JDK1.8中很多接口会新增方法,为了保证1.8向下兼容,1.7版本中的接口实现类不用每个都重新实现新添加的接口方法,引入了default默认实现,static的用法是直接用接口名去调方法即可。当一个类继承父类又实现接口时,若后两者方法名相同,则优先继承父类中的同名方法,即“类优先”,如果实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法。

  • 新时间日期API

    新的日期API都是不可变的,更使用于多线程的使用环境中。

java语言的特征

简单性、面向对象、分布式、健壮性、安全性、体系结构中立、可移植性、跨平台性、解释型、高性能、多线程、动态性。

不要小看此问题,他会引出一连串问题:

例如:如何实现跨平台,JVM作用,面向对象的理解,多线程…

如何实现跨平台

为不同的平台的提供不同的JVM(虚拟机),将字节码翻译/编译成不同平台支持的指令.

JDK:Java开发工具包 包含jre

JRE:Java运行环境(核心工具类) 包含JVM

JVM:Java虚拟机

谈谈你对面向对象的认识理解

面向过程分析出解决问题所需的步骤,然后将步骤一步步实现。

比如将大象装进冰箱,分为三步,打开冰箱,放入大象,关上冰箱。

面向过程以分类的方法进行思考和解决问题,面向对象的思维方式适合于处理复杂的问题。面向对象无法取代面向过程,两者是相辅相成的,面向对象关注于宏观上把握事物之间的关系,在具体到如何实现某个细节时,任然采用面向过程的思维方式。

比如将大象装进冰箱,首先设计三个类冰箱类(门类(包含开门,关门两个方法)),人类(包含操作方法),大象类(进入冰箱功能)

接着按照面向过程的思想调用三个类,解决问题。

类的构成{

​ 成员变量

​ 成员方法

​ 构造方法

​ 代码块

​ 内部类

}

1、易维护
采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
2、质量高
在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
3、效率高
在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
4、易扩展
由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低

聊聊面向对象的特征,封装,继承,多态

封装

​ 隐藏类的信息,不向外界暴露,隐藏实现细节,向外提供特定的方法访问.

​ 成员变量私有化,单例模式是封装的一种体现

继承

​ 在原有类的基础上派生出新的类,新的类具有原有类的所有非私有属性和非私有方法,并扩展新的能力。

​ 优点:可扩展性,代码可重用性

多态

​ 实现多态的三个必要条件:继承,重写,父类的引用指向子类对象

同一事物不同时刻的不同状态

程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。

多态

​ 同一事物,在不同时候表现不同状态

​ 前提: 继承,方法重写,父类引用指向子类对象

​ 优点:提升程序可维护性,可扩展性 集合 异常 String.valueOf(Object obj) jdbc 接口 mysql servlet Httpetservletrequest

​ 缺点:不能调用子类特有的方法 解决方法:向下转型

多态满足面向对象设计原则中的里氏替换原则.

访问权限

包: 避免类重名

​ 管理类

​ 访问权限控制

public 修饰类,变量,方法 在任何地方都可以访问

private 修饰变量,方法,内部类 只能在本类中访问

proected 修饰变量,方法 在同包中,不同包的子类中访问

默认 修饰类,变量,方法 在同包类中可以访问

继承

优点:

代码复用,易于扩展,维护

缺点:

打破了父类的封装性

父类和子类联系紧密,耦合度高 , 父类一旦发生了变化,子类可能会受影响

结合面向对象设计7大原则来讲. 单一职责原则,组合 聚合复用原则.

应用场景:

​ 多种相同类型有共性属性行为时使用继承

对象创建过程? 从jvm的角度出发,

延伸到对象在内存中的存储空间

img

new Car(int price);

new Car();

Java类初始化顺序

基类静态代码块,基类静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)——>派生类静态代码块,派生类静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)——>基类普通代码块,基类普通成员字段(并列优点级,按代码中出现先后顺序执行)——>基类构造函数——>派生类普通代码块,派生类普通成员字段(并列优点级,按代码中出现先后顺序执行)——>派生类构造函数.

静态变量->静态代码块->成员变量->构造代码块->构造方法->普通代码块

java中创建对象的方式

new

反射(Class类的newInstance方法、使用java.lang.relect.Constructor类的newInstance方法) Class.forName(“com.ff.Car”)

  • Class类的newInstance方法

    Student student2 = (Student)Class.forName(className).newInstance(); 
    // 或者:
    Student stu = Student.class.newInstance();
    
  • Constructor类的newInstance方法

    Constructor<Student> constructor = Student.class.getInstance();
    Student stu = constructor.newInstance();
    

区别:

两种newInstance方法区别:

  1. 从包名看,Class类位于java的lang包中,而构造器类是java反射机制的一部分。
  2. 实现上,Class类的newInstance只触发无参数的构造方法创建对象,而构造器类的newInstance能触发有参数或者任意参数的构造方法。(查看第二部分的源码第2 条解释,)
  3. Class类的newInstance需要其构造方法是共有的或者对调用方法可见的,而构造器类的newInstance可以在特定环境下调用私有构造方法来创建对象。这点可以从上面源码的第1 条解释可以看出。
  4. Class类的newInstance抛出类构造函数的异常,而构造器类的newInstance包装了一个InvocationTargetException异常。这是封装了一次的结果。

对象反序列化 IO

对象clone()

对象克隆,浅克隆,深克隆

什么是对象克隆?

创建一个新的对象,将原对象中的数据复制到新对象中

什么时候用?

​ 将请求中接收到数据的对象信息, 拷贝到向dao层传入的参数对象中.

浅克隆?

​ 基本类属于浅克隆,

​ 只将对象中关联的对象的引用地址复制属于浅克隆

深克隆?

​ 只将对象中关联的对象也进行了克隆,多级克隆.

​ 解决多级克隆: 在关联的对象中 继续重写克隆方法

​ 通过对象序列化,反序列化.

构造方法

特征

方法名与类名相同,没有返回值,不可以被void修饰

一个类中可以有多个构造方法, 默认有一个无参的构造(隐式),一旦显示的定义有参的构造方法,默认的就失效,

一般情况,显示的定义无参的构造方法

作用: 初始化创建对象的成员变量

new Car(int age,int a) Car() age=0; String name=null

方法重载,方法重写

​ 在一个类中,有多个名称相同的方法,参数不同.

在继承关系中,父类实现不能满足子类的需求,在子类中重写(覆盖,覆写)父类方法, 功能的扩展,super.父类

为什么构造方法不能重写

​ 构造方法名与当前类名相同, 重写要求与父类方法结构一致.

​ 在子类的构造方法中调用父类的构造方法, 必须放在子类构造方法的第一行.

对象与引用

基本类型

何为引用类型

针对类类型(复合类型), 变量 指向堆中一个对象

值传递和引用传递

基本类型,传值 10

int a = 10;

int b = a; //b=10
引用类型

引用传递, 传递的对象的地址值.

本质都是传:

Pass By Value 和 Pass By Reference

细化为:值传递(基本类型),引用传递(引用类型 不是对象本身,只是传递对象引用地址)

静态static

表示静态(指在内存只有一份的)

修饰内部类,成员变量,成员方法,代码块

特点:

  • 都是随着类的加载而加载
  • 优先于对象的存在
  • 修饰的成员被所有对象共享
  • 可以不创建对象,直接被类调用

**注意:**在static方法内部只能访问类的static属性,不能访问非static属性

抽象类和接口

作用:抽象,功能定义 开闭原则,里氏替换原则都依赖接口,抽象类.

相同点:

​ 都可以包含抽象方法, 不能被实例化.抽象类构造方法和静态方法不可以修饰为abstract

不同点:

​ 抽象类中可以包含成员变量 接口中只能包含静态常量

​ 抽象类中可以包含成员方法 接口中可以包含抽象方法,默认(子类调用),静态(接口调用静态方法jdk8后添加)

​ 抽象类可以包含构造方法,初始化父类成员 接口中不能有构造方法

​ 抽象类中的抽象方法的访问类型可以是public ,protected和默认类型 接口中每一个方法也是隐式抽象的,默认为public abstract 。接口中声明的属性默认为 public static final 的

从jdk1.8开始,接口中的方法不再是只能有抽象方法(普通方法会被隐式地指定为public abstract方法),他还可以有静态方法和default方法。并且静态方法与default方法可以有方法体!

Object

Java语言不同于C++语言,是一种单根继承结构语言,也就是说,Java中所有的类都有一个共同的祖先。这个祖先就是Object类。

equals() hashCode wait notify finalize() clone()

如图可知,Object类有12个成员方法,按照用途可以分为以下几种
1,构造函数
2,hashCode和equale函数用来判断对象是否相同,
3,wait(),wait(long),wait(long,int),notify(),notifyAll()
4,toString()和getClass
5,clone()函数的用途是用来另存一个当前存在的对象。
6,finalize()用于在垃圾回收

判断两个对象是否相等

基本原理

java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
java中的引用类型的对象比较变态,假设有两个引用对象obj1,obj2,obj1==obj2 判断是obj1,obj2这两个引用变量是否相等,即它们所指向的对象是否为同一个对象。言外之意就是要求两个变量所指内存地址相等的时候,才能返回true,每个对象都有自己的一块内存,因此必须指向同一个对象才返回ture。

遇到的问题

如果想要自定义两个对象(不是一个对象,即这两个对象分别有自己的一块内存)是否相等的规则,那么必须在对象的类定义中重写equals()方法,如果不重写equals()方法的话,默认的比较方式是比较两个对象是否为同一个对象。
在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。

重写equals的原则

  • 1、先用“==”判断是否相等。
  • 2、判断equals()方法的参数是否为null,如果为null,则返回false;因为当前对象不可能为null,如果为null,则不能调用其equals()方法,否则抛java.lang.NullPointerException异常。
  • 3、当参数不为null,则如果两个对象的运行时类(通过getClass()获取)不相等,返回false,否则继续判断。
  • 4、判断类的成员是否对应相等。

String

特征

一旦创建时赋值后,值不可变.不可以被继承.一旦修改会重新创建应该新的对象

final class String {

​ final char[] value;

}

创建对象时,先在常量池检查有没有相同的,如果没有就在堆中创建一个字符串对象,值在常量池中,第二次创建时,常量池已经存在,直接让引用变量指向已有的对象地址

为什么不可变

不能改变指的是对象内的成员变量值

private final char[] value;

String类不可变性的好处? 为什么设计为不可变.

1.便于实现字符串常量池

在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。

2.使多线程安全

看下面这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加"bbb"public class test {
  // 不可变的String
  public static String appendStr(String s) {
      s += "bbb";
      return s;
  }

  // 可变的StringBuilder
  public static StringBuilder appendSb(StringBuilder sb) {
      return sb.append("bbb");
  }
  
  public static void main(String[] args) {
      String s = new String("aaa");
      String ns = test.appendStr(s);//aaabbb
      System.out.println("String aaa>>>" + s.toString());//aaa
      // StringBuilder做参数
      StringBuilder sb = new StringBuilder("aaa");
      StringBuilder nsb = test.appendSb(sb);
      System.out.println("StringBuilder aaa >>>" + sb.toString());
  }
}

如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”.因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之后,就变成了"aaabbb"。
有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。

在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。

3.避免安全问题

在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

4.加快字符串处理速度

由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。

在String类的定义中有如下代码:

private int hash;//用来缓存HashCode

总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。

String的值不可以改变吗?

//通过反射改变String底层数组值
public static void main(String[] args) throws Exception {
        String s = "Hello World";
        System.out.println("s = " + s); //Hello World
        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
        System.out.println("s = " + s);  //Hello_World
        System.out.println(s);//Hello_World
    }

String对象创建方式

Java中创建String的两种方式:

 String s1 = "abc";
String s2 = new String("abc");

​ 方法1中,先在常量池中查找有没有"abc"这个字符串对象存在,

​ 如果存在就把常量池中的指向这个字符串对象;
​ 方法2中,不论常量池中中是否已经存在"abc"这个字符串对象,都会新建一个对象。

        String strA  =   " abc " ;
        String strB  =   " abc " ;
        String strAA  =   new  String( " abc " );
        String strBB  =   new  String( " abc " );
        System.out.println(strA  ==  strB);//true
        System.out.println(strAA  ==  strBB);// false

这句代码的字面量在哪里?  String str = new ("hello"); 
  常量池

创建了几个对象?

1.
String s = new String("abc")创建了几个对象
情况1: 创建两个对象
  String s = new String("abc");
  直接new对象,此前在字符串常量池中不存在"abc",此种情况在堆中创建一个对象,然后在字符串常量池中创建一个对象
情况2: 创建一个对象
  String s = "abc";
  String s1 = new String("abc");
  因为字符串常量池中已存在"abc",只需要在堆中创建即可.
2.    
创建了一个对象,底层优化创建了一个StringBuilder对象,append()追多个字符串对象,只产生一个对象.
 String st1 = "a" + "b" + "c";//只创建了一个字符串对象
 String st2 = "abc";
 System.out.println(st1 == st2);//true
3.
 String st1 = "ab";
 String st2 = "abc";
 String st3 = st1 + "c";//一旦发生拼接中有变量,会重新创建一个字符串对象
 System.out.println(st2 == st3);//false


string中常用的方法

构造方法

String()

String("")

String(byte[] b ,0,length,chatset)

String(char[] b ,0,length,chatset)

判断功能

equals, compareTo, contains, isempty startWith

获取功能

length indexOf charAt substring

转换

split valueOf() tocharArray

正则表达式

String,StringBuffer,StringBuilder区别

String 值不可以改变

StringBuffer 值可变 线程安全的

StringBuilder 值可变 线程不安全

StringBuilder和StringBuffer的初始容量都是16,程序猿尽量手动设置初始值。以避免多次扩容所带来的性能问题,默认数组容量扩充为原数组容量的2倍+2。假设这个新容量仍然小于预定的最小值(minimumCapacity),那么就将新容量定为(minimumCapacity),最后推断是否溢出,若溢出,则将容量定为整型的最大值0x7fffffff。

基本类型包装类

因为基本类型不是对象,所以每个基本类提供了一个类包装基本类.使用面向对象方式操作。基本数据类型放在堆栈中,对象放在堆中

最大值,最小值,转换

Integer

​ int value

自动装箱 Integer a = 10 valueOf() 注意 -128 +127 之间会从缓存池中直接获取

​ ==

自动拆箱 int b = a; intvalue()

Object

Arrays 排序(冒泡,选择,插入 ,快排 , 堆排) ,二分搜索

日期

异常

什么是异常?

运行时异常,可以使用异常处理机制处理 不是错误(OOM)

异常体系

​ Throwable

Error Exception

​ RunTimeException

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HB0SnuyY-1634353769845)(D:\桌面\笔记\1632106539174.png)]

异常分类

运行时异常

编译期(检查)异常 编写代码时,需要处理

常见的异常类有哪些?

NullPointerException 空指针异常
ClassNotFoundException 指定类不存在
NumberFormatException 字符串转换为数字异常
IndexOutOfBoundsException 数组下标越界异常
ClassCastException 数据类型转换异常
FileNotFoundException 文件未找到异常
NoSuchMethodException 方法不存在异常
IOException IO 异常
SocketException Socket 异常

异常处理

try{ 可能出现异常的代码

​ return

}

catch(异常类型 ){

​ 处理异常

return

}

finally{

​ 无论是否出现异常,都会执行

return

}

try{ 可能出现异常的代码

}finally{

}

  • throws

​ throws,定义一个方法的时候可以使用throws关键字声明,表示此方法不处理异常,而交给方法调用处进行处理.

基本语法

public void test() throws 异常1,异常2,异常3{

}

注意:

  1. 任何方法都可以使用throws关键字声明异常类型,包括抽象方法。
  2. 子类重写父类中的方法,子类方法不能声明抛出比父类类型更大的异常。
  3. 使用了throws的方法,调用时必须处理声明的异常,要么使用try-catch,要么继续使用throws声明。
  • throw

throw关键字用于显示抛出异常,抛出的时候是一个异常类的实例化对象。

在异常处理中,try语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出.

语法:throw new 异常类构造方法

public static void someMethod() {
if (1==1) {
    throw new RuntimeException("错误原因");
}
}

自定义异常

​ 根据业务需要,自定义不同的异常类.

public static void main(String[] args) {
try {
            test(101);
        } catch (ScoreException e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }

    }

    public  static int test(int s)throws ScoreException{
              if(s<0||s>100){
                  //throw new RuntimeException("分数不合法"); //在程序中主动抛出异常对象,在构造方法中可以传入异常原因
                  throw new ScoreException("分数不合法");
              }
              return s;
    }
}

/*当分数不满足条件时,抛出此对象.*/
public class ScoreException extends  Exception{

    public ScoreException() {
        super();
    }

    public ScoreException(String message) {
        super(message);
    }
}

集合

为什么需要集合

​ 程序中的数据长度可变.

​ 数据结构不同.

许多集合类

单列集合 双列集合

集合体系

各集合类的底层结构实现

List: 可以重复,有序的

ArrayList: 底层Object数组实现,查询快,(数组中间)增删慢, 浪费空间

​ 第一次添加时,初始化一个默认容量为10,jdk8的时候初始化为{},只有在add元素时才创建10个容量

​ 创建时,通过构造方法指定初始容量

​ 扩容: 添加满了之后,会发生扩容 grow() 扩容为原来的1.5倍

LinkedList: 底层是双向链表, 查询慢,增删快 实现队列,栈

​ 没有扩容机制,就是一直在前面或者后面新增就好。

什么时候用ArrayList,LinkedList

Vector: 底层是数组 ,线程安全的.第一次添加时,初始化一个默认容量为10,添加满了之后,会发生扩容 grow() ,扩容为原来的2倍

集合遍历

​ for 支持增删(注意索引变化) 增强for(只能删除一个,报错 使用break)

迭代器(里面有一个计数器)

iterator();从前向后
listIterator(arrayList.size()); 从指定的位置开始遍历  
 从后向前listIterator.hasPrevious() previous()

stream 流式

Set 不能重复,元素无序

  • HashSet:底层是hash表(无序,唯一)。继承hashmap双列,value值默认object

    如何保证元素唯一性?

    根据key计算hash值,确定在hash表中的索引,若该位置未存在元素,则插入,反之,通过equals方法对key的真实内容进行比较,如果比较结果是key值已经存在于表中,那么就不对其进行插入,而是返回原本的key值对应的value值。如果这个表中的元素不存在key值相同的元素,那么就将(hash, key, value, i)插入链表的下一个位置。

  • LinkedHashSet:底层链表和哈希表。(FIFO插入有序,唯一)

    链表保证元素有序

    哈希表保证元素唯一

  • TreeSet:底层红黑树。(唯一,有序)

HashSet -->HashMap, TreeSet—>TreeMap

hashCode() equals() “”.hashCode()

解决hash冲突:

  • 开发寻址法(在表中寻找下一个空位置)
  • 链地址法(发生冲突时在采用拉链的方法)
  • 再散列法

保证效率,又保证安全

HashMap结构

线程不安全

https://blog.csdn.net/qq_26542493/article/details/105482732

哈希表+链表+红黑树

允许null(key和value都允许)

无序

哈希值的使用,HashMap的父类是AbstractMap。

而HashMap重新计算hash值

创HashMap是默认哈希表容量是16,加载因子是0.75(当元素个数超过容量长度的0.75倍时扩容),扩容为原来的2倍 也可以指定长度

put(k,value)

​ 添加时首先是通过k的哈希值,再通过哈希函数计算位置,

​ 位置上如果没有元素添加在链表的头结点,如果有插入到链表的下一个节点.

链表的长度==8,转为红黑树

扩展 负载因子 默认为0.75

​ 1 效率低

​ 0.5 浪费空间

扩容为原来的2倍

​ 效率高

​ 减少哈希重冲突

源码:

  1. 常用变量

    //默认的初始化容量为16,必须是2的n次幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
    //最大容量为 2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    //默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
    //为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。
    //若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
    //若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
    static final int TREEIFY_THRESHOLD = 8;
    
    //当红黑树上的元素个数,减少到6个时,就退化为链表
    static final int UNTREEIFY_THRESHOLD = 6;
    
    //链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
    //这是为了避免,数组扩容和树化阈值之间的冲突。
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    //存放所有Node节点的数组
    transient Node<K,V>[] table;
    
    //存放所有的键值对
    transient Set<Map.Entry<K,V>> entrySet;
    
    //map中的实际键值对个数,即数组中元素个数
    transient int size;
    
    //每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
    //当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
    transient int modCount;
    
    //数组扩容阈值
    int threshold;
    
    //加载因子
    final float loadFactor;					
    
    //普通单向链表节点类
    static class Node<K,V> implements Map.Entry<K,V> {
    	//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
    	final int hash;
    	final K key;
    	V value;
    	//指向单链表的下一个节点
    	Node<K,V> next;
    
    	Node(int hash, K key, V value, Node<K,V> next) {
    		this.hash = hash;
    		this.key = key;
    		this.value = value;
    		this.next = next;
    	}
    }
    
    //转化为红黑树的节点类
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    	//当前节点的父节点
    	TreeNode<K,V> parent;  
    	//左孩子节点
    	TreeNode<K,V> left;
    	//右孩子节点
    	TreeNode<K,V> right;
    	//指向前一个节点
    	TreeNode<K,V> prev;    // needed to unlink next upon deletion
    	//当前节点是红色或者黑色的标识
    	boolean red;
    	TreeNode(int hash, K key, V val, Node<K,V> next) {
    		super(hash, key, val, next);
    	}
    }	
    
    
  2. 构造函数

    //默认无参构造,指定一个默认的加载因子
    public HashMap() {
    	this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    
    //可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说
    public HashMap(int initialCapacity) {
    	//同样使用默认加载因子
    	this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子
    public HashMap(int initialCapacity, float loadFactor) {
    	if (initialCapacity < 0)
    		throw new IllegalArgumentException("Illegal initial capacity: " +
    										   initialCapacity);
    	if (initialCapacity > MAXIMUM_CAPACITY)
    		initialCapacity = MAXIMUM_CAPACITY;
    	if (loadFactor <= 0 || Float.isNaN(loadFactor))
    		throw new IllegalArgumentException("Illegal load factor: " +
    										   loadFactor);
    	this.loadFactor = loadFactor;
    	//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
    	//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
    	//先卖个关子,等到 resize 的时候再说
    	this.threshold = tableSizeFor(initialCapacity);
    }
    
    //可传入一个已有的map
    public HashMap(Map<? extends K, ? extends V> m) {
    	this.loadFactor = DEFAULT_LOAD_FACTOR;
    	putMapEntries(m, false);
    }
    
    //把传入的map里边的元素都加载到当前map
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    	int s = m.size();
    	if (s > 0) {
    		if (table == null) { // pre-size
    			float ft = ((float)s / loadFactor) + 1.0F;
    			int t = ((ft < (float)MAXIMUM_CAPACITY) ?
    					 (int)ft : MAXIMUM_CAPACITY);
    			if (t > threshold)
    				threshold = tableSizeFor(t);
    		}
    		else if (s > threshold)
    			resize();
    		for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    			K key = e.getKey();
    			V value = e.getValue();
    			//put方法的具体实现,后边讲
    			putVal(hash(key), key, value, false, evict);
    		}
    	}
    }
    
    
  3. hash()计算

    static final int hash(Object key) {
    	int h;
    	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    
    //h原来的值
    0110 1101 0110 1111 0110 1110 0010 1000
    //无符号右移16位,其实相当于把低位16位舍去,只保留高16位
    0000 0000 0000 0000 0110 1101 0110 1111
    //然后高16位和原 h进行异或运算
    0110 1101 0110 1111 0110 1110 0010 1000
    ^
    0000 0000 0000 0000 0110 1101 0110 1111
    =
    0110 1101 0110 1111 0000 0011 0100 0111
    
    

    高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。

    为什么高低位异或运算可以减少哈希碰撞

    //例如我有另外一个h2,和原来的 h相比较,高16位有很大的不同,但是低16位相似度很高,甚至相同的话。
    //原h值
    0110 1101 0110 1111 0110 1110 0010 1000
    //另外一个h2值
    0100 0101 1110 1011 0110 0110 0010 1000
    // n -1 ,即 15 的二进制
    0000 0000 0000 0000 0000 0000 0000 1111
    //可以发现 h2 和 h 的高位不相同,但是低位相似度非常高。
    //他们分别和 n -1 进行与运算时,得到的结果却是相同的。(此处n假设为16)
    //因为 n-1 的高16位都是0,不管 h 的高 16 位是什么,与运算之后,都不影响最终结果,高位一定全是 0
    //因此,哈希碰撞的概率就大大增加了,并且 h 的高16 位特征全都丢失了。
    
    

    与运算,结果会趋向于0;或运算,结果会趋向于1;而只有异或运算,0和1的比例可以达到1:1的平衡状态。

HashTable结构

线程安全(所有public方法声明中都有synchronized)

不允许null(key和value都不允许)

无序

Hashtable的父类是Dictionary

扩容机制:默认初始容量11,扩容加载因子0.75,当超出默认长度(int)(11*0.75)=8时,扩容为oldx2+1,新容量为原容量的2倍+1

哈希值的使用,HashTable直接使用对象的hashCode。

红黑树

一个二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此,红黑树是一种弱平衡二叉树(所以在相同节点情况下,AVL树的高度低于红黑树),相对与要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,就用红黑树。

  1. 每个节点非红即黑。
  2. 根节点是黑的。
  3. 每个叶节点(树尾端NULL指针或NULL节点)都是黑的。
  4. 如果一个节点是红的,那么它的两个儿子都是黑的。
  5. 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点。
  6. 每条路径都包含相同的黑节点

(3)应用

1,广泛用于C ++的STL中,地图和集都是用红黑树实现的;

2,着名的Linux的的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间;

3,IO多路复用的epoll的的的实现采用红黑树组织管理的的的sockfd,以支持快速的增删改查;

4,Nginx的的的中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器;

5,Java的的的中TreeMap中的中的实现;

AVL树

AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右字数树高不超过1,和红黑相比,AVL树是严格的平衡二叉树,平衡条件必须满足(所有的节点的左右字数高度差的绝对值不超过1)。不管我们是执行插入还是删除操作,只要不满足条件,就要通过旋转来保持平衡,而旋转是非常耗时的,所以AVL树适合用于插入与删除次数比较少,但查找多的情况。

二叉平衡树有以下规则:

  • 每个节点最多只有两个子节点
  • 每个节点的值比它的左子树所有的节点大,比它的右子树所有节点小(有序)
  • 每个节点左子树的高度与右子树高度之差的绝对值不超过1

hashmap和hashtable区别

相同点:

hashmap和Hashtable都实现了map、Cloneable(可克隆)、Serializable(可序列化)这三个接口
不同点:

  1. 底层数据结构不同:jdk1.7底层都是数组+链表,但jdk1.8 HashMap加入了红黑树
  2. Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。
  3. 添加key-value的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法,而HashTable是直接采用key的hashCode()
  4. 实现方式不同:Hashtable 继承的是 Dictionary类,而 HashMap 继承的是 AbstractMap 类。
  5. 初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
  6. 扩容机制不同:当已用容量>总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 +1。
  7. 支持的遍历种类不同:HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration两种方式遍历
  8. 迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。而Hashtable 则不会。
  9. 部分API不同:HashMap不支持contains(Object value)方法,没有重写toString()方法,而HashTable支持contains(Object value)方法,而且重写了toString()方法
  10. 同步性不同: Hashtable是同步(synchronized)的,适用于多线程环境,
    而hashmap不是同步的,适用于单线程环境。多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

concurrenthashmap:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。只能保证单个方法是同步的,不能保证先读后写的原子性。其余类似hashmap

jdk5使用分段锁,JDK8放弃了分段锁采用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁

对于多线程的操作,介于HashMap与HashTable之间

弃用分段锁原因

  • 浪费内存空间
  • 生产环境中,map再放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  • 提高GC效率

**操作:**put时首先通过hash找到对应链表后,查看是否是第一个Node,如果是,直接用CAS原则插入,无需加锁。如果不是链表的第一个Node,则直接用链表第一个Node加锁,这里加的锁是synchronized。

TreeSet TreeMap 底层是红黑树结构 根据内容的自然顺序排序

set遍历

不能用for循环遍历,

map遍历

keySet

EntrySet

Collections类 :collection工具类

泛型

类型参数化

IO 延伸到操作系统 NIO

File类

​ 一个File的对象表示硬盘上的一个文件/目录

createNewFile()

mkdir()

mkdirs()

delete()

判断

流的类型

字节流

输入流,输出流

InputStream OutputStream

字符流

输入流,输出流

节点流

FIleInputStream FileOutputStream 直接来操作文件 read() read(byte[] b)

处理流

BufferedInputStream 缓存功能 提示效率

对象序列化,反序列化

对象的寿命通常随着生成该对象的程序的终止而终止。
有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。
对象的输出流将指定的对象写入到文件的过程,就是将对象序列化的过程,对象的输入流将指定序列化好的文件读出来的过程,就是对象反序列化的过程。既然对象的输出流将对象写入到文件中称之为对象的序列化,所以必须要实现Serializable接口。

 Serializable接口中没有任何方法。当一个类声明实现Serializable接口后,表明该类可被序列化。

 在类中可以生成一个编号
 private static final long serialVersionUID = -5974713180104013488L; 随机生成 唯一的
 可以显示的声明创建序列号id,如果实现接口不显式的生成序列化版本号,类的信息一旦改变,序列化id也会随之改变。
 transient 所修饰的属性不被序列化到文件中。
 serialVersionUID 用来表明实现序列化类的不同版本间的兼容性。某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确反序列化

ObjectInputStream ObjectOutputStream 反序列化 一种创建对象方式 深克隆

​ tomcat 缓存session对象 sessions.ser

线程

最近最久未使用lru

如果一个数据在最近一段时间没有被访问到, 那么在讲台它被访问到的概率也会比较小, 所以当内存空间有限时, 应该把这部分数据淘汰掉。

线程,进程名词解释,关系

程序:是一段静态的代码。

进程:是正在执行的程序,是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。一个进程中可以启动多个线程。

线程:进程中的一个执行流程,是进程内部最小执行单元,没有主机的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

线程包含:

  • 一个指向当前被执行指令的指令指针
  • 一个栈
  • 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值。
  • 一个私有的数据区

如何创建线程

  • 继承Thread类的方式

    线程代码存放Thread子类run方法中(没有返回值)

  • 实现Runnable接口方式

    线程代码存在接口的子类run方法中(重写run方法)(没有返回值)

    **好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源

  • 实现Callable接口方式

    与Runnable相比:

    • run方法可以有返回值。
    • 方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,获取返回值

状态

  1. 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时具备了运行的条件,只是没有分配到CPU资源
  3. 运行:当就绪的线程被调度并获取CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
  4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  5. 死亡:线程完成了它的全部工作或线程被提前强制性中止或出现异常导致结束。

多线程优缺点

优点:

  • 提高程序的响应
  • 提高CPU的利用率
  • 改善程序结构,将复杂任务分为多个线程,独立运行

缺点:

  • 需要占用内存
  • 多线程需要协调管理,需要CPU时间跟踪线程
  • 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源问题

如何解决线程安全问题:并发编程

并发:多个任务在同一个CPU核上,按细分时间片轮流执行,从逻辑上来看是同时执行的。在单核CPU下,线程实际还是串行执行的。

并行:单位时间内,多个处理器或多核处理器同时处理多个任务。

并发编程的核心问题:
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。

    为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会即使将数据刷新到主内存中。缓存不能及时刷新导致了可见性问题。

  • 原子性:一个或多个操作在cpu执行的过程中不被中断的特性,称为原子性。

    原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。

    线程切换导致了原子性问题

  • 有序性:程序按照代码的先后顺序执行。

    编译器为了优化性能,有时候会改变程序中语句的先后顺序。

总结: 缓存导致的可见性问题线程切换带来的原子性问题编译优化带来的有序性问题

如何保证原子性?
  • synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个 资源时,发现锁还未释放,所以只能在外面等待。

    synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一定能保证原子操作.

    synchronized 也能够保证可见性和有序性。

  • JUC-原子变量

    属于java.util.concurrent.atomic

    原子类的原子性是通过volatile+CAS实现原子操作的。

    AtomicInteger类中的value是又volatile关键字修饰的,保证了value的内存可见性。

    低并发情况下:使用AtomicInteger

CAS

cas:比较并交换,该算法是硬件对并发操作的支持。

cas是乐观锁的一种实现方式.·,是一种轻量级的锁机制。

过程:每次判断我的预期值和内存中的值是否相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到最新值作为预期值,重新判断。而该线程不断的循环判断是否内存值已经被其他线程更新过了,这就是自旋的思想。

优点:效率高于加锁

缺点

  • 不断的自旋,会导致cpu消耗。

  • ABA问题:某个线程将内存值由A改为了B,再由B改为了A。当另一个线程使用预估值去判断时,预期值于内存值相同,误以为该变量没有被修改过而导致的问题。

    解决方法:通过使用类似于添加版本号的方式。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。

死锁,如何避免死锁.

死锁根本原因:
是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
一个线程T1持有锁L1,并且申请获得锁L2;而另一个线程T2持有锁L2,并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。

java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

既然我们知道了产生死锁可能性的原因,那么就可以在编码时进行规避。
如何避免
1、避免嵌套锁
2、只锁需要的部分
3、避免无限期等待

守护线程

java中的线程分为两类:用户线程和守护线程

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作。只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

守护线程的作用是为其他线程的运行是提供便利服务,守护线程最典型的应用就是GC(垃圾回收器)

注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个IllegalThreadStateException异常。

线程间通信

线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题.

不同线程之间无法直接访问对方工作内存中的变量,线程间通信有两种方式:共享内存、消息传递、IPC通信、线程上下文、socket。java采用的是共享内存。

  • 使用volatile关键字

    一旦一个共享变量被volatile修饰后:

    1. 保证了不同线程对这个变量进行操作时的可见性,及一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    2. 禁止进行指令重排序。
    3. volatile不能保证对变量操作的原子性。
  • 使用Object类的wait() 和 notify() 方法

  • 使用JUC工具类 CountDownLatch

  • 使用 ReentrantLock 结合 Condition

  • 基本LockSupport实现线程间的阻塞和唤醒

线程间的通信方式: wait/notify机制

线程面试题

join方法

join()方法是Thread类中的一个方法,该方法的定义是等待该线程终止。其实就是join()方法将挂起调用线程的执行,直到被调用的对象完成它的执行。

/**
 *等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。
 *millis - 以毫秒为单位的等待时间。
 */
public final synchronized void join(long millis) 
throws InterruptedException {
	//获取启动时的时间戳,用于计算当前时间
	long base = System.currentTimeMillis();
   	//当前时间
	long now = 0;

    if (millis < 0) {//等待时间不能小于0则抛出IllegalArgumentException
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {//等待时间为0,则无限等待
		//需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false
		//即意味着join方法不会生效
        while (isAlive()) {
            wait(0);
        }
    } else {
		//需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false
		//即意味着join方法不会生效
        while (isAlive()) {
			//计算剩余的等待时间
            long delay = millis - now;
            if (delay <= 0) {//如果剩余的等待时间小于等于0,则终止等待
                break;
            }
			//等待指定时间
            wait(delay);
			//获取当前时间
            now = System.currentTimeMillis() - base;
        }
    }
}

从源码中可以得知,如果要join正常生效,调用join方法的对象必须已经调用了start()方法且并未进入终止状态。

sleep()方法和wait()方法区别
  • sleep属于Thread类,wait属于Object类
  • sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁
  • 它们都可以被interrupted方法中断。
  • sleep方法导致了程序暂停执行指定的时间,让出cpu,但是它的监控状态依然保持,当指定时间到了又会自动恢复运行状态;调用wait方法会使线程进入阻塞状态,直到notify方法唤醒线程后才会进入就绪。
  • wait是实例方法,而sleep是静态方法
  • wait只能在同步上下文中调用,否则抛出异常;而sleep可以在任何地方使用
Thread和Runnable的关系,区别
  • 继承Thread类的方式

    线程代码存放Thread子类run方法中(没有返回值)

  • 实现Runnable接口方式

    线程代码存在接口的子类run方法中(重写run方法)(没有返回值)

    **好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源

锁优化,怎么优化?
  • 减少锁持有时间
  • 减小锁粒度(大对象拆成小对象)
  • 锁分离(读写分离)
  • 锁粗化(很多次请求的锁拿到一个锁里面)
  • 锁消除(在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作)
锁状态

jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

锁的状态:锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 无锁状态
  • 偏向锁状态:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。为了减少同一线程获取锁的代价而引入。省去了申请锁的操作。
  • 轻量级锁状态:当锁是偏向锁的时候,被另一个线程锁访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋转的形式获取锁,不会阻塞,提高性能。
  • 重量级锁:当锁为轻量级锁时,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁:当线程抢锁失败后,重试几次

锁分类

锁的分类并不是全指锁的状态,有的指锁的特性,有的指锁的设计。

  • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • **公平锁:**将锁分配给排队时间最长的线程;在多核的情况下要维护一个线程等待队列。效率远低于 非公平锁。

  • 非公平锁:不考虑排队时间,抢占式的;(synchronized) ReentrantLock默认是非公平锁,但是底层通过****AQS来实现线程调度,可以使其变成公平锁**。

  • **可重入锁(递归锁):**同一个线程在外层方法获取锁时,在进入内层方法时会自动获取锁。

    • ReentrantLock与synchronized都是可重入锁
    • 优点:一定程度上避免死锁读写锁****(ReadWritedLock):是一个新类
    • 多个读者可以同时进行读
    • 写者必须互斥(读者写者不能同时进行)
    • 写者优先于读者
  • **分段锁:**是一种思想。将数据分段,在每个分段上加锁,把锁细粒度化,以提高并发效率。

  • 自旋锁**(SpinLock):**是一种思想。指自己重试,当抢锁失败后,重试几次,抢到了就继续,抢不 到就阻塞线程。**目的还是为了尽量不阻塞线程。

    • 适用场景:加锁时间较短的场景
    • 缺点:比较消耗cpu
  • **共享锁:**该锁可被多个线程所持有,并访问共享资源。(读锁)

  • **独占锁(互斥锁):**一次只能被一个线程持有

    ReentrantLock,synchronized都是独占锁;对于ReadWriteLock来说,其读锁是共享锁,其写锁是独占锁。

synchronized和ReentrantLock(Lock的实现类)

共同点:

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

不同点:

  1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
  2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的
    不可用性提供了更高的灵活性
  3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
  4. ReentrantLock 可以实现公平锁
  5. ReentrantLock 通过 Condition 可以绑定多个条件
  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻
    塞,采用的是乐观并发策略
  7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言
    实现。
  8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
    而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,
    因此使用 Lock 时需要在 finally 块中释放锁。
  9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,
    等待的线程会一直等待下去,不能够响应中断。
  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等

AtomicInteger底层实现原理是什么? 包证原子性

AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS技术。

CAS+volatile 可见性,有序性

比较并交换 无锁实现 乐观锁 自旋锁

ThreadLocal的底层原理

线程封闭

对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。

ThreadLocal线程封闭:简单易用

使用ThreadLocal来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。所以说ThreadLocal是用来解决变量共享的并发安全问题,是不精确的。

概述

ThreadLocal叫做线程变量,在ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

原理分析

ThreadLocal是一个泛型类,保证可以接受任何类型的对象。

因为一个线程内可以存在多个ThreadLocal对象,所以其实是ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法。

createMap方法

在这里插入图片描述

ThreadLocalMap是个静态的内部类

在这里插入图片描述

set方法

在这里插入图片描述

get方法

在这里插入图片描述

最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。

对象引用

强引用(默认):是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。造成Java内存泄漏的主要原因之一。
软引用:内存不足即回收。在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用:发现即回收。被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用:对象回收跟踪。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

内存泄漏问题

在这里插入图片描述

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value

永远无法回收,造成内存泄漏。

解决方法:
  • key使用强引用

    当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

  • key使用弱引用

    当ThreadLocalMap的key为弱引用回收ThredaLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

ThreadLocal正确的使用方法

每次使用完ThreadLocal都调用它的remove()方法清除数据。

synchronized 和 volatile 的区别是什么?

volatile关键字的作用
  • 保证了不同线程对这个变量进行操作时的可见性,及一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    为什么?

    比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。

  • 禁止进行指令重排序(有序性)

  • volatile不能保证对变量操作的原子性

synchronized作用

synchronized 有三种方式来加锁,分别是

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。

因为synchronized保证了同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于单线程操作了,那么线程的可见性、原子性、有序性它都能保证了。

区别
  • volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广
  • volatile只能保证可见性和有序性,不能保证原子性。而synchronized都可以保证
  • volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

synchronized底层

Java对象头和monitor是实现synchronized的基础。

线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

线程的 run() 和 start() 有什么区别?

系统通过调用线程类的start()方法来启动一个线程、此时该线程处于就绪状态,等待调度,即就是这个线程可以被JVM来调度执行。在调度过程中,JVM底层通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。

如果直接调用线程类的run()方法,此时run()方法仅仅被当作一个普通的函数调用,程序中任然只有主线程这一个线程

start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的。

start()方法实现了多线程,无需等待run()方法体中的代码执行完毕而直接继续执行后续的代码。

Runnable和Callable的区别 T call()Throws Exception{}

  • 实现Runnable接口方式

    线程代码存在接口的子类run方法中(重写run方法)(没有返回值)

    **好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源

  • 实现Callable接口方式

    与Runnable相比:

    • run方法可以有返回值。
    • 方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,获取返回值

什么是CAS

cas:比较并交换,该算法是硬件对并发操作的支持。

cas是乐观锁的一种实现方式,是一种轻量级的锁机制。

过程:每次判断我的预期值和内存中的值是否相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到最新值作为预期值,重新判断。而该线程不断的循环判断是否内存值已经被其他线程更新过了,这就是自旋的思想。

优点:效率高于加锁

缺点

  • 不断的自旋,会导致cpu消耗。

  • ABA问题:某个线程将内存值由A改为了B,再由B改为了A。当另一个线程使用预估值去判断时,预期值于内存值相同,误以为该变量没有被修改过而导致的问题。

    解决方法:通过使用类似于添加版本号的方式。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。

什么是AQS

抽象的队列式的同步器。

**核心思想:**如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时所分配的机制,这个机制AQS是用CLH队列锁(CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理)实现的,即将暂时获取不到锁的线程加入到队列中。

线程池

由来:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

解决方法:在Java中可以通过线程池来达到这样的效果。线程池里的每个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

ThreadPoolExector类

java.util.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。

ThreadPoolExecutor继承了AbstarctExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用第四个构造器进行的初始化工作。

构造器中各个参数的含义:
  • corePoolSize:核心池的大小。在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程区执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法预创建线程,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
  • maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数大于corePoolSize时,如果一个线程空闲时间到达keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7中静态属性:
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响。
  • threadFactory:线程工厂,主要用来创建线程。
  • handler:表示当拒绝处理任务时的策略。
  • Executors 目前提供了 5 种不同的
    线程池创建配置:
    1)newCachedThreadPool():用来处理大量短时间工作任务的线程池。当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;其内部使用 SynchronousQueue 作为工作队列。
    2)newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。
    3)newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态
    4)newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
    5)newWorkStealingPool(int parallelism),Java 8 才加入这个创建方法,并行地处理任务,不保证处理顺序。

执行流程:

创建完成ThreadPoolExecutor之后,当向线程池提交任务时,通常使用execute方法。

execute方法的执行流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvZPqlxY-1634353769848)(D:\桌面\tu\1629514662034.png)]

如果线程池中存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
如果线程池核心线程数已满,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
当线程池里面存活的线程数已经等于corePoolSize且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,如果没到达,创建一个非核心线程执行提交的任务。
如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
execute与submit的区别

执行任务除了可以使用execute方法还可以使用submit方法。

区别:

  • execute适用于不需要关注返回值的场景。
  • submit方法适用于需要关注返回值的场景。

阿里编程规约建议使用ThreadPoolExecutor类,是最原始的线程池创建.

4种拒绝策略

  1. AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
  2. CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
  3. DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
  4. DiscardPolicy 策略:该策略默默的丢弃无法处理的任务,不予任何处理。

关闭线程池
两个方法实现:

shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

设置合理的线程池大小

N代表CPU的个数

  1. CPU密集型应用,线程池大小设置位N+1
  2. IO密集型应用,线程池大小设置位2N
  3. 利特尔法则,公式线程池大小 = ((线程 IO time + 线程 CPU time )/线程 CPU time )* CPU数目
    • 一个请求所消耗的时间 (线程 IO time + 线程 CPU time)
    • 该请求计算时间(线程 CPU time)
    • CPU数目

java反射

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

Apple apple = new Apple(); //直接初始化,「正」
apple.setPrice(4);

上面这样子进行类对象的初始化,我们可以理解为「正」。

而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

API:

ClassClass clz = Class.forName("com.ff.api.Apple");

Constructor appleConstructor = clz.getConstructor();

Object appleObj = appleConstructor.newInstance();

Method setPriceMethod = clz.getMethod("setPrice", int.class);
       setPriceMethod.invoke(appleObj, 14);

Field field = clz.getField("price");

boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型
boolean isArray = class1.isArray();//判断是否是集合类
boolean isAnnotation = class1.isAnnotation();//判断是否是注解类
boolean isInterface = class1.isInterface();//判断是否是接口类
boolean isEnum = class1.isEnum();//判断是否是枚举类
String simpleName = class1.getSimpleName();//获取class类名
int modifiers = class1.getModifiers();//获取class访问权限
Class<?>[] declaredClasses = class1.getDeclaredClasses();//内部类
Class<?> declaringClass = class1.getDeclaringClass();//外部类
Annotation[] annotations = class1.getAnnotations();//获取class对象的所有注解
Class parentClass = class1.getSuperclass();//获取class对象的父类
Class<?>[] interfaceClasses = class1.getInterfaces();//获取class对象的所有接口

反射机制的应用场景

  • 逆向代码 ,例如反编译
  • 与注解相结合的框架
  • 根据类动态生成对象

反射机制的优缺点

优点:

​ 运行期类型的判断,动态类加载,动态代理使用反射。 代理对象

缺点:

​ 性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。

注解

java注解是jdk5.0引入的一种注释机制。

Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机标注内容,在运行时可以获取到标注内容。当然它也支持自定义Java标注。

内置的注解
  • @Override

    检查该方法是否可以重写方法。如果发现其父类,或者是引用的接口并没有该方法时,会报编译错误。

  • @Deprecated

    标记过时方法。如果使用该方法,会报编译警告。

  • @SuppressWarnings

    指示编译器去忽略注解中声明的警告。

作用在其他注解的注解(元注解)是:

  • @Retention(重点

    标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者在运行时可以通过反射访问。

    定义了该注解被保留的时间长短:某些注解仅出现在源代码中,而被编译器丢弃;而另一些在class被编译在class文件中;编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。

    使用这个meta-Annotation可以对注解的”生命周期“限制。

    作用:标识需要在什么级别保存该注释信息,用于描述注解的声明周期。

    取值:

    • SOURCE:在源文件中有效(即源文件保留)
    • CLASS:在class文件中有效(即class保留)
    • RUNTIME:在运行时有效(即运行时保留)
  • @Documented

    标记这些注解是否包含在用户文档中。

  • @Target(重点

    用于描述注解的使用范围。

    ElementType.TYPE可以应用于类的任何元素。

    ElementType.CONSTRUCTOR可以应用于构造函数。

    ElementType.FLELD可以应用于字段或属性。

    ElementType.LOCAL_VARIABLE 可以应用于局部变量。

    ElementType.METHOD 可以应用于方法级注释。

    ElementType.PACKAGE 可以应用于包声明。

    ElementType.PARAMETER 可以应用于方法的参数。

  • @Inherited

    标记这个注解是继承于哪个注解类(默认注解没有继承于任何子类)

  • @SafeVarargs

    Java7开始支持,忽略任何使用参数未泛型变量的方法或构造函数调用生产的警告。

  • @Repeatable

    java8开始支持,标识某注解可以在同一个声明上使用多次。

自定义注释
  1. 创建一个Annotaion

    public @interface NotNull { 
        String message() default ""; 
    }
    
  2. 使用元注释修饰

    @Target(ElementType.FIELD) 
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface NotNull { 
        String message() default ""; 
    }
    
  3. 使用注释

    @NotNull 
    private String name;
    
  4. 测试

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, Exception { 
        User user = new User(); 
        //user.setName("jim");
        Field[] fields = user.getClass().getDeclaredFields(); 
        for (Field field : fields) { 
            NotNull notNull = field.getAnnotation(NotNull.class); 
            if (notNull != null) { 
                Method m = user.getClass().getMethod("get" + getMethodName(field.getName())); 
                Object obj=m.invoke(user); 
                if (obj==null) { 
                    System.err.println(field.getName() +notNull.message());
                    throw new NullPointerException(notNull.message()); 
                } 
            } 
        } 
    } 
    /** 
    * 把一个字符串的第一个字母大写
    */ 
    private static String getMethodName(String fildeName) throws Exception{
        byte[] items = fildeName.getBytes(); 
        items[0] = (byte) ((char) items[0] - 'a' + 'A'); 
        return new String(items);
    }
    

面向对象设计原则

设计原则总结目的
开闭原则对扩展开放,对修改关闭降低维护带来的新风险
依赖倒置原则高层不应该依赖底层,要面向接口编程便于理解,提高代码的可读性
单一职责原则一个类只干一件事,实现类要单一便于理解,提高代码的可读性
接口隔离原则一个接口只干一件事,接口要精简单一功能解耦,高聚合,低耦合
迪米特法则不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度只是和朋友说话,不和陌生人说话,减少代码的臃肿
里氏替换原则不要破环继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义防止继承泛滥
合成复用原则尽量使用组合或者聚合关系实现代码复用,少使用继承降低代码耦合

设计模式

概念:设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。

目的:为了提高代码的可重用性、代码的可读性和代码的可靠性。

本质上:面向对象设计原子弹实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

优势

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

Java设计模式类型

根据模式是用来完成什么工作来划分:

创建型模式:用于描述“怎样创建对象”,它的主要特点是”将对象的创建与使用分离“。提供了单例、原型、工厂方法、抽象工厂、建造者 5 种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,提供了代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎么样分配职责。提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器 11 种行为型模式。

常用设计模式
单例模式(创新型模式)

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

特点

  • 单例类只有一个实例对象。
  • 该单例对象必须由单例类自行创建。
  • 单例类对外提供一个访问该单例的全局访问点。

两种实现模式

  • 懒汉式单例

    特点:类加载时没有生成单例,只有当第一次调用getInstance方法时才去创建这个单例。

    public class Singleton{
        private static Singleton instance = null;
        private Singleton(){}
        public static synchronized Singleton getInstance(){
             if(instance == null){
                 instance = new Singleton();
             }
             return instance;
        }
    }
    
  • 饿汉式单例

    特点:类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在了。

    public class Singleton{
       private final static Singleton instance = new Singleton();
       private Singleton(){}
       public static Singleton getInstance(){
           return instance;
       }
    }
    
工厂模式(创新型模式)

**定义:**定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。

按照实际业务场景划分,工厂模式有3中不同的实现方式,分别是简单工厂模式、 工厂方法模式和抽象工厂模式

  • 简单工厂

    我们把创建的对象称为”产品“,把创建产品的对象称为”工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式“

    简单工厂模式中创建实例的方法通常为静态方法。

    简单工厂模式的主要角色如下:

    • 简单工厂:核心。负责实现创建所有的实例的内部逻辑。工厂类的创建产品类的方法可用被外界直接调用,创建所需的产品对象。
    • 抽象产品:是简单工厂创建的所有对象的父类。负责描述所有实例共有的公共接口。
    • 具体产品:是创建目标。
代理模式(结构型模式)

特征:代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等.

概念:我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度

结构:

  • 抽象主题类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

代理实现可以分为静态代理和动态代理。

  • 静态代理

    静态代理模式的特点,代理类接受一个Subject接口的对象,任何实现该接口的对象,都可以通过代理类进行代理,增加了通用性。但是也有缺点,每一个代理类都必须实现一遍委托类的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托类对象非常多,则静态代理类就非常臃肿,难以胜任。

  • 动态代理

    动态代理中,代理类并不是在java代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为JDK动态代理和cglib动态代理。

    • jdk代理

      jdk动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。

      步骤

      1. 编写一个委托类的接口。即静态代理的。
      2. 实现一个真正的委托类,即静态代理的。
      3. 创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法。
      4. 在测试类中,生成动态代理的对象。
    • Cglib代理

      jdk实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现SpringAOP的基础。

    总结:

    • cglib创建的动态代理对象比jdk创建的动态代理使用的是Java反射技术实现,生成类的过程比较高效,对象的性能更高,但是cglib创建代理对象时所花费的时间比jdk多。所以对于单例的对象,因为无需频繁创建对象,用cglib合适,反之使用jdk方式更为合适。
    • jdk动态代理只能对实现了接口的类生成代理
装饰
观察者
建造者

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

建造者模式中有以下几个角色:

  • 产品product:表示被构造的复杂对象
  • 抽象建造者Builder:为创建一个产品对象的各个部件指定抽象接口
  • 具体建造者ConcreteBuilder:实现Builder的接口,构造和装配该产品的各个部件。
  • 指导者Director:构造一个使用Builder接口的对象,它复杂控制产品对象的生产过程,隔离了客户与对象的生产过程

建造者模式在使用过程中可以演化出多种形式,如

  1. 省略抽象建造者角色
  2. 省略指导者角色

java.lang.StringBuilder的append方法使用的就是建造者模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBRyTl4V-1634353769851)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631806135814.png)]

import lombok.Data;

// 产品
@Data
class Product {
    String inner;
    String outer;
}

// 抽象建造者:抽象出建造过程
interface Builder {
    void buildInner();

    void buildOuter();

    Product buildProduct();
}

// 具体建造者:不同产品,创建不同的具体建造者
class ConcreteBuilder implements Builder {
    Product product;

    public ConcreteBuilder(Product product) {
        this.product = product;
    }

    @Override
    public void buildInner() {
        product.inner = "建造产品内部";
    }

    @Override
    public void buildOuter() {
        product.outer = "建造产品外部";
    }

    @Override
    public Product buildProduct() {
        return product;
    }
}

// 指导者:根据建造过程组装产品
class Director {
    public Product constructProduct(Builder builder) {
        builder.buildInner();
        builder.buildOuter();
        return builder.buildProduct();
    }
}

public class BuilderClient {
    public static void main(String[] args) {
        ConcreteBuilder concreteBuilder = new ConcreteBuilder(new Product());
        Director director = new Director();
        Product product = director.constructProduct(concreteBuilder);
        // Product(inner=建造产品内部, outer=建造产品外部)
        System.out.println(product);
    }
}

网络

OSI 的七层模型都有哪些?

物理层:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。
数据链路层:接收来自物理层的位流形式的数据,并封装成帧,传送到上一层
网络层:将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径。
传输层:在源端与目的端之间提供可靠的透明数据传输
会话层:负责在网络中的两节点之间建立、维持和终止通信
表示层:处理用户信息的表示问题,数据的编码,压缩和解压缩,数据的加密和解密
应用层:为用户的应用进程提供网络通信服务

img

img

img

TCP与UDP区别

Socket ServerSocket

数据报 ip+端口+数据

TCP特点

  • TCP是面向连接的
  • 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的
  • TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达
  • TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据。
  • 面向字节流。TCP 中的“流”(Stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。

UDP特点:

  • UDP是无连接的
  • UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态
  • UDP是面向报文的
  • UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低
  • UDP支持一对一、一对多、多对一和多对多的交互通信
  • UDP的首部开销小,只有8个字节,比TCP的20个字节的首部要短。
TCPUDP
是否面向连接面向连接无连接
传输可靠性可靠不可靠
传输形式字节流数据报文段
传输效率
所需资源
应用场景要求通信数据可靠,如:文件传输、邮件传输要求通信速度高,如:域名转换、直播
首部字节20-608

什么是三次握手?

TCP建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个TCP报文段。

最初客户端和服务端都处于CLOSED(关闭)状态。

一开始,服务器端的TCP服务器进程首先创建传输控制块TCB,准备接收客户端进程的连接请求。然后服务端进程就处于LISTEN(监听)状态,等待客户端的连接请求。如有,立即作出响应。

  • 第一次握手:客户端的TCP客户端进程也是首先创建传输控制块TCB。然后,再打算建立TCP连接时,向服务器发出连接请求报文段SYN,并指明客户端的初始化序列号ISN。此时客户端处于SYN_SENT(请求连接)状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

  • 第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN。同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_RCVD(同步收到)状态

    在确认报文段中设置SYN=1,ACK=1,确认号ACK=x+1,初始化序号seq=y

  • 第三次握手:客户端收到SYN报文后,会发送一个ACK报文,当然,也是一样把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISHED (已建立连接)状态。服务器收到了ACK报文之后,也处于ESTABLISHED (已建立连接)状态。

    确认报文段ACK=1,确认号ACK=y+1,序号seq=x+1,ACK报文段可以携带数据,不携带数据则不消耗序号。

为什么不能两次?

客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

注意:第三次握手可以携带数据

什么是四次挥手?

TCP连接的拆除需要发送四个包,因此称为四次挥手,客户端或服务器端均可发起挥手动作。

刚开始双方都处于ESTABLISHED (已建立连接)状态,假如是客户端先发起关闭请求。

  • 第一次挥手:客户端发送了一个FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1 状态。

    即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。

  • 第二次挥手:服务器端收到FIN之后,会发送ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT (关闭等待)状态。

    此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务器发出的连接释放报文段。

  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给FIN报文,且指定一个序列号。此时服务端处于LAST_ACK 的状态。

    即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。

  • 第四次挥手:客户端收到FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号值+1作为自己的ACK报文的序列号值,此时客户端处于TIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态

    即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

TCP如何保证有序传输?

TCP包头中有32位的序号,有序性是通过序号保证的

tcp通过字节编号,每一个数据字节都会有一个编号,比如发送了三包,每个包100字节,假设第一个包首个字节标号是1,那么发送的三包的编号就是1,101,201,三包数据,只有接收端收到连续的序号的包,才会讲数据包提交到应用层例如收到1,201,101,是不会提交到上层应用层的,只有收到正确连续顺序才会提交,所以就保证了数据的有序性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U9cnhSlg-1634353769857)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630205725872.png)]

TCP如何保证流量控制传输?

通过滑动窗口来解决

发送端根据自己的实际情况发送数据。但是,接收端可能收到的是一个毫无关系的数据包又可能会在处理其他问题上花费一些时间。因此在为这个数据包做其他处理时会耗费一些时间,甚至在高负荷的情况下无法接收任何数据。如此一来,如果接收端将本应该接收的数据丢弃的话,就又会触发重发机制,从而导致网络流量的无端浪费。

  • 流量控制 是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止丢失数据包的。

TCP协议里窗口机制有2种:一种是固定的窗口大小;一种是滑动的窗口。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

TCP如何保证拥塞控制传输?

拥塞控制是通过拥塞窗口控制的

  • 拥塞控制 拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况

有了TCP的滑动窗口控制,收发主机之间即使不再以一个段为单位,而是以一个窗口为单位发送确认应答信号,所以发送主机够连续发送大量数据包。然而,如果在通信刚开始的时候就发送大量的数据包,也有可能会导致网络的瘫痪。

在拥塞控制中,发送方维持一个叫做拥塞窗口cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。

拥塞控制使用了两个重要的算法:慢启动算法拥塞避免算法

  • 慢启动算法

    慢启动算法的思路,不要一开始就发送大量的数据,先试探一下网络的拥塞程度,也就是说由小到大逐渐增加窗口的大小。慢算法中,每个传输轮次后将cwnd加倍。指数增长

  • 拥塞避免算法

    拥塞避免算法也是逐渐增大的cwnd的大小,只是采用的是线性增长

    具体来说就是每个传输轮次后将cwnd的大小加一,如果发现出现网络拥塞的话就按照上面的方法重新设置ssthresh的大小并从cwnd=1开售那个重新执行慢开始算法。

TCP如何保证可靠传输?

(1)序列号、确认应答、超时重传;数据到达接收方,接收方需要发出一个确认应答,表示收到该数据段,并且确认序列号会说明下次接收的数据序列号。如果发送方迟迟未收到确认应答,那么可能是发送数据丢失,也可能是确认应答丢失,这时发送方会等待一定时间后重传。

(2)窗口控制与高速重发控制/快速重传(重复确认应答);TCP利用窗口控制来提高传输速度,意思是在一个窗口大小内,不一定等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。

(3)拥塞控制;如果窗口定义的很大,发送端连续发送大量的数据,可能会造成网络的拥堵,甚至网络瘫痪。所以TCP为了防止这种情况而进行了拥塞控制。
TCP建立连接和断开连接过程:三次握手、四次挥手。

dns是什么?dns的工作原理

DNS(Domain Name System)域名系统,将主机域名转换为ip地址,属于应用层协议,使用UDP传输。

它作为将[域名]和[IP地址]的一个[分布式数据库],能够使人更方便地访问[互联网].

过程:
总结: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
一、主机向本地域名服务器的查询一般都是采用递归查询。
二、本地域名服务器向根域名服务器的查询的迭代查询。
1)当用户输入域名时,浏览器先检查自己的缓存中是否 这个域名映射的ip地址,有解析结束。
2)若没命中,则检查操作系统缓存(如Windows的hosts)中有没有解析过的结果,有解析结束。
3)若无命中,则请求本地域名服务器解析( LDNS)。
4)若LDNS没有命中就直接跳到根域名服务器请求解析。根域名服务器返回给LDNS一个 主域名服务器地址。
5) 此时LDNS再发送请求给上一步返回的gTLD( 通用顶级域), 接受请求的gTLD查找并返回这个域名对应的Name Server的地址
6) Name Server根据映射关系表找到目标ip,返回给LDNS
7) LDNS缓存这个域名和对应的ip, 把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束.

Cookie和session区别

会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。

  • Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份
  • cookie不安全,别人可以分析存放在本地的cookie并进行cookie欺骗考虑到安全应当使用session
  • 设置cookie时间可以使cookie过期。但是使用session-destory,我们将会销毁会话。
  • session会在一定时间内保存在服务器上。当访问增多,会比较占用服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。Session对象没有存储的数据量限制,其中可以保存更为复杂的数据类型。
  • 最大区别在于生存周期,浏览器页面一关,session就消失了,而cookie是预先设置的生存周期,或永久保存在本地文件。

GET 和 POST 的区别

get : 获取 参数在地址中传输, 大小受限,不安全

post:发送 在请求体 数据大小不限制 安全

  • GET把参数包含在URL中,POST通过request body传递参数。
  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • GET比POST更不安全,因为参数直接暴露在URL上。

TCP/IP

TCP/IP 协议你一定听过,TCP/IP 我们一般称之为协议簇,什么意思呢?就是 TCP/IP 协议簇中不仅仅只有 TCP 协议和 IP 协议,它是一系列网络通信协议的统称。而其中最核心的两个协议就是 TCP / IP 协议,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。

TCP 协议的全称是 Transmission Control Protocol 的缩写,意思是传输控制协议,HTTP 使用 TCP 作为通信协议,这是因为 TCP 是一种可靠的协议,而可靠能保证数据不丢失。

IP 协议的全称是 Internet Protocol 的缩写,它主要解决的是通信双方寻址的问题。IP 协议使用 IP 地址 来标识互联网上的每一台计算机,可以把 IP 地址想象成为你手机的电话号码,你要与他人通话必须先要知道他人的手机号码,计算机网络中信息交换必须先要知道对方的 IP 地址。(关于 TCP 和 IP 更多的讨论我们会在后面详解)

HTTPS和HTTP的区别

1.HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全, HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。

2.https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

什么是SSL ?秘钥

SSL代表安全套接字层。它是一种用于加密和验证应用程序(如浏览器)和Web服务器之间发送的数据的协议。 身份验证 , 加密Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。SSL/TLS协议作用:认证用户和服务,加密数据,维护数据的完整性的应用层协议加密和解密需要两个不同的密钥,故被称为非对称加密;加密和解密都使用同一个密钥的 对称加密。 优点在于加密、解密效率通常比较高HTTPS 是基于非对称加密的, 公钥是公开的,
(1)客户端向服务器端发起SSL连接请求;
(2) 服务器把公钥发送给客户端,并且服务器端保存着唯一的私钥
(3)客户端用公钥对双方通信的对称秘钥进行加密,并发送给服务器端
(4)服务器利用自己唯一的私钥对客户端发来的对称秘钥进行解密,
(5)进行数据传输,服务器和客户端双方用公有的相同的对称秘钥对数据进行加密解密,可以保证在数据收发过程中的安全,即是第三方获得数据包,也无法对其进行加密,解密和篡改。
因为数字签名、摘要是证书防伪非常关键的武器。 “摘要”就是对传输的内容,通过hash算法计算出一段固定长度的串。然后,在通过CA的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名”.

HTTP 请求响应过程

我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index,当我们输入网址并点击回车时,浏览器内部会进行如下操作

DNS服务器会首先进行域名的映射,找到访问www.someSchool.edu所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 www.someSchool.edu 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个套接字与其相连。
HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 someDepartment/home.index 的资源,我们后面会详细讨论 HTTP 请求报文。
HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。
HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。
HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。
检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。
至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的请求-响应全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。

一次完整的HTTP请求过程

域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户。

详解 HTTP 报文

HTTP 协议主要由三大部分组成:

起始行(start line):描述请求或响应的基本信息;
头部字段(header):使用 key-value 形式更详细地说明报文;
消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
其中起始行和头部字段并成为 请求头 或者 响应头,统称为 Header;消息正文也叫做实体,称为 body。HTTP 协议规定每次发送的报文必须要有 Header,但是可以没有 body,也就是说头信息是必须的,实体信息可以没有。而且在 header 和 body 之间必须要有一个空行(CRLF),如果用一幅图来表示一下的话,我觉得应该是下面这样

如图,这是 http://www.someSchool.edu/someDepartment/home.index 请求的请求头,通过观察这个 HTTP 报文我们就能够学到很多东西,首先,我们看到报文是用普通 ASCII 文本书写的,这样保证人能够可以看懂。然后,我们可以看到每一行和下一行之间都会有换行,而且最后一行(请求头部后)再加上一个回车换行符。

每个报文的起始行都是由三个字段组成:方法、URL 字段和 HTTP 版本字段。

session 的工作原理?

其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。

http长连接和短连接的区别

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。而从HTTP/1.1起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。

什么是CSRF攻击

CSRF(Cross-Site Request Forgery)的全称是“跨站请求伪造”,指攻击者通过跨站请求,以合法的用户的身份进行非法操作。可以这么理解CSRF攻击:攻击者盗用你的身份,以你的名义向第三方网站发送恶意请求。CRSF能做的事情包括利用你的身份发邮件,发短信,进行交易转账,甚至盗取账号信息。如何防范CSRF攻击
安全框架:例如Spring Security。
token机制:在HTTP请求中进行token验证,如果请求中没有token或者token内容不正确,则认为CSRF攻击而拒绝该请求。
**验证码:**通常情况下,验证码能够很好的遏制CSRF攻击,但是很多情况下,出于用户体验考虑,验证码只能作为一种辅助手段,而不是最主要的解决方案。
**referer识别:**在HTTP Header中有一个字段Referer,它记录了HTTP请求的来源地址。如果Referer是其他网站,就有可能是CSRF攻击,则拒绝该请求。但是,服务器并非都能取到Referer。很多用户出于隐私保护的考虑,限制了Referer的发送。在某些情况下,浏览器也不会发送Referer,例如HTTPS跳转到HTTP。
1)验证请求来源地址;
2)关键操作添加验证码;
3)在请求地址添加 token 并验证。

常见的状态码有哪些?

HTTP状态码表示客户端HTTP请求的返回结果、标记服务器端的处理是否正常或者是出现的错误,能够根据返回的状态码判断请求是否得到正确的处理。

状态码由3位数字原因短语组成

状态码分类表

类别原因短语
1xxinformational(信息性状态码)接受的请求正在处理
2xxSuccess(成功状态码)请求正常处理完毕
3xxRedirection(重定向)需要进行附加操作以完成请求
4xxclient error(客户端错误)客户端请求出错,服务器无法处理请求
5xxServer Error(服务器错误)服务器处理请求出错

各类别常见状态码:

  • 2xx

    • 200 OK:表示从客户端发送给服务器的请求被正常处理并返回。
    • 204 No Content:表示客户端发送给客户端的请求得到了成功处理,但在返回响应报文中不包含实体的主体部分;
    • 206 Patial Content:表示客户端进行了范围请求,并且服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容。
  • 3xx

    • 301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL。
    • 302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;
    • 301与302的区别:前者是永久移动,后者是临时移动(之后可能还会更改URL)
    • 303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源。
    • 302与303的区别:后者明确表示客户端应当采用GET方式获取资源
    • 304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求未满足条件的情况下返回该状态码;
    • 307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET。
  • 4xx

    • 400 Bad Request:表示请求报文中存在语法错误。
    • 401 Unauthorized:未经许可,需要通过HTTP认证
    • 403 Forbidden:服务器拒绝该次访问
    • 404 Not Found:表示服务器上无法找到请求的资源,除此之外,也可以在服务器拒绝请求但不想给拒绝原因时使用。
  • 5xx

    500 inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;

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

防范常见的 Web 攻击

SQL注入攻击
攻击者在HTTP请求中注入恶意的SQL代码,服务器使用参数构建数据库SQL命令时,恶意SQL被一起构造,并在数据库中执行。
用户登录,输入用户名 lianggzone,密码 ‘ or ‘1’=’1 ,如果此时使用参数构造的方式,就会出现
select * from user where name = ‘lianggzone’ and password = ‘’ or ‘1’=‘1’
不管用户名和密码是什么内容,使查询出来的用户列表不为空。如何防范SQL注入攻击使用预编译的PrepareStatement是必须的,但是一般我们会从两个方面同时入手。
Web端
1)有效性检验。
2)限制字符串输入的长度。
服务端
1)不用拼接SQL字符串。
2)使用预编译的PrepareStatement。
3)有效性检验。(为什么服务端还要做有效性检验?第一准则,外部都是不可信的,防止攻击者绕过Web端请求)
4)过滤SQL需要的参数中的特殊字符。比如单引号、双引号。

解决跨域问题

项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。

前端解决

后端解决

跨域资源共享,实现了跨站访问控制, 使得安全地进行跨站数据传输成为可能。

服务器端对于 CORS 的支持,主要就是通过设置响应头Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许 Ajax 进行跨域的访问。

只需要在后台中加上响应头来允许域请求!在被请求的 Response header 中加入以下设置,就可以实现跨域访问了。

Spring项目中的解决方法:

  • 手工设置响应头(HttpServletResponse )

    使用 HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里 Origin 的值也可以设置为"*" ,表示全部放行。

  • 使用注解(@CrossOrigin)

    在方法上(@RequestMapping)使用注解 @CrossOrigin

  • 返回新的CorsFilter

    在任意配置类,返回一个新的 CorsFilter Bean,并添加映射路径和具体的 CORS 配置信息。

密码加密机制

算法特点有效破解方式破解难度其他
明文保存实现简单无需破解简单
对称加密可以解密出明文获取密钥需要确保密钥不泄露
单向HASH(MD5、SHA1)不可解密碰撞、彩虹表
特殊HASH不可解密碰撞、彩虹表需要确保“盐”不泄露
Pbkdf2不可解密需要设定合理的参数

javaEE

jsp和Servlet的区别和联系

  • jsp经编译后就变成了Servlet
  • jsp更擅长表现页面显示,servlet更擅长于逻辑控制。
  • Servlet中没有内置对象,Jsp中的内置对象都必须通过HttpSevletRequest对象得到。Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器完成。而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。

联系:

JSP是Servlet技术的扩展点,本质上就是Servlet的简易方式。JSP编译后是“类Servlet”

Servlet和JSP最主要的不同点在于:Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML里分离开来。

而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。

JSP侧重于视图,Servlet主要用于控制逻辑

Servlet更多的是类似于一个Controller,用来做控制。

servlet

​ 用java编写的服务器端程序,

servlet作用

​ 接收客户端请求 service

​ 处理

​ 响应

服务器负责 客户端 与 servlet之间的通信

生命周期

服务器启动时创建/第一次访问时创建

init() 初始化

服务 service() doget/dopost -----> springmvc解析—> 方法

销毁 destroy()

servlet是线程安全的吗

不安全,servlet对象是单例的, 成员变量会被多个线程共享, 解决方法:使用ThreadLocal

拦截器

拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。

首先编写一个简单的拦截器处理类,请求的拦截是通过HandlerInterceptor 来实现,看到HandlerInterceptor 接口中也定义了三个方法。

preHandle() :这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。

postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 有意思的是:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。

过滤器

过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。

  • init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。注意:这个方法必须执行成功,否则过滤器会不起作用。
  • doFilter() :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
  • destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次

我们不一样

过滤器 和 拦截器 均体现了AOP的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。

  1. 实现原理不同

    过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

    在我们自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个 doFilter() 方法就是回调方法。

    ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter()里调用各个自定义xxxFilter过滤器,并执行 doFilter() 方法。

    而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调。

  2. 使用范围不同

    我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

    而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于ApplicationSwing等程序中。

  3. 触发时机不同

    过滤器拦截器的触发时机也不同,我们看下边这张图。

    过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

    拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

  4. 拦截的请求范围不同

    过滤器Filter执行了两次,拦截器Interceptor只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

  5. 注入Bean情况不同

    因为加载顺序导致的问题,拦截器加载的时间点在springcontext之前,而Bean又是由spring进行管理。

  6. 控制执行顺序不同

    看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

    postHandle() 方法被调用的顺序跟 preHandle() 居然是相反的!如果实际开发中严格要求执行顺序,那就需要特别注意这一点。

    我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch() 方法,而拦截器postHandle()preHandle()方法便是在其中调用的。

    发现两个方法中在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的。。。,导致postHandle()preHandle() 方法执行的顺序相反。

框架

mybatis

什么是Mybatis?

一个优秀的(半自动的)数据持久层框架,是一个将sql与java分离的,提供对象关系映射(ORM),

将JDBC中的接口进行了封装,简化了JDBC代码. 提供了动态sql语句使得功能强大.

Mybaits的优缺点

优点:

1.sql语句与代码分离,存放于xml配置文件中

2.用逻辑标签控制动态SQL的拼接

3.查询的结果集与java对象自动映射,保证名称相同,配置好映射关系即可自动映射或者

4.编写原生SQL 效率高

缺点:

JDBC方式可以用用打断点的方式调试,但是Mybatis不能,需要通过log4j日志输出日志信息帮助调试

对 SQL语句依赖程度很高;并且属于半自动,数据库移植比较麻烦(相比hibernate) 配置方言oracle

简述Mybatis的运行流程

第一步:通过Resources加载配置好的mybatis.xml配置文件.

第二步:然后看第二句话,这句话是关键。我们首先new了一个SqlSessionFactoryBuilder对象,他是SqlSessionFactory的构建者。我们调用了他的build()方法.

第三步:我们继续往下走,我们最终的目的是获取一个SqlSession对象,现在我们有了一个SqlSessionFactory了,就愉快的生成SqlSession吧.

第四步:jdk动态代理生成mapper接口的代理对象.

第五步:通过第四步返回的代理对象的方法调用mapper方法最终执行的方法.

第六步: 封装查询结果.

Mybatis核心类:

SqlSessionFactory:
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或通过Java的方式构建出 SqlSessionFactory 的实例。SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory。

SqlSession:
SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager。SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。此外SqlSession不是线程安全的,因为每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession。

Executor:
Execounterrevolutionary接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statement,跟Simple的唯一区别就是内部缓存statement),SimpleExecutor(默认,每次都会创建新的statement)。以上三个就是主要的Executor。通过下图可以看到Mybatis在Executor的设计上面使用了装饰器模式,我们可以用CachingExecutor来装饰前面的三个执行器目的就是用来实现缓存。

#{}和${}的区别是什么?

#{} 预编译方式 安全

${} 字符串拼接 不安全 传一个列名

MyBatits的resultType和resultmap的区别?

resultmap 自定义映射 列名与类中属性名不相同, 关联关系(单个,集合)

resultType 具体的返回结果类型

使用 MyBatis 的 mapper 接口调用时有哪些要求

接口中的方法 与 xml中 匹配

方法名与 xml中id名相同

参数类型 与 xml中参数类型相同

返回值 与 xml中返回值相同

方法能重载 不能重载

什么是MyBatis的接口绑定?

接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们通过代理对象调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。

Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?

Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。

其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

where

​ and age = 10

Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

在mybatis的多表操作中,是可以实现查询A时,一并把其关联对象B给查出来的操作。
现在问题来了,若有一对多的关系“用户-钱包”:某用户A名下有1000个钱包,并且有关联查询的需求,那么我们每次查询用户,难道都要把他的1000个钱包全都查询出来吗?当然不是,由于user对象中包含钱包集合的引用,每次查询后,JVM都会在堆中开辟空间来存储这个集合对象,这样会浪费巨大的内存空间,我们不需要钱包信息的时候,当然就不应该去查询它。那么怎么去控制这个查询的时机呢?这就需要Mybatis的延迟加载了。

延迟加载就是在真正使用数据时才发起查询,不用的时候不查询——也叫按需加载(懒加载)。
反之就是立即加载,无论数据是否需要使用,只要调用查询方法,将马上发起查询。

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 把一个关联查询 ,拆分为多次查询,需要时,在发出查询,嵌套查询

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

Mybatis的一级、二级缓存

(1)一级缓存: 其存储作用域为SqlSession,当 SqlSession.flush 或 close 之后,该 SqlSession中的所有 Cache 就将清空,默认打开一级缓存。

(2)二级缓存与一级缓存其机制相同,其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache .要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;

当配置了二级缓存后,关闭sqlSession时会将数据写入到二级缓存,下次在新会话中仍然可以从二级缓存中查询到数据.

spring

什么是spring

Spring是一个轻量级Java开源工具,它是为了解决企业应用开发的复杂性而创建的,即简化Java开发。

spring构成模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ftr9YZYb-1634353769859)(D:\桌面\tu\1628498683597.png)]

  • Core Container(核心容器)

    Bean:管理Beans

    Core:Spring核心

    Context:配置文件

    ExpressionLanguage:SpEL表达式

  • AOP(切面编程)

  • Aspects:AOP框架

  • Data Access(数据库整合)

    JDBC,ORM,OXM,JMS,Transaction

  • Web(MVC Web开发)

    Web,Servlet,Portlet,Struts

  • Test(Junit整合)

优点

  • 轻量级的:Spring框架使用的jar都比较小,一般在1M以下或者几百KB,Spring核心功能的所需的jar总共在3M左右。Spring框架运行占用的资源少,运行效率高。
  • 非侵入式:编写一些业务类的时候不需要继承Spring特定的类,通过配置完成依赖注入后就可以使用,此时,Spring就没有侵入到业务类的代码里。
  • 方便解耦,简化开发:Spring就是一个大工厂,可以将所有对象创建和依赖关系维护,交给Spring管理。
  • 控制反转IOC:Spring提供了Ioc控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
  • AOP编程的支持:通过Spring提供的AOP功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付在Spring中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
  • 声明式事务的支持:Spring提供了容器功能,容器可以管理对象的生命周期、对象与对象之间的关系、我们可以通过编写XML来设置对象关系和初始值,这样容器在启动后,所有对象都直接可以使用,不用编写任何编码来产生对象。Spring有两种不同的容器:Bean工厂以及应用上下文。
  • 方便集成各种优秀的框架:可以将简单的组件配置,组成复杂的应用,Spring也提供了很多基础功能(事务管理,持久化框架集成,数据访问,web)将应用逻辑留给开发者。
  • 降低JavaEE API使用难度:Spring对JavaEE开发中非常难用的一些API,都提供了封装,使这些API应用难度大大降低。

缺点

Spring依赖反射,反射影响性能。

如何避免?

不要频繁地使用反射,大量地使用反射会带来性能问题。

通过反射直接访问实例会比访问方法快很多,所以应该优先采用访问实例的方式。

IOC

控制反转是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。

IOC容器是具有依赖注入功能的容器,负责对象的实例化、对象的初始化,对象和对象之间依赖关系配置、对象的销毁、对外提供对象的查找等操作,对象的整个生命周期都是由容器来控制。我们需要使用的对象都由IOC容器进行管理,不需要我们再去手动通过new的方式去创建对象,由IOC容器直接帮我们组装好,当我们需要使用的时候直接从IOC容器中直接获取就可以了。

正控:若要使用某个对象,需要自己去负责对象的创建。

反控:若要使用某个对象,只需要从Spring容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架。

目的:降低耦合度。

底层实现方式:解析xml/扫描注解标签+工厂模式+反射机制

控制反转是一种通过描述(XML或注解)并通过第三方去生成或获取特定对象的方式。在Spring中实现控制反转的是IOC容器,其实现方式是依赖注入(DI)。

作用:

  • 管理对象的创建和依赖关系的维护。
  • 解耦降低了依赖,由容器去维护具体的对象的创建。
  • bean对象生命周期管理 。

优点:

  • IOC和DI的配合使用能把应用的实际代码降到最低。
  • Spring集成了自己的测试模块,无需依赖junit。
  • IOC容器支持立即加载和延迟加载。

功能:

  • 依赖注入
  • 依赖检查
  • 自动装配
  • 支持集合
依赖注入(DI)

概念:组件之间依赖关系由容器在运行期间决定,形象的说,即由容器动态的将某个依赖关系注入到组件中。通过依赖注入机制,只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,有谁实现。

目的:提升组件的重用的频率,并为系统搭建一个灵活、可扩展的平台。

依赖注入方式 xml 注解

概念:组件之间依赖关系由容器在运行期间决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。通过依赖注入机制,只需要通过简单的配置,而无需任何代码就可以指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

目的:提升组件重用的频率,并为系统搭建应该灵活、可扩展的平台。

基于xml配置方式

bean:配置需要 spring管理的类

id:生成的对象名

class:全类名

name:对象别名,可以为多个

scope(作用域):

  • singleton(默认值):在Spring中只存在一个bean实例,单例模式。
  • prototype:原型 getBean()的时候都会new Bean()
  • request:每次http请求都会创建一个bean,仅用于WebApplicationContext环境
  • session:同一个Http session共享一个Bean,不同Session使用不同的Bean,使用环境同上

Xml配置方式依赖注入

指Spring创建对象的过程中,将对象依赖属性通过配置设置给该对象。

  • set注入:容器通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JS0wHxah-1634353769860)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976202459.png)]

    xml配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XfrWlI8Q-1634353769860)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976221951.png)]

  • 构造器注入:通过容器触发一个类的构造器来实现的,该类由一系列参数,每个参数代表一个对其他类的依赖。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsaSL7x7-1634353769861)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976354305.png)]

    xml配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4DufSGI-1634353769862)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976382048.png)]

注解方式实现

注解开发准备工作:需要的jar包,注解功能封装在AOP包中,导入Spring AOP jar包即可。

  • 开启注解扫描

    <context:component-scan base-package="包名"> </context:component-scan>
    
  • 注解创建对象

    @Component(value=“user”)等于

    @Service

    @Repository

    都可以实现创建对象功能,只是为了后续扩展功能,在不同的层使用不同的注解标记

注解方式注入属性

  • byType自动注入@Autowired

    需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean的方式。使用该注解完成属性注入时,类中无需setter

  • byName自动注入@Autowired与@Qualifier

    需要在引用属性上联合使用注解@Autowired与@Qualifier。@Qualifier的value属性用与指定要匹配的Bean的id值。

  • JDK注解@Resource自动注入

    Spring提供了对jdk中@Resource注解的支持。@Resource注解既可以按名称匹配Bean,也可以按类型匹配Bean。默认是按名称注入。

  • byName注入引用类型属性

    @Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。

注解的工作原理

注解的工作步骤主要由以下几步:

  1. 通过键值对的形式为注解属性复制
  2. 编译器检查注解的使用范围,将注解信息写入元素属性表(也就是CLASS文件)
  3. 运行时JVM将RUNTIME的所有注解属性取出并最终存入map里
  4. JVM创建AnnotationInvocationHandler 实例 并且传入 刚才的map
  5. JVM使用动态代理机制为注解生成代理类,并初始化处理器
  6. 最后通过调用invoke方法,传入方法名返回注解对应的属性值

@Autowired 与@Resource的区别

@Autowired//默认按type注入 , Spring提供的注解

@Qualifier(“userDao”)//一般作为@Autowired()的修饰用 按照对象名注入

UserDao userDao;

@Resource(name=“cusInfoService”)//默认按name注入,可以通过name和type属性进行选择性注入, jdk提供

UserDao userDao;

AOP

比如权限认证、日志、事务处理。

  1. 概念

    面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以堆业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。

spring容器创提供声明式事务;允许用户自定义切面

以下名词需要了解下:

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 …
  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。
  • 切入点(PointCut):切面通知 执行的 “地点”的定义。
  • 连接点(JointPoint):与切入点匹配的执行点。

动态代理中,代理类并不是在java代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为JDK动态代理和cglib动态代理。

  • jdk代理

    jdk动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。

    步骤

    1. 编写一个委托类的接口。即静态代理的。
    2. 实现一个真正的委托类,即静态代理的。
    3. 创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法。
    4. 在测试类中,生成动态代理的对象。
  • Cglib代理

    jdk实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现SpringAOP的基础。

总结:

  • cglib创建的动态代理对象比jdk创建的动态代理使用的是Java反射技术实现,生成类的过程比较高效,对象的性能更高,但是cglib创建代理对象时所花费的时间比jdk多。所以对于单例的对象,因为无需频繁创建对象,用cglib合适,反之使用jdk方式更为合适。
  • jdk动态代理只能对实现了接口的类生成代理

spring事务管理

声明式事务:管理建立在AOP基础上,本质是对方法前后进行拦截,所以声明式事务是方法级别的。

管理方式:基于XML配置;基于注解实现

编程式事务:在项目中很少使用,需要注入一个事务管理对象TransactionTemplate,然后在我们代码中需要提交事务或回滚事务时自己写代码实现。

说一下 spring 的事务传播行为

指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用这methodA的事务中运行,还是为自己开启应该新事务运行,这就是由methodB的事务传播行为决定的。

在methodB上加@Transaction(Propagation=XXX)设置决定。

事务传播行为是Spring框架独有的事务增强特性,不属于事务实际提供方数据库行为。

Spring定义了七种传播行为:

事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

BeanFactory和ApplicationContext

在spring容器中,BeanFactory接口是IOC容器要实现的最基础的接口,定义了管理Bean的最基本方法,例如获取实例、基本的判断等。

BeanFactory有多个子接口来进一步扩展bean相关的功能。

ApplicationContext也间接继承了BeanFactory,如果说BeanFactory是Spring的心脏,那么ApplicationContext就是完整的身躯。他们都可以当作Spring的容器,Spring容器是生成Bean实例的工厂,并管理容器中的Bean。

区别:

  • BeanFactory是Spring中比较原始的Factory,它不支持AOP、Web等Spring插件。而ApplicationContext不仅包含了BeanFactory的所有功能,还支持Spring的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实现继承。
  • BeanFactory是Spring框架的基础设施,面向Spring本身;而ApplicationContext面向使用Spring的开发者,相比BeanFactory提供了更多面向实际应用的功能,集合所有场合都可以直接使用。
  • 实现ApplicationContext接口的方式会在Spring启动是初始化所有的单例bean,也可以为bean设置lazy-init属性为true。而实现BeanFactory接口的方式不会在启动时创建单例bean,而是第一次getBean()时创建。在容器启动时,可以发现spring中存在的配置错误。
  • BeanFactory是不支持国际化功能的,因为BeanFactory没有扩展Spring中MessageResource接口;而由于ApplicationContext扩展了MessageResource接口,因而具有消息处理的能力。
  • ApplicationContext扩展了ResourceLoader(资源加载器)接口,从而可以用来加载多个Resource,而BeanFactory是没有扩展ResourceLoader。底层资源的访问

springBean的生命周期

**宏观上来讲,**spring Bean的生命周期可以分为5个阶段:

  1. 实例化Instantiation

    创建对象

  2. 属性赋值Populate

    给属性注入值

  3. 初始化Initialization

  4. 初始化bean,根据配置为bean添加额外的功能将bean对象放入容器中,使用

  5. 销毁Destruction

细化

  1. 实例化一个Bean–也就是我们常说的new;

  2. 按照Spring上下文对实例化的Bean进行配置–也就是IOC注入;

  3. (1)如果这个Bean以及实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值。

    (2)如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory,传递的是Spring工厂自身

    (3)如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext方法,传入Spring上下文(这个方式可以实现2的内容,比2更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法)。

    (4)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization方法;(BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用内存或缓存技术)。

    (5)如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。

    (6)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization方法。

    以上工作完成后就可以应用这个Bean了,这个Bean是一个Singleton(单例模式)的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例。

  4. 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destory()方法。

  5. 最后,如果这个Bean的Spring配置中配置了destory-metnod属性,会自动调用其配置的销毁方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qJ4wvB8h-1634353769863)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631011210856.png)]

Spring 中的bean 是线程安全的吗?

不是线程安全的。

Spring容器中的bean是否线程安全,容器本身并没有提供bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean情况。

Spring的Bean作用域类型

默认是单例

作用域字符描述
单例singleton整个应用中只会创建一个实例
原型prototype每次注入时都新建一个实例
会话session为每个会话创建一个实例
请求request为每个请求创建一个实例

线程安全要从单例与原型分别进行说明

  • 原型Bean

    对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

  • 单例Bean

    对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

bean又分为:

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据

如果一个单例Bean是一个无状态的Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring MVC的Controller、Service、Dao等只关注方法本身,如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这个是自己线程的工作内存,是安全的。

但是如果Bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的办法就是把Bean的作用域改为protopyte,这样每次请求Bean就相当于是new Bean() 这样就可以保证线程安全了。

Spring懒加载

scope 单例 原型

Spring默认创建对象是在启动Spring的时候。

把lazy-init=“true”(在bean中配置)后先启动了spring容器,然后就是我们调用该类的时候,spring容器才帮我们创建对象。减少启动Spring的时间,减少web服务器在运行的负担。

意义:tomcat启动时就创建配置文件中的所有bean对象,如果有些类或者配置文件的书写有误,这时候,spring容器就会报错,那么自然spring容器也就启动不起来了。这种情况可以避免,我们到了后面真正要调用该类的时候才报错。当然这种做法,会把一些类有早的加载到内存中。

当我们选择在调用某个类的时候,spring容器才帮我们创建这个类,首先我们可以解决第一种情况出现的问题,节省了内存但是这时候,类和配置文件中许多隐藏的错误,在调用的时候才发现,这时候添加了查错的压力。

Bean循环依赖

A对象依赖了B对象,B对象依赖了A对象。

发生在哪?

  • 构造器的循环依赖。构造器的循环依赖问题无法解决,只能抛出异常。
  • field属性的循环依赖。

是个问题吗?

如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖很正常。

但是在Spring中循环依赖就是问题了。

因为,在Spring中一个对象并不是简单new出来的,而是会经过一系列的Bean生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。

Spring内部三级缓存:
  1. 一级缓存

    singletonObjects,存放完全实例化且属性赋值完成的 Bean ,可以直接使用

  2. 二级缓存

    earlySingletonObjects,存放早期 Bean 的引用,尚未装配属性的 Bean

  3. 三级缓存

    singletonFactories,存放实例化完成的 Bean 工厂

产生循环依赖的问题,主要是:A创建时->需要B->去创建->需要A,从而产生了循环。

A,B循环依赖,先初始化A,先暴露一个半成品A,再去初始化依赖的B,初始化B时如果发现B依赖A,也就是循环依赖,就注入半成品A,之后初始化完毕B,再回到A的初始化过程时就解决了循环依赖,在这里只需要一个Map能缓存半成品A就行了,也就是二级缓存就够了,打不死这份二级缓存存的是Bean对象,如果这个对象存在代理,那应该注入的是代理,而不是Bean,此时二级缓存无法既缓存Bean,又缓存代理,因此三级缓存做到了缓存工厂,也就是生成代理,二级缓存就能解决缓存依赖,三级缓存解决的是代理。

Spring MVC 运行流程

1.概述

SpringMVC是Spring框架的一个模块,SpringMVC和Spring无需通过中间整合层进行整合。

SpringMVC是一个基于MVC的WEB框架,方便前后数据的传输。

SpringMVC拥有控制器,接受外部请求,解析参数传给服务层。

SpringMVC是一个基于java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几个部分来简化开发。

2.SpringMVC运行流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnChqC3D-1634353769864)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631237447277.png)]

  1. 用户向服务器发送请求,请求被Spring前端控制DispatcherServlet捕获

  2. 前端控制器DispatcherServlet接收请求后,调用处理器映射HandlerMapping

    处理器映射器HandlerMapping根据请求的url找到处理该请求的处理器Handler,将处理器Handler返回给前端控制器DispatcherServlet。

  3. DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter。在填充Handler的入参过程中,根据自己的配置,Spring将帮你做一些额外的工作:

    • HttpMessageConveter:将请求信息转换成一个对象,将对象转换为指定的响应信息。
    • 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等。
    • 数据根式化:对请求信息进行数据各式化。如将字符串转成格式化数字或格式化日期等。
    • 数据验证:验证数据的有效性,验证结果存储到BindingResult或Error中。
  4. Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象;

  5. 根据返回的ModelAndView选择一个适合的ViewResolver返回给DispatcherServlet;

  6. ViewResolver结合Model和View,来渲染视图

  7. 将渲染结果返回给客户端

SpringMVC组件

  • DispatcherServlet:Spring中提供了org.springframework.web.servlet.DispatcherServlet 类,它从 HttpServlet 继承而来,它就是 Spring MVC 中的前端控制器(Frontcontroller)。
  • HandlerMapping: DispatcherServlet 自己并不处理请求,而是将请求交给 页面控制器。那么在 DispatcherServlet 中如何选择正确的页面控制器呢? 这件事情就交给 HandlerMapping 来做了,经过了 HandlerMapping 处理 之后,DispatcherServlet 就知道要将请求交给哪个页面控制器来处理了。
  • HandlerAdapter:经过了HandlerMapping处理之后,DispatcherServlet就获取到了处理器,但是处理器有多种,为了方便调用,DispatcherServlet将这些处理器包装成处理器适配器HandlerAdapter,HandlerAdapter调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView对象。
  • ModelAndView:DispatcherServlet取得了ModelAndView之后,需要将把逻辑视图名解析为具体的View,比如jsp视图,pdf视图等,这个解析过程由ViewResolver来完成。
  • ViewResolver:ViewResolver将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术。
  • View:DispatcherServlet通过ViewResolver取得了具体的view之后,就需要将model种的数据渲染到视图上,最终DispatcherServlet将渲染的结果响应到客户端。

Spring MVC的常用注解解释

  • @Controller

    在SpringMVC中,Controller主要负责处理前端控制器发过来的请求,经过业务逻辑层处理之后封装成一个model,并将其返回给view进行展示。@Controller注解通常用于类上。

  • @RequestMapping

    注解是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上的注解会将一个特定请求或者请求模式映射到一个控制器上,标识类中所有响应请求的方法都是以该地址作为父路径;方法的级别上注解标识进一步指定到处理方法的映射关系。

  • @PathVariable

    用来获取URL参数。url/{id}

  • @RequestParam

    从Request里获取参数 url?id=1

  • @RequestBody

    用于接收前端传来的实体,将json转换为Java对象。

  • @ResponseBody注解

    将Controller方法返回的对象,转化为Json对象响应给客户端。

SpringMVC中如何解决POST请求中文乱码问题,GET请求的又如何处理呢?

**post:**在web.xml中配置一个CharacterEncodingFilter过滤器,设置编码为utf-8

get:在tomcat配置文件中添加与项目工程编码一致的编码类型

Servlet的过滤器与Spring拦截器详解

实现原理不同

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

使用范围不同

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

触发时机不同

过滤器 和 拦截器的触发时机也不同,我们看下边这张图。

img

过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

拦截的请求范围不同

过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

springBoot

什么是 Spring Boot?

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

Spring Boot 有哪些优点?

Spring Boot 主要有如下优点:

  • 容易上手,提升开发效率,为 Spring 开发提供一个更快、更广泛的入门体验。
  • 开箱即用,远离繁琐的配置。
  • 提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
  • 没有代码生成,也不需要XML配置。
  • 避免大量的 Maven 导入和各种版本冲突。

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
  • @ComponentScan:Spring组件扫描。

什么是 JavaConfig?

Spring JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。使用 JavaConfig 的优点在于:

(1)面向对象的配置。由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean 方法等。

(2)减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfig 配置类来配置容器是可行的,但实际上很多人认为将JavaConfig 与 XML 混合匹配是理想的。

(3)类型安全和重构友好。JavaConfig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。

Spring Boot 自动配置原理是什么?

Spring Boot 默认使用 application.properties 或 application.yml 作为其全局配置文件,我们可以在该配置文件中对各种自动配置属性进行修改,并使之生效。

SpringBoot的自动配置是基于SpringFactories机制实现的。

Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

SpringBoot的start

加载流程

1、去META-INF 目录下找到这个spring.factories文件
2、通过 文件内指定的类路径,找到 配置类
3、配置类加载进 属性类
4、配置类通过属性类的参数构建一个新的bean

实现自己的start

  1. 编写属性类

    @ConfigurationProperties(prefix = "redis")
    public class RedisProperties {
        private Integer port;
        private String host;
        private String password;
        private int index;
    	//省略了get set 方法
    }
    

    之后我们就可以在properties 中 使用 redis.port=这样子来指定参数了

  2. 编写配置类

    @Configuration
    //只有当Jedis 存在的时候 才执行,就是说一定要引入了Jedis的依赖才会执行这个配置
    @ConditionalOnClass(Jedis.class)
    //引入属性类
    @EnableConfigurationProperties(RedisProperties.class)
    public class RedisAutoConfiguration {
        @Bean
        //当这个bean不存在的时候才执行,防止重复加载bean
        @ConditionalOnMissingBean
        public Jedis jedis(RedisProperties redisProperties) {
            Jedis jedis = new Jedis(redisProperties.getHost(), redisProperties.getPort());
            jedis.auth(redisProperties.getPassword());
            jedis.select(redisProperties.getIndex());
            return jedis;
        }
    }
    
  3. 编写spring.factories文件

    在resources 目录下创建入口文件,编写内容

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=xyz.xiezihao.myjedis.RedisAutoConfiguration
    
    
  4. 测试

    然后我们新建一个springboot项目,在pom中加入依赖

    	<dependency>
                <groupId>xyz.xiezihao</groupId>
                <artifactId>redis-start</artifactId>
                <version>0.0.1-SNAPSHOT</version>
           </dependency>
    
    

    然后写一个测试文件

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class TestStartApplicationTests {
        @Resource
        private Jedis jedis;
        @Test
        public void contextLoads() {
            jedis.set("zhu","大肥猪");
            String zhu = jedis.get("zhu");
            System.out.println(zhu);
        }
    }
    

你如何理解 Spring Boot 配置加载顺序?

在 Spring Boot 里面,可以使用以下几种方式来加载配置。

1)properties文件;

2)YAML文件;

3)系统环境变量;

4)命令行参数;

等等……

什么是 YAML?

YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。

YAML 配置的优势在哪里 ?

YAML 现在可以算是非常流行的一种配置文件格式了,无论是前端还是后端,都可以见到 YAML 配置。那么 YAML 配置和传统的 properties 配置相比到底有哪些优势呢?

配置有序,在一些特殊的场景下,配置有序很关键
支持数组,数组中的元素可以是基本数据类型也可以是对象
简洁
相比 properties 配置文件,YAML 还有一个缺点,就是不支持 @PropertySource 注解导入自定义的 YAML 配置。

Spring Boot 是否可以使用 XML 配置 ?
Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。

spring boot 核心配置文件是什么?bootstrap.properties 和 application.properties 有何区别 ?
单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。

spring boot 核心的两个配置文件:

bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。

什么是 Spring Profiles?

Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。

如何在自定义端口上运行 Spring Boot 应用程序?

为了在自定义端口上运行 Spring Boot 应用程序,您可以在application.properties 中指定端口。server.port = 8090

安全

如何实现 Spring Boot 应用程序的安全性?

为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。

比较一下 Spring Security 和 Shiro 各自的优缺点 ?

由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易,甚至只需要添加一个依赖就可以保护所有的接口,所以,如果是 Spring Boot 项目,一般选择 Spring Security 。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。Shiro 和 Spring Security 相比,主要有如下一些特点:

Spring Security 是一个重量级的安全管理框架;Shiro 则是一个轻量级的安全管理框架
Spring Security 概念复杂,配置繁琐;Shiro 概念简单、配置简单
Spring Security 功能强大;Shiro 功能简单

什么是 CSRF 攻击?

CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。

监视器

Spring Boot 中的监视器是什么?

Spring boot actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的REST 端点来检查状态。

如何在 Spring Boot 中禁用 Actuator 端点安全性?

默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATOR 角色的用户才能访问它们。安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。

我们如何监视所有 Spring Boot 微服务?
Spring Boot 提供监视器端点以监控各个微服务的度量。这些端点对于获取有关应用程序的信息(如它们是否已启动)以及它们的组件(如数据库等)是否正常运行很有帮助。但是,使用监视器的一个主要缺点或困难是,我们必须单独打开应用程序的知识点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务,管理员将不得不击中所有 50 个应用程序的执行终端。为了帮助我们处理这种情况,我们将使用位于的开源项目。 它建立在 Spring Boot Actuator 之上,它提供了一个 Web UI,使我们能够可视化多个应用程序的度量。

前后端分离,如何维护接口文档 ?

前后端分离开发日益流行,大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档,不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。

spring-boot-starter-parent 有什么用 ?

我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:

定义了 Java 编译版本为 1.8 。
使用 UTF-8 格式编码。
继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
执行打包操作的配置。
自动化的资源过滤。
自动化的插件配置。
针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。
Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。

Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

运行 Spring Boot 有哪几种方式?

1)打包用命令或者放到容器中运行

2)用 Maven/ Gradle 插件运行

3)直接执行 main 方法运行

Spring Boot 需要独立的容器运行吗?

可以不需要,内置了 Tomcat/ Jetty 等容器。

开启 Spring Boot 特性有哪几种方式?

1)继承spring-boot-starter-parent项目

2)导入spring-boot-dependencies项目依赖

如何使用 Spring Boot 实现异常处理?

Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,来处理控制器类抛出的所有异常。

如何使用 Spring Boot 实现分页和排序?

使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。

微服务中如何实现 session 共享 ?

在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。

Spring Boot 中如何实现定时任务 ?

定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。

在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方框架 Quartz。

使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。

使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。

Spring缺点

缺点
虽然Spring的组件代码是轻量级的,但它的配置确实重量级的。虽然Spring引入了注解功能,但是仍然需要编写大量的模板化配置文件。
项目的依赖管理也是一件耗时耗力的事情,在环境搭建时,需要分析要导入大量库的坐标,而且还需要分析导入与之有依赖关,一旦选错依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。
SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。

什么是springboot

在spring的基础上进行开发的,

解决spring问题: 依赖多,模板化的配置

搭建非常方便

起步依赖 直接将相关的jar包直接进行依赖

自动装配

SpringBoot本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来代替Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。
SpringBoot以约定大于配置的核心思想,从而使开发人员不再需要定义样板化的配置。使他集成了大量常用的第三方库配置(如Redis,MongoDB,Jpa RabbitMQ,Quartz等等),SpringBoot应用中这些第三方库几乎可以零配置的开箱即用,通过这种方式,SpringBoot致力于在蓬勃发展的快速应用开发领域成为领导者。
SpringBoot你只需要“run”就可以非常轻易的构建独立的、生产级别的Spring应用。

3.SpringBoot特点

​ 创建独立的Spring应用程序;
​ 直接内嵌tomcat、jetty和undertow
​ 提供了固定化的“starter”配置,以简化构建配置;
​ 尽可能的自动配置Spring和第三方库;
​ 提供产品级的功能,如:安全指标,运行状况监测和外部化配置等;
​ 绝对不会生成代码,并且不需要XML配置。
1.SpringBoot的核心功能
(1)起步依赖
​ 起步依赖就是将具备某种功能的坐标打包在一起,并提供一些默认的功能。
(2)自动配置
​ SpringBoot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的。

SpringBoot注解

  • @SpringBootApplication
    这个注解是SpringBoot最核心的注解,用在SpringBoot的主类上,标识这是一个SpringBoot应用,用来开启SpringBoot的各项能力。实际上这个注解是@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解的组合。由于这些注解一般都是一起使用,所以SpringBoot提供了一个统一的注解@SpringBootApplication。
  • @EnableAutoConfiguration
    允许SpringBoot自动配置注解,开启这个注解之后,SpringBoot就能根据当前类路径下的包或者类来配置SpringBean。
    如:当前路径下有Mybatis这个JAR包,MybatisAutoConfiguration注解就能根据相关参数来配置Mybatis的各个SpringBean,
  • @Configuration
    用于定义配置类,指出该类是Bean配置的信息源,相当于传统的xml配置文件,一般加在主类上。如果有些第三方库需要用到xml文件,建议仍然通过@Configuration类作为项目的配置主类。
  • @ComponentScan
    组件扫描。让SpringBoot扫描到Configuration类并把它加入到程序上下文。@ComponentScan注解默认就会装配标识了@Controller,@Service,@Repository,@Component注解的类到Spring容器中。
  • @RestController
    用于标注控制层组件(如struts中的action),表示这是个控制器bean,并且是将函数的返回值直接填入HTTP响应体中,是REST风格的控制器;它是@Controller和@ResponseBody的合集。
  • @Bean
    相当于XML中的< bean> < /bean>,放在方法的上面,而不是类,意思是产生一个bean,并交给Spring管理。
  • @PathVariable
    路径变量,参数与大括号里的名字一样要相同。

@Component和@Bean的区别

@Component用在类上 创建该类的对象,并管理

@Bean用在方法

public User {

​ return new User();

}

JWT令牌

Json web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。 因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

1.起源

(1)传统的session认证
http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。
(2)基于session认证所显露的问题
Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求再这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF(跨站请求伪造):因为是基于cookie来进行用户识别的,cookie如果被截获,用户很容易受到跨站请求伪造的攻击。
(3)基于token的鉴别机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCsG0Mka-1634353769867)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631587773413.png)]

用户使用账号和密码发出post请求;
服务器使用私钥创建一个jwt;
服务器返回这个jwt给浏览器;
浏览器将该jwt串在请求头中向服务器发送请求;
服务器验证该jwt;
返回响应的资源给浏览器。

2.JWT的主要应用场景

​ 身份认证这种场景下,一旦用户完成了登录,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO) 中比较广泛的使用了该技术。信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
(1)优点

简洁(Compact):可以通过URL,POST参数或者在HTTP haeder发送,因为数据量小,传输速度也很快;
自包含(Self-contained):负载中包含了所有的用户所需要的信息,避免了多次查询数据库;
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;
不需要在服务器端保存会话信息,特别适用于分布式微服务。
3.JWT的构成
JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiw ibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab3 0RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload,用户的信息),第三部分是签证(signature)。
(1)第一部分
header
jwt的头部承载两部分信息:
声明类型,这里是jwt
声明加密算法 通常直接使用HMAC HS256
完整的头部就像下面的JSON:
{
‘typ’: ‘JWT’,
‘alg’: ‘HS256’
}
然后将头部进行base64转码,构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
10010101 01010101 100101 010101 010100
(2)第二部分
payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
标准中注册的声明;
公共的声明(公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务,需要的必要信息,但不建议加敏感信息,如密码,因为该部分在客户端可解密);
私有的声明。
定义一个payload:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}

然后将其进行base64转码,得到jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4i OnRydWV9
(3)第三部分
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header(base64后的)
payload(base64后的)
secret
这个部分需要base64转码后的header和base64转码后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加密secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JVM

概述

1.虚拟机

虚拟机就是一台虚拟的计算机。通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机(例如:VMware)和程序虚拟机(例如:java虚拟机)。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源。

  1. VMware

    完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。

  2. java虚拟机(JVM)

    专门为执行某个单个计算机程序而设计。Java虚拟机是一种执行java字节码文件的虚拟计算机,它拥有独立的运行机制。

java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。

2.JVM作用

java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条java指令,java虚拟机中都有详细定义,如怎么去操作数,怎么处理操作数,处理结果放在哪。

主要功能:

  1. 通过ClassLoader寻找和装载class文件。
  2. 解释字节码称为指令并执行,提供class文件的运行环境。
  3. 自动进行运行期间的内存分配和垃圾回收。
  4. 提供与硬件交互的平台。

现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。

3.JVM的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFeLVLYZ-1634353769868)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765807309.png)]

JVM是运行在操作系统之上的,它与硬件没有直接的交互。

4.JVM整体组成

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M87EteIN-1634353769868)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765967233.png)]

过程:程序在执行之前先要把java代码转换成字节码(class文件),jvm首先需要把字节码通过一定的方式类加载器把文件加载到内存中运行时数据区,而字节码文件是jvm的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令再交有CPU去执行,而这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

5.java代码的执行流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iyPD7G8d-1634353769869)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765990422.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6j3BhBvL-1634353769869)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766008532.png)]

java编译器编译过程中,任何一个节点执行失败就会造成编译失败。虽然各个平台的java虚拟机内部实现细节不尽相同,但是它们执行的字节码内容却是一样的。

JVM主要任务就是负责将字节码装载到其内部,解释/编译为对应平台上的机器指令执行。JVM使用类加载器装载class文件。

类加载完成后,会进行字节码校验,字节码校验通过之后JVM解释器会把字节码翻译成机器码交由操作系统执行。

但不是所有的代码都是解释执行,JVM对此做了优化,比如HoySpot虚拟机,它本身提供了JIT。

6.JVM架构模型

java编译器输入的指令流基本上是一种基于栈的指令集架构

两种架构特点

1.基于栈式架构的特点:

  1. 设计和实现更简单,适用于资源受限的系统。
  2. 使用零地址指令方式分配,其执行过程依赖与操作栈,指令集更小,编译器容易实现。
  3. 不需要硬件支持,可移植性好,更好跨平台。
  4. 指令集数量多,指令集小。

2.基于寄存器式架构特点:

  1. 指令完全依赖于硬件,可移植性差。
  2. 性能优秀,执行更高效。
  3. 完成一项操作使用的指令更少。

由于跨平台的设计,java指令集都是根据栈来设计的,不同CPU架构不同,所以不能设计为基于寄存器的。

面试题:时至今日,HotSpot虚拟机的宿主环境已经不局限于嵌入式平台了,那么为什么不将架构更换为性能更好的寄存器指令集架构呢?
答:两种指令集架构各有优劣,并存发展。首先,在设计和实现上,基于栈式的架构更简单。其次无论平台的资源是否受限制,基于栈式的架构都是可以使用的。(针对栈式的优点,可以继续balabala…)

类加载

1.类加载子系统的作用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIKNsNvG-1634353769870)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766032770.png)]

类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。

2.类加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这3步骤统称为类加载或类初始化。

类被加载到 JVM 开始,到卸载出内存,整个生命周期如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyG6O4QC-1634353769870)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766045934.png)]

1.加载
  1. 通过类名(地址)获取此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
2.链接

就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含以下步骤:

  1. 验证

    检验被加载的类是否有正确的内部结构,并和其他类协调一致。

  2. 准备

    准备阶段则负责为类的静态属性分配内存,并设置默认初始值;不包含final修饰的static实例变量,在编译时进行初始化。不会为实例变量初始化。

  3. 解析

    将类的二进制数据中的符号引用替换成直接引用。

3.初始化

类什么时候初始化?

  1. 创建类的实例,new对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(" "))
  5. 初始化一个类的子类(首先会初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

注意:对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

类的初始化顺序

对static修饰的变量或语句块进行赋值。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

顺序是:父类static -> 子类static -> 父类构造方法 -> 子类构造方法

3.类加载器分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2E0r8yPz-1634353769871)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766060018.png)]

  1. 自定义类加载器(User-Defined ClassLoader)

    从概念上来讲,自定义类加载器一般指的是程序汇总有开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

    1. 扩展类加载器(Extension ClassLoader)

      Java语言编写的,由sun.misc.Launcher$ExtClassLoader实现,父类加载器为null。

      派生于ClassLoader类。

      上层类加载器为引导类加载器。

      它负责加载JRE的扩展目录。

      从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载。

    2. 应用程序类加载器(系统类加载器 Application ClassLoader)

      Java语言编写的,由sun.misc.Launcher$AppClassLoader实现,父类加载器为ExtClassLoader。

      派生于ClassLoader类。

      上层类加载器为扩展类加载器。

      加载我们自己定义的类。

      该类加载器是程序中默认的类加载器。

      通过类名.class.getClassLoader(),ClassLoader.getSystemClassLoader()来获得。

  2. 引导类加载器(启动类加载器/根类加载器)(Bootstrap ClassLoader)

    这个类加载器使用C/C++语言实现,嵌套在JVM内部。用来加载Java核心类库。

    并不继承于java.lang.ClassLoader没有父加载器。

    负责加载扩展类加载器和应用类加载器,并为它们指定父类加载器。

    出于安全考虑,引用类加载器只加载器包名为java,javax,sun等开头的类。

注意:ClassLoader类,它是一个抽象类,其后所有类加载器都继承自ClassLoader(不包括启动类加载器)

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

4.类加载机制JVM的类加载机制主要有3种

JVM的类加载机制主要有3种

  1. 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
  2. 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  3. 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

细讲一下双亲委派机制(面试):

工作原理:

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器区执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果均加载失败,就会抛出ClassNotFoundException异常,这就是双亲委派模式。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了了时,儿子自己才想办法去完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQ7V7q9D-1634353769872)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766079714.png)]

双亲委派优点

  1. 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
  2. 避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。5

5.沙箱安全机制

作用:防止恶意代码污染java源代码。

比如我们定义了一个类名为String所在包也命名为java.lang,因为这个类本来属于jdk的,如果没有沙箱安全机制,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器。但是由于String就是jdk源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的String,后面的一概不能使用,这就保证了不被恶意代码污染。

6.类的主动使用/被动使用

JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就有被动使用。

主动使用

  1. 通过new关键字被导致类的初始化,导致类的加载并初始化。
  2. 访问类或接口的静态变量,包括读取和更新,或者对该静态变量赋值。
  3. 访问类的静态方法。
  4. 对某个类进行反射操作,会导致类的初始化。
  5. 初始化子类会导致父类的初始化。
  6. 执行该类的main函数。
  7. Java虚拟机启动时被表明为启动类的类(JavaTest)

被动使用

除了上面的几种主动使用其余就是被动使用了。

  1. 引用该类的静态常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致初始化。

    public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
    public final static int RANDOM = new Random().nextInt() ;//会导致类的初 始化,主动使用
    
  2. 构造某个类的数组时不会导致该类的初始化。

    Student[] students = new Student[10] ;
    

注意:主动使用和被动使用的区别在于类是否会被初始化。

7.类装载方式

面试题:
描述一下JVM加载Class文件的原理机制

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

类装载方式:

  1. 隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
  2. 显式装载,通过class.forName()等方法,显式加载需要的类。

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

面试题:
在jvm中如何判断两个对象是属于同一个类?
1.类的全类名(地址)完全一致。
2.类的加载器必须相同。

运行时数据区

运行时数据区

运行时数据区

1.概述

JVM的运行时数据区,不同虚拟机实现可能略微不同,但都会遵从Java虚拟机规范,Java 8虚拟机规范规定,Java虚拟机所管理的内存将会包括一下几个运行时数据区域:

  1. 程序计数器(Program Counter Register)

    程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

    java中最小的执行单位是线程,因为虚拟机的是多线程的,每个线程是抢夺cpu时间片,程序计数器就是存储这些指令去做什么,比如循环,跳转,异常处理等等需要依赖它。

    每个线程都有属于自己的程序计数器,而且互不影响,独立存储。

  2. Java虚拟机栈(Java Virtual Machine Stacks)

    描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应这一个线帧在虚拟机栈中入栈到出栈的过程。

    虚拟机栈

  3. 本地方法栈(Native Method Stack)

    与虚拟机的作用是相似的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的,与虚拟机栈相同的是栈的深度是固定的,当线程申请的大于虚拟机栈的深度就会抛出StackOverFlowError异常,当然虚拟机栈也可以动态的扩展,如果扩展到无法申请到足够的内存就会抛出outofMemoryError异常。

  4. Java堆(Java Heap)

    是Java虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有对象都分配在堆上渐渐变得不那么“绝对”了。

  5. 方法区(Method Area)

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

    内存区域是很重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异,我们现在以使用最为流行的HotSpot虚拟机为例讲解。

Java虚拟机定义了若干中程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的。这些与线程对应的区域会随着线程开始和结束而创建销毁。

如图:红色的为多个线程共享,灰色的为单个线程私有的,即

线程间共享:堆,对外内存。

每个线程:独立包括程序计数器,栈,本地方法栈。

运行时数据区

2.程序计数器(Program Counter Register)

  1. 概述

    JVM中的程序计数寄存器中的Register命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。

    这里,并非是广义上所值的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

  2. 作用

    程序计数器用来存储下一条指令的地址,也即将要执行的指令代码。有执行引擎读取下一条指令。

    程序计数器

    1. 它是一块很小的内存空间几乎可以忽略不计,也是运行速度最快的存储区域。
    2. 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致。
    3. 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。如果是在执行native方法,则是未指定值(undefined)。
    4. 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
    5. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
    6. 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    如下图所示:程序计数器的作用位置

    作用位置

  3. 面试题

    1.使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?

    因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿开始继续执行。
    JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
    

    2.程序计数器为什么被设定为线程私有的

    我们都知道所谓的多线程在一个特定的时间段内只会执行其中某个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
    为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都分配应该程序计数器,这样一来各个线程之间便可以独立计算,从而不互相干扰。
    

3.虚拟机栈(Java Virtual Machine Stack)

  1. 出现背景

    • 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

      基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更多的指令集。

  2. 栈和堆区别

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

    解决程序的运行问题,即程序如何执行,或者说如何处理数据。

    解决的是数据存储的问题,即数据怎么放,放在哪儿。

  3. Java虚拟机栈是什么?

    • Java虚拟机栈,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应这一次方法的调用。
    • Java虚拟机栈是线程私有的。
    • 生命周期和线程一致。
  4. 作用

    主管Java程序的运行,它保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

    作用

  5. 栈的特点

    1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
    2. JVM直接对Java栈的操作只有两个:调用方法进栈。执行结束后出栈。
    3. 对于栈来说不存在垃圾回收问题。

    出入栈

  6. 栈中出现的异常

    • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
    • OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
  7. 栈中存储什么?

    • 每个线程都有自己的栈,栈中的数据都以栈帧为单位存储。
    • 在这个线程上正在执行的每个方法都有各自对应一个栈帧。
    • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
  8. 栈的运行原理

    • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。

      在一条活动的线程中,一个时间点上,只会有一个活动栈。即只有当前执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈,与当前栈帧对应的方法称为当前方法,定义这个方法的类称为当前类。

      执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

      如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。

    栈

    • 不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。

      如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

    • Java方法有两种返回的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管哪种方式,都会导致栈帧被弹出。

  9. 栈帧的内部结构

    每个栈帧中存储着:

    1. 局部变量表(Local Variables)

      局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量则存的是只想对象的引用。

    2. 操作数栈(Operand Stack)(或表达式栈)

      栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

    3. 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)

      因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

    4. 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

      当一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

    5. 一些附加信息

    附加信息

  10. 两个栈帧之间的数据共享

    在概念模型中,两个栈帧作为虚拟机栈的元素,是完全互相独立的,但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数赋值传递,重叠的过程如图所示。

    共享

    Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

  11. 面试题

    1. 什么情况下会出现栈溢出(StackOverflowError)?

      栈溢出就是方法执行时创建的栈帧超出了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
      
    2. 通过调整栈大小,就能保证不出现溢出吗?

      不能。
      
    3. 分配的栈内存越大越好吗?

      并不是的,只能延缓这种现象的出现,可能会影响其他内存空间。
      
    4. 垃圾回收机制是否会涉及到虚拟机栈?

      不会。
      

4.本地方法栈(Native Method Stack)

  • Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
  • 本地方法栈也是线程私有的。
  • 允许被实现成固定或者是可动态扩展的内存大小。内存溢出方面也是相同的。

如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError。

如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出OutOfMemoryError。

  • 本地方法是用C语言写的。
  • 它的具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

5.Java堆内存

  1. 概述

    堆

    • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

    • Java堆区在JVM启动时的时候即被创建,其空间大小也确定了,是JVM管理的最大一块内存空间。

    • 堆内存的大小是可以调节的。

      例如:-Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)

      一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。

    • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。

    • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区。

    • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时配对在堆上。

    • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

    • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

  2. 堆内存区域划分

    Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)

    新生区分为Eden(伊甸园)区和Survivor(幸存者)区

    划分

  3. 为什么分区(代)?

    将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

    1

    2

    3

  4. 对象创建内存分配过程

    为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

    1. new的新对象先放到伊甸园区,此区大小有限制。

    2. 当伊甸园的空间填满时,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区。

    3. 然后将伊甸园区中的剩余对象移动到幸存者0区。

    4. 如果再次触发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区是空的。

    5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。

    6. 什么时候去养老区呢?默认是15此,也可以设置参数

      -XX:MaxTenuringThreshold=

    7. 在老年区,相对悠闲,当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。

    8. 若养老区执行了Major GC之后发现依然无法进行对象保存,就会产生OOM异常。

      Java.lang.OutOfMemoryError:Java heap space

      例如:

      public static void main(String[] args) {
          List<Integer> list = new ArrayList();
          while(true){
              list.add(new Random().nextInt());
          }
      }
      

    分配

  5. 新生区与老年区配置比例

    配置新生代与老年代在堆结构的占比(一般不会调)

    1. 默认-XX:NewRatio = 2,表示新生代占1,老年代占2,新生代占整个堆的1/3.

    2. 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5.

    3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优。

比例
在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1,当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。

新生区的对象默认生命周期超过15,就会区养老区养老。

大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

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

比例

  1. 分代收集思想Minor GC、Major GC、Full GC

    JVM在进行GC时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区。针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。

    • 部分收集:不是完整收集整个Java堆的垃圾收集,其中又分:

      • 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.。
      • 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.。
      • 混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾。
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

      整堆收集出现的情况:

      • System.gc();时。

      • 老年区空间不足。

      • 方法区空间不足。

      • 开发期间尽量避免整堆收集。

  2. TLAB机制

    1. 为什么有TLAB(Thread Local Allocation Buffer)

      • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
      • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
      • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
    2. 什么是TLAB?

      TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

      如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在之间的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

      JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

      TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

      TLAB

  3. 堆空间的参数设置

    官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

    -XX:+PrintFlagsInitial

    查看所有参数的默认初始值

    -XX:+PrintFlagsFinal

    查看所有参数的最终值(修改后的值)

    -Xms:初始堆空间内存(默认为物理内存的 1/64)

    -Xmx:最大堆空间内存(默认为物理内存的 1/4)

    -Xmn:设置新生代的大小(初始值及最大值)

    -XX:NewRatio:配置新生代与老年代在堆结构的占比

    -XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

    -XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

    -XX:+PrintGCDetails 输出详细的 GC 处理日志

  4. 字符串常量池

    字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。在jdk1.8后,将String常量池放到了堆中。

    String table还存在一个hash表的特性,里面不存在相同的两个字符串,默认容量为1009。当字符串常量池中的存储比较多的字符串时,会导致hash冲突,从而每个节点形成长长的链表,导致性能下降。所以在使用字符串常量池时,一定要控制容量。

    字符串常量池为什么要调整位置?

    JDK7中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC 是老年代的空间不足、永久代不足是才会触发。这就导致String Table回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

6.方法区

  1. 概念

    方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据、static final常量、static变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。

    《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。”所以,方法区看做是一块独立与Java堆的内存空间。

    方法区

    方法区在JVM启动时被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

    方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

    方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误: java.lang.OutOfMemoryError:Metaspace。

    关闭 JVM 就会释放这个区域的内存.

    方法区,栈,堆的交互关系

    三者关系

  2. 方法区大小设置

    Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.

    • 元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的两个参数。
    • 默认值依赖于平台,windows 下,-XXMetaspaceSize是21MB,-XX:MaxMetaspaceSize 的值是-1,级没有限制.
    • 这个-XX:MetaspaceSize初始值是21M也称为高水位线一旦触及就会触发Full GC.
    • 因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高的值。
  3. 方法区内部结构

    内部结构

    方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。

  4. 方法区的垃圾回收

    • 有些人认为方法区是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
    • 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

    方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量不再使用的类型

    回收废弃常量与回收Java堆中的对象非常类似。

    判断一个常量是否“废弃”还是相对简单,而要判定一个类是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    2. 加载该类的类加载器以及被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

7.常见面试题

为什么两个 survivor 区?Eden 和 survior 的比例分配

我们知道,目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成 Eden 空间、 From Survivor 和 To Survivor 三块区域。 

看书的时候有个疑问,为什么非得是两个 Survivor 空间呢?要回答这个问题,其实等价于:为什么不是0个或1个 Survivor 空间?为什么2个 Survivor 空间可以达到要求? 

为什么不是0个 Survivor 空间?

 这个问题等价于:为什么需要 Survivor 空间。我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。 

总之,设置 Survivor 空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。 

为什么不是1个 Survivor 空间?

回答这个问题有一个前提,就是新生代一般都采用复制算法进行垃圾收集。原始的复制算法是把一块内存一分为二, gc 时把存活的对象(Eden和Survivor to)从一块空间(From space)复制到另外一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换 From space 和 To space 的逻辑角色(这样下一次 gc 的时候还可以按这样的方式进行)。 

我们知道,在 HotSpot 虚拟机里, Eden 空间和 Survivor 空间默认的比例是 8:1 。我们来看看在只有一个 Survivor 空间的情况下,这个 8:1 会有什么问题。此处为了方便说明,我们假设新生代一共为 9 MB 。对象优先在 Eden 区分配,当 Eden 空间满 8 MB 时,触发第一次 Minor GC 。比如说有 0.5 MB 的对象存活,那这 0.5 MB 的对象将由 Eden 区向 Survivor 区复制。这次 Minor GC 过后, Eden 区被清理干净, Survivor 区被占用了 0.5 MB ,还剩 0.5 MB 。到这里一切都很美好,但问题马上就来了:从现在开始所有对象将会在这剩下的 0.5 MB 的空间上被分配,很快就会发现空间不足,于是只好触发下一次 Minor GC 。可以看出在这种情况下,当 Survivor 空间作为对象“出生地”的时候,很容易触发 Minor GC ,这种 8:1 的不对称分配不但没能在总体上降低 Minor GC 的频率,还会把 gc 的时间间隔搞得很不平均。把 Eden : Survivor 设成 1 : 1 也一样,每当对象总大小满 5 MB 的时候都必须触发一次 Minor GC ,唯一的变化是 gc 的时间间隔相对平均了。 

上面的论述都是以“新生代使用复制算法”这个既定事实作为前提来讨论的。如果不是这样,比如说新生代采用“标记-清除”或者“标记-整理”算法来实现幸存对象的移动,好像确实是只需要一个 Survivor 就够了。

为什么2个 Survivor 空间可以达到要求?

问题很清楚了,无论 Eden 和 Survivor 的比例怎么设置,在只有一个 Survivor 的情况下,总体上看在新生代空间满一半的时候就会触发一次 Minor GC 。那有没有提升的空间呢?比如说永远在新生代空间满 80% 的时候才触发 Minor GC ? 

事实上是可以做到的:我们可以设两个 Survivor 空间( From Survivor 和 To Survivor )。比如,我们把 Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后: 
1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ; 
2) 清空 Eden 和 From Survivor ; 
3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。 

可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。

说一下 JVM 内存模型吧,有哪些区?分别干什么的?

JVM 内存分布/内存结构?栈和堆的区别?堆的结构?

讲讲 jvm 运行时数据库区

jvm 的方法区中会发生垃圾回收吗?

后续问题解答可看上面。

JVM执行引擎

文章目录

概述

执行引擎是Java虚拟机核心的组成部分之一。

  1. JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM识别的字节码指令、符号表,以及其他辅助信息。
  2. 如果想要让一个Java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
  • 前端编译:从Java程序员-字节码晚间的这个过程叫前端编译。
  • 执行引擎这里由两种行为:一种是解释执行,一种是编译执行(这里的是后端编译)。

Java代码编译和执行过程

编译和执行过程

大部分的程序代码转换为物理机的目标代码或者虚拟机能执行的指令集之前,都需要执行上述步骤。

什么是解释器?什么是JIT编译器?

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。

为什么要保存解释器?

  • 程序启动解释器立即执行。(省略编译时间)
  • 解释执行在编译器进行激进优化不成立时,作为编译器的逃生门。

编译器不确定性

  • Java语言的编译器其实是一段不确定的操作过程,因为它可能是指一个前端编译器(编译器前端)把.java转换为.class文件的过程。
  • 可能指的是后端运行编译器(JIT)把字节码转换为机器码的过程。
  • 还可能是AOT编译器直接把.java文件编译成本地机器代码的过程。

编译器

为什么Java是半编译半解释语言?

起初将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

原因:

JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。

解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行,执行效率低。

JIT编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的JIT代码缓存中(执行效率更高了)。

是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被执行的频率而定。

JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。

什么是热点代码?

  • 多次调用的方法,或者是一个方法体内部循环次数较多的选题都可以称为热点代码。
  • JIT编译器将这些热点代码便以为本地机器指令执行,依靠热点探测功能。
  • 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
  • 基于计数器的热点探测虚拟机有两种不同类型的计数器,分别是方法调用计数器回边计数器
    1. 方法调用计数器用于统计方法的调用次数。
    2. 回边计数器则用于统计循环体执行的循环次数。

JIT编译器执行效率高为什么还需要解释器?

  1. 当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。
  2. 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。

JVM本地方法接口

本地方法接口

文章目录

1.什么是本地方法

一个Native Method就是一个java调用非java代码的接口,一个Native Method是这样一个java方法:该方法的底层实现由非Java语言实现,比如C。这个特征并非Java特有,很多其他的编程语言都有这一机制在定义一个Native Method时,并不提供实现体(有些像定义一个Java interface),因为其实现是由非Java语言在外面实现的。

关键字native可以与其他所有的Java标识符连用,但是abstract除外。

2.为什么使用Native Method

java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者在对程序的效率很在意时,问题就来了。

  1. 与java环境外交互

    有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需区了解Java应用之外的繁琐细节。

  2. 与操作系统交互(比如线程最后要回归于操作系统线程)

    JVM支持着java语言本身和 运行库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一部分就是用C写的。还有,如果我们要使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。

  3. Sun‘s Java

    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的事该类里的本地方法setPriority().这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority()API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

文章目录

JVM-垃圾回收

1.垃圾回收概述

1.概述

在这里插入图片描述

  1. Java和C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动收集。

  2. 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。

  3. 关于垃圾收集有三个经典问题:

    哪些内存需要回收?

    什么时候回收?

    如何回收?

  4. 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。

2.什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

为什么需要GC?

清理内存

3.Java垃圾回收机制

自动内存管理

官网介绍:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html

优点:

  • 降低内存泄漏和内存溢出的风险。

    内存泄漏:指你向系统申请分配内存进行使用(new/malloc),然后系统在堆内存中给这个对象申请一块内存空间,但当我们使用完了却没有归系统(delete),导致这个不使用的对象一直占据内存单元,造成系统将不能再把它分配给需要的程序。

    内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

  • 将程序员从繁重的内存管理中释放出来,专注业务开发。

缺点:

  • 弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。

哪些区域要被回收?

垃圾收集器可以对年轻代、老年代、全栈和方法区的回收,其中Java堆是重点

次数上讲:

  • 频繁收集Young区
  • 较少收集Old区
  • 基本不收集元空间(方法区)

2.垃圾回收相关算法

1.垃圾标记阶段算法

**标记阶段目的:**主要为了判断对象是否存活

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段

一个对象已经不再被任何的存活对象继续引用时,就可以宣判已经死亡。

判断对象存活一般有两种方式:

  • 引用计数算法

    对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况

    对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

    优点

    • 实现简单,垃圾对象便于辨别;
    • 判定效率高,回收没有延迟性。

    缺点

    • 需要单独的字段存储计数器,增加了存储空间的开销。
    • 每次赋值都需要更新计数器,伴随这加法和减法操作,增加了时间开销。
    • 引用计数器有一个严重的问题,即无法处理循环引用的情况。导致java的垃圾回收器中没有使用这类算法。
  • 可达性分析算法(根搜索算法/追踪性垃圾收集)

    优点:

    • 实现简单。
    • 执行高效。
    • 有效解决再引用计数算法中循环引用的问题,反之内存泄漏的发生。

    实现思路

    GCRoots根集合就是一组必须活跃的引用。

    1. 可达性分析算法以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链。
    3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
    4. 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。

    在这里插入图片描述

    GC Roots可以是哪些元素?

    • 虚拟机栈中引用的对象
    • 本地方法栈内JNI(本地方法)引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 所有被同步锁synchronized持有的对象
    • Java虚拟机内部的引用

    **注意:**如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的化分析结果的准确性就无法保证。这点也是导致GC进行时必须“Stop The World”(停止整个程序)的一个重要原因。

  • 对象的finalization机制

    finalize()方法机制(对象销毁前的回调函数)

    Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

    当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

    finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。

    注意:永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。

    原因

    • 在finalize()时可能会导致对象复活。
    • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
    • 一个糟糕的finalize()会严重影响GC的性能。比如finalize是个死循环。
  • 生存还是死亡?

    由于finalize()方法的存在,虚拟机中的对象一般处于三种可能状态。

    • 可触及的: 从根节点开始,可以到达这个对象。
    • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
    • 不可触及的:对象的finalize()被调用,并没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。此时可被回收

具体过程

判断一个对象objA是否可回收,至少要经历两次标记过程:

  1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法。
    • 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要重写执行”,objA被判定为不可触及的。
    • 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
    • finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。
2.垃圾回收阶段算法

JVM中比较常见的三种垃圾收集算法:

  • 标记-清除算法

    执行过程

    当堆中有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作:1.标记2.清除

    • 标记

      Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

    • 清除

      Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

    在这里插入图片描述

    **优点:**容易理解

    **缺点:**效率低;在进行GC的时候,需要停止整个应用程序,用户体验较差;这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表(记录垃圾对象地址)。

    **注意:**这里的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次由新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(覆盖原有的地址)。

  • 复制算法

    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换 两个内存的角色,最后完成垃圾回收。

    在这里插入图片描述

    优点:

    • 实现简单,运行高效。
    • 复制过去以后保证空间的连续性,不会出现”碎片“问题。

    缺点:

    • 内存占用大。
    • 时间开销大。GC需要维护region之间对象的引用关系

    应用场景:如果系统中的垃圾对象很多,需要复制的存活对象数量并不会太大,效率较高。在新生代中回收性价比高。现在的商业虚拟机都是用这种收集算法回收新生代。

3.标记-压缩算法

过程

  • 标记:从根节点开始标记所有被引用对象。
  • 压缩:将所有存活对象压缩到内存的一端,按顺序排放。之后,清理边界外的所有空间。

在这里插入图片描述

优点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中内存减半的高额代价。

缺点:

  • 效率低于复制算法。
  • 移动过程中,需要全程暂停用户应用程序。
  • 移动过程中,需要全程暂停用户应用程序。
总结
标记清除标记压缩复制
速率中等最慢最快
空间开销少(但会堆积碎片)少(不堆积碎片)通常需要活对象的2倍空间(不堆积碎片)
移动对象
4.分代收集算法

为什么要使用分代收集算法?

不同对象的生命周期不一样。因此不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

**年轻代:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。**适合复制算法的回收整理。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

**老年代:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。**适合由标记-清除或者标记-清除与标记-整理的混合实现。

分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

5.增量收集算法

为什么要使用?

上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop The World的状态,应用程序所有线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,将严重影响用户体验或系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

基本思想

每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

缺点:线程切换和上下文转换的消耗,会使垃圾回收成本上升,造成系统吞吐量的下降。

6.分区算法

将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。好处是可以控制一次回收多少个小区间。

在这里插入图片描述

3.垃圾回收相关概念

1.System.gc()

在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)。

JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,立即回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写应该性能基准,我们可以在运行之间调用System.gc()。

2.内存溢出与内存泄漏

内存溢出:没有空闲内存,并且垃圾收集器也无法提供更多内存。

内存泄漏只有对象不会再被程序用到了,但是GC又不能回收他们的情况,叫做严格意义上的内存泄漏。但实际情况很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM,也叫做宽泛意义上的内存泄漏。

比如:

  • 单例模式,单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
  • 一些提高close()的资源未关闭导致内存泄漏。数据库连接、网络连接、io连接。。。
3.Stop The World

是指GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,这个停顿称为STW。

可达性分析算法中枚举根节点会导致所有Java执行线程停顿,为什么需要停顿所有Java执行线程呢?

  • 分析工作必须在一个能确保一致性的快照中进行(一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上)。如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。被STW中断的应用程序线程会在完成GC之后恢复。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

4.对象的引用

在JDK1.2版后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference) 、软引用(Soft Reference) 、弱引用(Weak Reference)、虚引用(Phantom Reference) 。

四种引用强度逐渐减弱。除强引用外,其他三种引用均可以在java.lang.ref包中找到。

在这里插入图片描述

Reference 子类中只有终结器引用是包内可见的,其他 3 种引用类型均为public,可以在应用程序中直接使用.

  • 强引用(默认):是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。造成Java内存泄漏的主要原因之一。
  • 软引用:内存不足即回收。在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:发现即回收。被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
  • 虚引用:对象回收跟踪。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

4.垃圾回收器

线程数分:

  • 串行垃圾回收器:指的是同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
  • 并行垃圾回收器:可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,此时工作线程被暂停,直至垃圾收集工作结束。

工作模式分:

  • 并发式垃圾回收器:与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
  • 独占式垃圾回收器:一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

工作的内存区间分:

  • 年轻代垃圾回收器
  • 老年代垃圾回收器
1.GC性能指标

吞吐量:运行用户代码的时间栈总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间) 。

垃圾收集开销:垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

收集频率:相对于应用程序的执行,收集操作发生的频率。

内存占用:Java 堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

2.HotSpot垃圾收集器

串行回收器:Serial,Serial old

并行回收器:ParNew,Parallel scavenge,Parallel old

并发回收器:CMS、G1

新生代收集器:Serial,ParNew.Parallel scavenge;

老年代收集器:Serial old.Parallel old.cMS;

整堆收集器:G1;

  • Serial垃圾收集器(单线程)

    只开启一条GC线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程。

    适合客户端使用(内存小,堆内存不大,不会创建太多对象)。

    优点:简单高效。

    在这里插入图片描述

  • ParNew垃圾收集器(多线程)

    ParNew是Serial的多线程版本。使用了多线程进行垃圾收集,在多CPU环境下性能比Serial会有一定提升;但线程切换需要额外的开销,因此在单CPU环境中表现不然Serial。

    在这里插入图片描述

  • Parallel Scavenge垃圾收集器(多线程)

    和ParNew一样都是多线程、新生代垃圾收集器。

    不同:

    Parallel Scavenge追求CPU吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。

    ParNew:追求降低用户停顿时间,适合交互式应用。

  • Serial Old垃圾收集器(单线程)

    是Serial的老年版本,都是单线程收集器,只启用一条GC线程,都适合客户端应用。

    唯一区别:Serial Old工作在老年代,使用“标记-整理”算法;Serial工作在新生代,使用复制算法。

  • Parallel Old垃圾收集器(多线程)

    是Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

  • CMS回收器(低延迟)

    CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器,他在垃圾收集时使得用户线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

    • 初始标记:Stop The World,仅使用一条初始标记线程对所有与GC Roots之间关联的对象进行标记。

    • 并发标记:使用多条标记线程,与用户并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。

    • 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发

      标记过程中新出现的废弃对象标记出来。

    • 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记

      的对象。这个过程非常耗时。

    在这里插入图片描述

    优点:

    • 并发收集
    • 低延迟

    缺点:

    • 会产生内存碎片

    • CMS收集器对CPU资源非常敏感。

    • 无法处理浮动垃圾。

  • G1回收器(区域划分代式)

    因为应用程序所应对的业务越来越大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以在Java7 update 4后引入了G1(Garbage-First)。

    **目标:**在延迟可控的情况下获得尽可能高的吞吐量。

    在这里插入图片描述

    G1是一个并行回收器,它把堆内存分割为很多不相关的区域。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。

    G1跟踪各个Region里面的垃圾堆积价值的大小(回收所获得的空间大小以及回收所需时

    间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。侧重点在于回收垃圾最大量的区间。

    G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高的概率满足GC停顿时间的同时,还兼备高吞吐量的性能特征。

    从整体上看,G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片

    一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?

    并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上Remembered Set 即可防止对整个堆内存进行遍历。

    工作过程:

    • 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。

    • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。

    • 最终标记:Stop The World,使用多条标记线程并发执行。

    • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收

      线程并发执行。

JVM性能调优参数

  • -Xss:规定了每个线程虚拟机栈的大小
  • -Xms:堆的初始值
  • -Xmx:堆能达到的最大值

-Xms:设置初始分配大小,默认为物理内存的“1/64”
-Xmx:最大分配内存,默认为物理内存的“1/4”
-Xss规定了每个线程堆栈的大小。一般情况下256K是足够了。影响了此进程中并发线程数大小。

在整个堆内存的调整策略之中,有经验的人基本只会调整两个参数:“-Xmx”(最大内存)、“-Xms”(初始化内存)。如果要取得这些内存的整体信息,直接利用Runtime类即可;

在很多情况下,-Xms和-Xmx设置成一样的。这么设置,是因为当Heap不够用时,会发生内存抖动,影响程序运行稳定性。

JVM内存模型/JMM

JMM(java内存模型)Java Memory Model,本身是一个抽象的概念,不是真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式

JMM同步规定

(1)线程解锁前,必须把共享变量的值刷新回主内存
(2)线程加锁前,必须读取主内存的最新值到自己的工作内存
(3)加锁解锁是同一把锁

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zFseVXtd-1634353769874)(D:\桌面\笔记\1631939243812.png)]

内存溢出,内存泄漏,哪些区域会出现内存溢出OOM

内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
**Java内存泄漏的根本原因是什么呢?**长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:

  • 静态集合类引起内存泄漏:
    像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
  • 当集合里面的对象属性被修改后,再调用remove()方法时不起作用:
    原因是对象属性改变以后hashcode变了
  • 监听器
    在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
  • 各种连接
    比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
  • 内部类和外部模块的引用
    内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:public void registerMsg(Object b);这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
  • 单例模式
    不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏
    内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
  • Java堆溢出:
    触发方法:将内存设置为较小的值,然后一直new对象,或者直接new一个大于内存大小的byte数组。
    解决:首先确认内存中的对象是否是必要的,也就是首先分清楚到底是出现了内存泄漏还是内存溢出。 如果是内存泄漏就找到具体代码,如果是内存溢出就检查内存是否可以再扩大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况。
  • .虚拟机栈和本地方法栈溢出
    StackOverflowError异常通过设置栈内存大小,然后递归调用方法可以触发
    OutOfmemoryError异常通过一直创建线程触发,这个在电脑上没有实验成功,电脑会假死,但程序没有抛出异常
  • 方法区溢出
    触发方式:运行时产生大量的类去填充方法区,直至溢出
  • 运行时常量池溢出
    触发方式:循环创建字符串,但是在实验中发现抛出的是堆溢出
  • 本机直接内存溢出

说说Java栈和堆

栈:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量,for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

堆:存储的是数组和对象,凡是new建立的都是在堆中,堆中存放的都是实体,实体用于封装数据,而且是封装多个,如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。

哪些区域是线程私有,哪些区域是线程共享

线程私有:虚拟机栈、本地方法栈、程序计数器

线程共享:堆、方法区

JVM 年轻代到年老代的晋升过程的判断条件是什么

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

虚拟机给每个对象定义了一个对象年龄计数器,如果对象在eden出生,并经过第一次GC后仍然存活,并且能被Survivor容纳的话,被移动到Survivor空间,并将对象年龄设为1,对象在Survivor区每熬过一次GC年龄就加一岁,当它的年龄增加到一定程度(默认15岁)时,就会晋升到老年代中。

各种回收器,各自优缺点,重点CMS、G1

串行回收器:Serial,Serial old

并行回收器:ParNew,Parallel scavenge,Parallel old

并发回收器:

  • CMS(低延迟)

    CMS收集器是以获取最短回收停顿时间为目标的收集器,它在垃圾收集时使得用户线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

    • 初始标记:Stop The World,仅使用一条初始标记线程对所有与GC Roots之间关联的对象进行标记。
    • 并发标记:使用多条标记线程,与用户并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
    • 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
    • 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记

    的对象。这个过程非常耗时。

  • G1(区域划分代式)

    在延迟可控的情况下获得尽可能高的吞吐量

    G1是一个并行回收器,它把堆内存分割为很多不相关的区域。使用不同的Region来表示Eden、幸存者0区、幸存者1区、老年代等。

    G1跟踪各个Region里面的垃圾堆积价值的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。侧重点在于回收垃圾最大量的区间。

    G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高的概率满足GC停顿时间的同时,还具备高吞吐量的性能特征。

    从整体上看,G1是基于“标记-整理”算法实现的收集器,从局部上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间的碎片。

新生代收集器:Serial,ParNew.Parallel scavenge;

老年代收集器:Serial old.Parallel old.cMS;

整堆收集器:G1;

final finally finalize()区别

finalize()方法机制(对象销毁前的回调函数)

垃圾回收此对象之前,总会调用这个对象的finalize()方法。

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。

虚拟机中的对象一般处于三种可能状态

可触及:从根节点开始,可以到达这个对象。

可复活:对象的所有引用都被释放,但是对象有可能在finalize中复活finalize() 只会调用一次

不可触及:对象的finalize()被调用,并没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。此时可被回收

MySQL数据库

mysql如何做分库分表的?

使用mycat或者shardingsphere中间件做分库分表,选择合适的中间件,水平分库,水平分表,垂直分库,垂直分表

在进行分库分表的时候要尽量遵循以下原则:

1、能不切分尽量不要切分;

2、如果要切分一定要选择合适的切分规则,提前规划好;

3、数据切分尽量通过数据冗余或表分组来降低跨库 Join 的可能;

4、由于数据库中间件对数据 Join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表 Join。

基本操作

select count(1) from table 也是返回表中包括空行和重复行在内的行数,不会扫描所有列,1其实就是表示有多少个符合条件的行,但是此时没有where,所有没条件也就是返回总行数

引擎

1.概述

数据库引擎是用于存储、处理和保护数据的核心服务。利用数据库引擎可控制访问权限并快速处理事务,从而满足企业内大多数需要处理大量数据应用程序的要求。

存储引擎:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。

2.InnoDB与MyISAM

InnoDB:默认的存储引擎。

InnoDB是一个事务型的存储引擎,有行级锁定和外键约束。

InnoDB引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别,该引擎还提供了行级锁和外键约束。

设计目标是处理大容量数据库系统。

MySQL运行是InnoDB会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引(全文索引),而且它没有保存表的行数。

由于锁粒度更小,写操作不会锁定全表,所以在并发较高时,使用InnoDB引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。

使用场景:经常更新的表,适合处理多重并发的更新请求;支持事务;外键约束。只有它支持外键;支持自动增加列属性auto_increment。

MyISAM

MyISAM没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT或UPDATE数据时即写操作需要锁定整个表,效率低。

使用场景:不支持事务的设计,不支持外键的表设计。

MyISAM极度强调快速读取操作。

MyISAM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的只而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyISAM也是很好的选择。

MyISAMInnoDB
非事务安全型的事务安全型的
锁的粒度是表级的支持行级锁定
支持全文类型索引不支持全文类型索引
简单,效率高,适合小型应用复杂,效率低
保存成文件的形式,在跨平台的数据转移中使用MyISAM存储会省去不少麻烦
不安全安全,可以在保证数据不会丢失的情况下,切换非事务表到事务表
管理非事务表。提供高速存储和检索,以及全文搜索能力。适合需要执行大量select查询用于事务处理应用程序,具有众多特性,包括ACID事务支持。适合需要执行大量的INSERT或UPDATE操作
非聚簇索引聚簇索引

索引

索引:在MySQL中,由数据表中一列或多列组合而成。创建索引的目的是为了优化数据库的查询速度。

索引失效的情况:

1、组合索引不遵循最左匹配原则,因此要遵循最左匹配原则

2、组合索引的前面索引列使用范围查询(<,>,like),会导致后续的索引失效

3、不要在索引上做任何操作(计算,函数,类型转换)

4、is null和is not null无法使用索引

5、尽量少使用or操作符,否则连接时索引会失效

6、字符串不添加引号会导致索引失效(隐式类型转化)

7、两表关联使用的条件字段中字段的长度、编码不一致会导致索引失效

8、like语句中,以%开头的模糊查询

9、如果mysql中使用全表扫描比使用索引快,也会导致索引失效

1.为什么使用索引

在查询大量数据时,逐条查询效率太低,而索引类似于书的目录,在查找内容时借助索引,执行查询时不必扫描整个表就能快速地找到所需要的数据。

2.优点

大大加快数据的检索速度,降低数据库IO成本。

通过索引列对数据进行排序,降低数据排序成本,降低了CPU的消耗。

3.缺点

占用磁盘空间大。

提高查询速度,但是降低了更新表的速度。因为更新表时,MySQL不仅要保存数据,还要保存索引文件,每次更新添加了索引列的字段,都会调整因为更新带来的键值变化后的索引信息。

4.索引分类

  • 主键索引:设定为主键后数据库会自动建立索引。
ALTER TABLE 表名 add PRIMARY KEY 表名(列名); 
删除建主键索引: 
ALTER TABLE 表名 drop PRIMARY KEY ;
  • 单值索引:即一个索引只包含单个列,一个表可以有多个单列索引

    创建单值索引
    CREATE INDEX 索引名 ON 表名(列名); 
    删除索引: 
    DROP INDEX 索引名 ON 表名;
    
  • 唯一索引:索引列的值必须唯一,允许为null

    CREATE UNIQUE INDEX 索引名 ON 表名(列名); 
    删除索引 
    DROP INDEX 索引名 ON 表名;
    
  • 复合索引:即一个索引包含多个列,在数据库操作期间,复合索引比单值索引所需要的开销更小(对相同的多个列键索引),当表的行数远大于索引列的数目时可以使用复合索引

    创建复合索引
    CREATE INDEX 索引名 ON 表名(列 1,列 2...); 
    删除索引: 
    DROP INDEX 索引名 ON 表名;
    
  • 查看索引:

    SHOW INDEX FROM 表名;
    

5.索引创建原则

哪些情况需要创建索引?
  • 主键自动建立唯一索引
  • 频繁作为查询条件的字段(where后面的语句)
  • 查询中与其他表关联的字段,外键关系建立索引
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
哪些情况不需要创建索引?
  • 表记录太少
  • 经常增删改的表
  • where条件里用不到的字段
  • 数据重复且分布均匀的表字段(性别)

6.索引数据结构(B树)

B树和B+树的出现是因为磁盘IO,IO操作的效率很低,当大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作。平衡二叉树由于树圣都过大而造成磁盘IO读写过于频繁,进而导致效率低下。

为了减少磁盘IO的次数,就必须降低树的深度。

B树:

  • 数据分布在整棵树中。
  • 数据只出现在一个节点中,且不能重复

索引底层是B+树

B+Tree是在B-Tree基础上的一种优化,适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现索引结构。

特点:

  • 非叶子节点不存储数据,只存储索引,可以放更多的索引。
  • 所有叶子节点之间都有一个链指针。
  • 数据记录都存放在叶子节点中。

7.聚簇索引和非聚簇索引(辅助索引)

  • 聚簇索引:找到了索引就找到了需要的数据,那么这个索引就是聚簇索引。主键就是聚簇索引。用的B+树

    按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚簇索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。

    Innodb通过主键聚集数据,如果没有定义主键,Innodb会选择非空的唯一索引代替。如果没有这样的索引,INNODB会隐式的定义一个主键来作为聚簇索引。

    优点:

    1. 数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快。
    2. 聚簇索引对于主键的排序查找和范围查找速度非常快。

    缺点:

    1. 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于Innodb表,我们一般都会定义一个自增的ID列为主键。
    2. 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于Innodb表,我们一般定义主键为不可更新。
    3. 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
  • 非聚簇索引:索引的存储和数据的存储是分离的,即找到了索引但没找到数据,需要根据索引上的值再次回表查询。用的B+树

事务

1.概述

事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成

即一次对十几块的一个完整操作过程,这个过程中包含一次或多次操作,要么都成立,要么都失败。

事务用来管理insert,update,delete语句。

2.事务特性

  • 原子性:一个事务中的操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即系统故障也不会丢失。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改能力,隔离性可以防止多个事务并发执行时,由于交叉执行而导致数据的不一致。事务隔离级别:读未提交、读提交、可重复读和串行化。
  • 一致性:事务在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的数据必须完全符合所有预设规则。原子性、持久性和隔离性都是为了保证数据库状态的一致性。

原子性和持久性是怎么保证的?

原子性通过undolog来实现,持久性通过redo log来实现

3.事务设置

默认情况下,MySQL启动自动提交模式(变量auto commit为ON)。只要执行DML操作语句,MySQL会立即隐式提交事务。

变量autocommit分会话系统变量与全局系统变量。

事务处理方法:

  • 用BEGIN,ROLLBACK,COMMIT来实现。

    BEGIN; 
    / START TRANSACTION; 开始一个事务 
    ROLLBACK 事务回滚 
    COMMIT 事务确认
    
  • 直接用SET来改变MySQL的自动提交模式

    SET SESSION / GLOBAL autocommit=0; 禁止自动提交 
    SET SESSION / GLOBAL autocommit=1;开启自动提交
    

查看auto commit模式

SHOW SESSION / GLOBAL VARIABLES LIKE 'autocommit'
并发事务处理带来的问题
  • 脏读:读到其他事务未提交的数据。

    比如:

    1. 事务B更新年龄18
    2. 事务A读取数据库信息,年龄是18
    3. 事务B回滚

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auN9H9E1-1634353769876)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631629828411.png)]

    这时候事务A读取到了18,不合理。

  • 不可重复读:在事务A中先后两次读取同一数据,两次读取的结果不一样。即在同一事务中两次读取的数据不一致

    比如

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aKC4dD1q-1634353769877)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631631348986.png)]

  • 幻读:在事务A中按照某种条件先后两次查询数据库,两次查询结果的条件不同。数据的行数变量。一般幻读出现在范围查询。

    比如:

    1. 事务A读取年龄大于15的数据,发现有一条记录
    2. 事务B插入一条记录,并提交
    3. 事务A在读取年龄大于15的数据,发现有两条记录

4.事务隔离级别

只有InnoDB支持事务,所以这里说的事务隔离级别是指InnoDB下的事务隔离级别。

  • 读未提交:一个事务可以读取到另一个事务未提交的修改。

    问题:导致脏读,幻读,不可重复读。

  • 读已提交:一个事务只能读取另一个事务已经提交的修改。

    问题:避免了脏读,仍然存在不可重复读和幻读问题。

  • 可重复读:同一个事务中多次读取相同的数据返回的结果是一样的。

    问题:避免了脏读和不可重复读问题,但是幻读依然存在。

  • 串行化:事务串行执行。避免了以上所有问题

1.概述

隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。

基本原理:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

2.行锁与表锁

按照锁度,锁可以分为表锁、行锁以及其他位于两者之间的锁。

表锁在操作数据时会锁定整张表,并发性能较差。MyISAM支持,InnoDB支持。

行锁只锁定需要操作的数据,并发性能号。InnoDB支持。

行锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。能大大减少数据库操作的冲突。由于加锁粒度最小,导致加锁的开销也最大。

特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

行级锁分为共享锁和排他锁。

共享锁(S):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A,但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这样保证了其他事务可以读A,但T释放A上的S锁之前不能对A做任何修改。

排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

注意:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select…for update语句,加共享锁可以使用select…lock in share mode 语句。

表锁:

表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。表级锁定分为表共享锁与表排他锁。

特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率高,并发度低。

底层机制MVCC

MVCC多版本并发控制,是MySQL提高性能的一种方式,配合Undo log和版本链,替代锁,让不同事务的读-写,写-读操作可以并发执行,从而提升系统性能。一般在使用读已提交和可重复读隔离级别的事务中实现。

MVCC在MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

2、当前读

读取的是最新版本

像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

3、快照读(提高数据库的并发查询能力)

读取的是历史数据

像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑

快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

4、当前读、快照读、MVCC关系

MVCC多版本并发控制指的是维持一个数据的多个版本,使得读写操作没有冲突,快照读是MySQL为实现MVCC的一个非阻塞读功能。MVCC模块在MySQL中的具体实现是由三个组件实现的:隐式字段,undo日志、read view

基本原理
  • 事务

  • 版本链

    对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。

    • trx_id:每次对某条聚簇索引记录进行修改时,都会把对应的事务id赋值给trx_id隐藏列。
    • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

    基本特征

    每行数据都存在一个版本,每次数据更新时都更新该版本。

    修改时Copy出当前版本随意修改,各个事务之间无干扰。

    保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)。

    假设插入该记录的事务id为80,那么此刻该条记录的示意图如下所示:

    在这里插入图片描述

    假设之后两个 id 分别为 100、200 的事务对这条记录进行 UPDATE 操作,操作流程如下:在这里插入图片描述

    每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

    在这里插入图片描述

    对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id,这个信息很重要。

  • ReadView

    对于使用读未提交隔离级别的事务来说,直接读取记录的最新版本。

    对于使用串行化隔离级别的事务来说,使用加锁的方式来访问记录。

    对于使用读已提交和可重复读隔离级别的事务来说,就需要版本链。但是需要判断版本链中的哪个版本是当前事务可见的。

    所以InnoDB中设计了一个ReadView的概念,主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为m_ids。在开始一次会话进行SQL读写时,开始事务时生成readview时,会把当前系统中正在执行的写事务写入到m_ids列表中,另外还会存储两个值:

    • min_trx_id:该值代表生成readview时m_ids中的最小值
    • max_trx_id:该值代表生成readview时系统中应该分配给下一个事务的id值。

    可见性判断的步骤:

    • 如果记录的trx_id列小于min_trx_id,说明肯定可见。
    • 如果记录的trx_id列大于max_trx_id,说明肯定不可见。
    • 如果记录的trx_id列在min_trx_id和max_trx_id之间,就要看一下该trx_id在不在m_ids列表中,如果在,说明不可见,否则可见。

    如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该记录对该事务不可见,查询结果就不包含该记录。

    在 MySQL 中,读已提交和可重复读隔离级别的的 一个非常大的区别就是它们生成 ReadView 的时机不同。

    **读已提交:**每次读取数据前都生成一个 ReadView

    **可重复读:**在第一次读取数据时生成一个 ReadView

视图

1.概述

视图是基于查询的虚拟表,即一条SELECT语句执行后返回的结果集。

SELECT语句所查询的表称为视图的基表,而查询的结果集称为虚拟表,视图本身并不存储具体的数据,视图的数据存在于视图的基表中,基本表数据发生了改变,视图的数据也会跟着改变。

2.为什么使用

使用视图是为了方便复杂的查询语句。

基本思路是将复杂的查询语句定义在视图内部,然后对视图进行查询,从而简化复杂的查询语句。

3.语法

定义视图

CREATE VIEW 视图名 AS SELECT 列1,列2... FROM 表(查询语句)

使用视图

SELECT * FROM 视图名

删除视图

DROP VIEW 视图名

存储过程

1.概述

把编写在数据库中的SQL语句集称为存储过程。

存储过程是事先警告编译并存储在数据库中的一段SQL语句的集合。

存储过程类似于Java语言中的方法,需要先定义,使用时需要调用。

存储过程可以定义参数,参数分为IN、OUT、INOUT三种类型。

  • IN类型的参数表示接收调用者传入的数据
  • OUT类型的参数表示向调用者返回数据
  • INOUT类型的参数既可以接收调用者传入的参数,也可以向调用者返回参数
2.语法

创建存储过程

create procedure 存储过程名([in 变量名 类型,out 参数 2,...])
begin 
 [declare 变量名 类型 [DEFAULT 值];]
 存储过程语句块
end;

解析:

  • 存储过程的参数分为 in,out,inout 三种类型。
  • in 代表输入参数(默认情况下为 in 参数),表示该参数的值必须由调用程序指定。
  • out 代表输出参数,表示该参数的值经存储过程计算后,将 out 参数的计算结果返回给调用程序。
  • inout 代表即是输入参数,又是输出参数,表示该参数的值即可以由调用程序指定,又可以将 inout 参数的计算结果返回给调用程序。
  • 存储过程中的语句必须包含在begin和end之间。
  • declare中用来声明变量,变量默认赋值使用default,语句块中改变变量值,使用set变量=值。

例如:

定义一个存储过程

-- 开始位置
DELIMITER$$
CREATE PROCEDURE test()
BEGIN
  -- 声明变量
  DECLARE v_name VARCHAR(20) DEFAULT 'jim';
    set v_name = 'tom';-- 变量赋值
    SELECT v_name;-- 测试输出语句
END$$-- 结束位置

-- 调用存储过程
CALL test()

定义一个有参数的存储过程

DELIMITER$$
CREATE PROCEDURE findUserCount(IN p_type,OUT p_count INT)
BEGIN
  -- 把sql中查询的结果赋给变量
  SELECT COUNT(*) INTO P_COUNT FROM USER WHERE TYPE = p_type;
  SELECT p_count;
END$$

测试
CALL findUserCount(1,@p_count);-- @p_count测试输出参数

流程控制语句 if else

DELIMITER$$
CREATE PROCEDURE test(IN p_day INT)
BEGIN
  IF p_day = 0 THEN
    SELECT "星期天";
  ELSEIF p_day = 1 THEN
    SELECT "星期一";
  ELSEIF p_day = 2 THEN
    SELECT "星期二";
  ELSE 
    SELECT "无效日期";
  END IF;
END$$

测试
CALL test(2)

case when

DELIMITER$$
CREATE PROCEDURE test3(IN p_day INT)
BEGIN 
  CASE WHEN p_day = 0 THEN
    SELECT "星期天";
  WHEN p_day = 1 THEN
    SELECT "星期一";
  ELSE 
    SELECT "星期二";
  END CASE;
END$$

测试
CALL test2(2);

循环

DELIMITER$$
CREATE PROCEDURE test4()
BEGIN 
  DECLARE v_num INT DEFAULT 0;
  -- 循环开始
  addnum : LOOP
    SET v_num = v_num+1; -- 循环语句
  IF v_num = 10 THEN
    LEAVE addnum; -- Leave语句表明退出指定标签的流程控制语句块
  END IF;
  END LOOP;-- 循环结束
  SELECT v_num;
END$$

测试
call test4()

使用存储过程插入信息

DELIMITER$$
CREATE PROCEDURE saveUser(IN p_account VARCHAR(20),IN p_sex CHAR(1),OUT res_mark INT) 
BEGIN 
  DECLARE v_count INT DEFAULT 0; 
    SELECT COUNT(*) INTO v_count FROM t_user WHERE account = p_account; 
    IF v_count = 0 THEN 
        INSERT INTO t_user(account,sex)VALUES(p_account,p_sex); 
        SET res_mark = 0; 
      ELSE
        SET res_mark = 1; 
    END IF; 
END$$

函数

1.语法
create function 函数名([参数列表]) returns 数据类型
begin
  declare 变量;
    sql语言;
  return 值;
end;

注意

  • 参数列表包含两部分:参数名 参数类型
  • 函数体:肯定会有return语句,如果没有会报错
  • 函数体中仅有一句话,则可以省略begin end
  • 使用delimter语句设置结束标记

设置函数可以没有参数

set global log_bin_trust_function_creators=TRUE;

删除函数

DROP FUNCTION 函数;
不带参数
DELIMITER$$
CREATE FUNCTION test6() RETURNS INT
BEGIN 
  DECLARE v_num INT;
  SELECT COUNT(*) INTO v_num FROM t_user;
  RETURN v_num;
END$$
带参数
DELIMITER$$ 
CREATE FUNCTION findDeptNameById(p_id INT) RETURNS 
VARCHAR(10) 
BEGINDE
  CLARE v_name VARCHAR(10); 
  SELECT NAME INTO v_name FROM dept WHERE id = p_id; 
  RETURN v_name; 
END$$ 

SELECT account,findDeptNameById(dept_id) FROM USER
有参数,有判断
DELIMITER$$ 
CREATE FUNCTION checkUserType(p_type INT) RETURNS VARCHAR(4) 
BEGIN
  IF p_type = 0 THEN
    RETURN '管理员'; 
  ELSE 
    RETURN '业务用户'; 
  END IF; 
END$$ 

SELECT tu.account,checkUserType(tu.type)utype FROM user tu

触发器

1.概述

触发器是一种特殊的存储过程,其特殊性在于它并不需要用户直接调用,而是在对表添加、修改、删除之前或者之后自动执行的存储过程。

2.特点
  • 与表相关联。触发器定义在特定的表上,这个表称为触发器表。
  • 自动激活触发器。如果对表上的这个特定操作定义了触发器,该触发器自动执行,不可撤销。
  • 不能直接调用。与存储过程不同,触发器不能被直接调用,也不能传递或接受参数。
  • 作为事务的一部分。触发器与激活触发器的语句一起作为对一个单一的事务来对待,可以从触发器中的任何位置回滚。
3.语法

定义触发器

create trigger 触发器名称 触发时机 触发事件
on 表名称
for each row -- 行级触发
begin
  语句
end;
  • 触发器名称:用来标识触发器的,由用户自定义。
  • 触发时机:before之前或者after之后。
  • 触发事件:值是insert,update和dalete。
  • 表名称:标识建立触发器的表名,即在哪张表上建立触发器。
  • 语句:触发器的程序体,触发器程序可以使用begin和end作为开始和结束,中间包含多条语句。

例如:删除用户时,自动触发删除用户菜单关系

DELIMITER $$ 
CREATE TRIGGER delete_user_menu BEFORE DELETE ON t_user 
FOR EACH ROW 
BEGIN 
  DELETE FROM t_user_menu WHERE user_id = old.id; 
END$$;

新增用户时,自动向其他表插入数据

DELIMITER $$ 
CREATE TRIGGER save_user_log AFTER INSERT ON user 
FOR EACH ROW 
BEGIN 
  INSERT INTO test(id,NAME)VALUES(new.id,new.account); 
END$$; 
  
INSERT INTO user(account)VALUES('jim')

在行级触发器代码中,可以用old和new访问到该行的旧数据和新数据,old和new是对应表的行记录类型变量。

SQL优化

方法
  • 对查询进行优化,应尽量避免全表扫描,首先应考虑在where及order by涉及的列上建立索引。

  • 尽量避免索引失效

    • 在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

      select id from t where num is null

      可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

      select id from t where num = 0

    • 尽量避免在where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

    • 尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

      select id from t where num = 10 or num 20

    • in 和 not in 也要慎用,否则会导致全表扫描

      对于连续的数值,能用between就不要用in了

    • 下面的查询也将导致全表查询:select id from t where name like ‘%abc%’

    • 应尽量避免在where子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。

      如:select id from t where num/2=100

      应改为:select id from t where num = 100*2

    • 尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。

      如:select id from t where substring(name,1,3)=‘abc’–name 以 abc 开头的id

      应改为:select id from t where name like ‘abc%’

  • 一个表的索引数最好不要超过6个,若太多则应该考虑一些不常使用到的列上建的索引是否有必要。

  • 尽量使用数字型字段,因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次。

  • 尽可能使用varchar代替char,可以节省空间,在相对小的字段内搜索效率高。

  • 不要使用select * from t,用具体的字段列表代替“*”,不要返回用不到的字段。

  • 避免向客户端返回大数据量。

执行计划

在这里插入图片描述

EXPLAIN关键字

模拟优化器执行SQL语句,分析查询语句或是结构的性能瓶颈。

在select语句之前增加explain关键字,MySQL会查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行SQL。

EXPLAIN SELECT * FROM USER WHERE id= 1

在这里插入图片描述

测试期间设置关闭对衍生表的合并优化

SET SESSION optimizer_switch = 'derived_merge=off';

EXPLAIN SELECT t.* FROM(SELECT * FROM USER u WHERE u.type=1)t

在这里插入图片描述

expain 出来的信息有 12 列,分别是 id、select_type、table、type、 possible_keys、key、key_len、ref、rows、Extra

概述描述:

  • id:选择标识符。

    select的查询序列号。

    id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行。

  • select_type:表示查询中每个select子句的类型。

    • SIMPLE(简单SELECT,不使用UNION或子查询等)
    • PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
    • UNION(UNION 中的第二个或后面的 SELECT 语句)
    • DEPENDENT UNION(UNION 中的第二个或后面的 SELECT 语句,取决于外面的查询)
    • UNION RESULT(UNION 的结果,union 语句中第二个 select 开始后面所有 select)
    • SUBQUERY(子查询中的第一个 SELECT,结果不依赖于外部查询)
    • DEPENDENT SUBQUERY(子查询中的第一个 SELECT,依赖于外部查询)
    • DERIVED(派生表的 SELECT, FROM 子句的子查询)
    • UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
  • table:输出结果集的表。

  • partitions:匹配的分区。

  • type:表示表的连接类型。对表访问方式,表示 MySQL 在表中找到所需行的方式,又称“访问类型”。

    常用的类型有: ALL、index、range、 ref、eq_ref、const、system、NULL (从左到右,性能从差到好)

    • ALL:Full Table Scan, MySQL 将遍历全表以找到匹配的行
    • index: Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树
    • range:只检索给定范围的行,使用一个索引来选择行
    • ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
    • eq_ref: 类似 ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用 primary key 或者 unique key 作为关联条件
    • const、system: 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量,system 是 const 类型的特例,当查询的表只有一行的情况下,使用system
    • NULL: MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
  • possible_keys:表示查询时,可能使用的索引。

  • key:表示实际使用的索引。

  • key_len:索引字段的长度。

  • ref:列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。

  • rows:扫描出的行数。

  • filtered:按表条件过滤的行百分比。

  • Extra:执行情况的描述和说明 。

mysql面试题

sql join原理?

MySQL是只支持一种Join算法Nested-Loop Join(嵌套循环连接),并不支持哈希连接和合并连接,不过在mysql中包含了多种变种,能够帮助MySQL提高join执行的效率。

1、Simple Nested-Loop Join

这个算法相对来说就是很简单了,从驱动表中取出R1匹配S表所有列,然后R2,R3,直到将R表中的所有数据匹配完,然后合并数据,可以看到这种算法要对S表进行RN次访问,虽然简单,但是相对来说开销还是太大了。

2、Index Nested-Loop Join

索引嵌套联系由于非驱动表上有索引,所以比较的时候不再需要一条条记录进行比较,而可以通过索引来减少比较,从而加速查询。这也就是平时我们在做关联查询的时候必须要求关联字段有索引的一个主要原因。

这种算法在链接查询的时候,驱动表会根据关联字段的索引进行查找,当在索引上找到了符合的值,再回表进行查询,也就是只有当匹配到索引以后才会进行回表。至于驱动表的选择,MySQL优化器一般情况下是会选择记录数少的作为驱动表,但是当SQL特别复杂的时候不排除会出现错误选择。

在索引嵌套链接的方式下,如果非驱动表的关联键是主键的话,这样来说性能就会非常的高,如果不是主键的话,关联起来如果返回的行数很多的话,效率就会特别的低,因为要多次的回表操作。先关联索引,然后根据二级索引的主键ID进行回表的操作。这样来说的话性能相对就会很差。

3、Block Nested-Loop Join

在有索引的情况下,MySQL会尝试去使用Index Nested-Loop Join算法,在有些情况下,可能Join的列就是没有索引,那么这时MySQL的选择绝对不会是最先介绍的Simple Nested-Loop Join算法,而是会优先使用Block Nested-Loop Join的算法。

Block Nested-Loop Join对比Simple Nested-Loop Join多了一个中间处理的过程,也就是join buffer,使用join buffer将驱动表的查询JOIN相关列都 给缓冲到了JOIN BUFFER当中,然后批量与非驱动表进行比较,这也来实现的话,可以将多次比较合并到一次,降低了非驱动表的访问频率。也就是只需要访问一次S表。这样来说的话,就不会出现多次访问非驱动表的情况了,也只有这种情况下才会访问join buffer。

在MySQL当中,我们可以通过参数join_buffer_size来设置join buffer的值,然后再进行操作。默认情况下join_buffer_size=256K,在查找的时候MySQL会将所有的需要的列缓存到join buffer当中,包括select的列,而不是仅仅只缓存关联列。在一个有N个JOIN关联的SQL当中会在执行时候分配N-1个join buffer。

innodb为什么要用自增id作为主键

结合索引的数据结构, 自增可以保证连续,查询快

数据库三范式

第一范式:列不可再分
  • 每一列属性都是不可再分的属性值,确保每一列的原子性
  • 两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据

比如有一个学生表,假设有两个字段分别是 name,address,而address内容写的是:江苏省南京市浦口区xxx街道xxx小区。如果这时来一个需求,需要按省市区分类,显然不符需求,这样的表结构也不是符合第一范式的。

应该设计成 name,province(省),city(市),area(区),address

第二范式属性完全依赖于主键

第二范式是在第一范式的基础上建立起来的,即满足第二范式必须先满足第一范式

第二范式要求数据库表中的每个实例或行必须可以唯一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的唯一标识。这个唯一属性列被称为主键。

每一行的数据只能与其中一列相关,即一行数据只做一件事,只要数据列中出现重复的数据,那么就要把表拆分开。

例:

有一个订单表如下:

orderId(订单编号),roomId(房间号), name(联系人), phone(联系电话),idn(身份证)

如果这时候一个人同时订了好几个房间,就会变成一个订单编号对应多条数据,这样子联系人都是重复的,就会造成数据冗余,这时我们应该把拆分开来。

如:

订单表:

orderId(订单编号),roomId(房间号), peoId(联系人编号)

联系人表:

peoId(联系人编号),name(联系人), phone(联系电话),idn(身份证)

第三范式属性不依赖与其他非主键,属性直接依赖与主键

第三范式是在第二范式的基础上建立起来的。

任何字段不能由其他字段派生出来,他要求字段没有冗余。

例如:

假设有一个员工(employee)表,它有九个属性:id(员工编号)、name(员工名称)、mobile(电话)、zip(邮编)、province(省份)、city(城市)、district(区县)、deptNo(所属部门编号)、deptName(所属部门名称)

员工表的province、city、district依赖于zip,而zip依赖于id,换句话说,province、city、district传递依赖于id,违反了 3NF 规则。为了满足第三范式的条件,可以将这个表拆分成employee和zip两个表,如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQCVtr2t-1634353769878)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765613785.png)]

如何做 MySQL 的性能优化

字段类型优化

索引优化

sql优化

CHAR和VARCHAR和TEXT的区别?

  • 区别一,定长和变长
    char 表示定长,长度固定,varchar表示变长,即长度可变。char如果插入的长度小于定义长度时,则用空格填充;varchar小于定义长度时,还是按实际长度存储,插入多长就存多长。

    因为其长度固定,char的存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好相反,以时间换空间。

  • 区别之二,存储的容量不同
    对 char 来说,最多能存放的字符个数 255,和编码无关。
    而 varchar 呢,最多能存放 65532 个字符。varchar的最大有效长度由最大行大小和使用的字符集确定。整体最大长度是 65,532字节。

  • text

    跟varchar基本相同, 理论上最多保存65535个字符, 实际上text占用内存空间最大也是65535个字节; 考虑到字符编码方式, 一个字符占用多个字节, text并不能存放那么多字符; 跟varchar的区别是text需要2个字节空间记录字段的总字节数。

    PS: 由于varchar查询速度更快, 能用varchar的时候就不用text。

    顺便提一句: 当表有成百上千万条数据时, 就要使用MySQL的分区(partition)功能, 原理有点像分治算法,就是将数据切割成多个部分。

NOW()和CURRENT_DATE()有什么区别?

  • DATE

    提取日期或日期时间表达式expr中的日期部分。

  • NOW()

    返回当前为’YYYY-MM-DD HH:MM:SS’或YYYYMMDDHHMMSS的日期和时间格式的一个值,根据函数是否用在字符串或数字语境中。该值表示在当前时区。

now() 年月日时分秒

CURRENT_DATE() 年月日

说一说drop、delete与truncate的区别

SQL中的drop、delete、truncate都表示删除,
但是三者有一些差别
delete和truncate只删除表的数据不删除表的结构
速度,一般来说: drop> truncate >delete
delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效;
如果有相应的trigger,执行的时候将被触发.
truncate,drop是ddl, 操作立即生效,原数据不放到rollbacksegment中,
不能回滚. 操作不触发trigger

文章目录

redis概述

持久化机制

Redis对数据的操作都是基于内存的,当遇到了进程退出、服务器宕机等意外情况,如果没有持久化机制,那么Redis中的数据将会丢失无法恢复。有了持久化机制,Redis在下次重启时可以利用之前持久化的文件进行数据恢复。理解和掌握Redis的持久机制,对于Redis的日常开发和运维都有很大帮助,也是在大厂面试经常被问到的知识点。Redis支持的两种持久化机制:

  1. RDB:把当前数据生成快照保存在硬盘上。
  2. AOF:记录每次对数据的操作到硬盘上。

RDB持久化

RDB持久化即通过创建快照(压缩的二进制文件)的方式进行持久化,保存某个时间点的全量数据。RDB持久化是Redis默认的持久化方式。RDB持久化的触发包括手动触发与自动触发两种方式。

AOF持久化

AOF(Append-Only-File)持久化即记录所有变更数据库状态的指令,以append的形式追加保存到AOF文件中。在服务器下次启动时,就可以通过载入和执行AOF文件中保存的命令,来还原服务器关闭前的数据库状态。

RDB、AOF混合持久化

Redis从4.0版开始支持RDB与AOF的混合持久化方案。首先由RDB定期完成内存快照的备份,然后再由AOF完成两次RDB之间的数据备份,由这两部分共同构成持久化文件。该方案的优点是充分利用了RDB加载快、备份文件小及AOF尽可能不丢数据的特性。缺点是兼容性差,一旦开启了混合持久化,在4.0之前的版本都不识别该持久化文件,同时由于前部分是RDB格式,阅读性较低。

Redis 持久化方案的建议
如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。

如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。

通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。

Redis 持久化方案的优缺点
RDB持久化

优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。

缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。

AOF持久化

与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。

数据一致性

当进行修改或保持、删除之后,redis中的数据页应该进行相应变化,不然用户再次查询的时候很可能查询出已经删除过的脏数据。

例如:保存了一个新用户之后,就应该同时在redis缓存中也插入该条数据,更新了某条数据在缓存中也应该同步更新,而redis默认的做法是:当你不去设置的时候redis中存放的一值是你之前存放的数据,只有在重启服务器的时候数据才会同步,显然这是非常不可取的,如果是这样的话岂不是每时每刻都要重启服务器,那将是多么大的灾难!

解决方法
  1. 当我们在进行插入操作之后,我们把该条数据取出来同时保存到redis缓存中去,这样再次查询缓存的时候我们也可以看到新的数据。
  2. 定期清除redis中的数据,例如设置一个定时任务,每当一个小时的时候就会清除redis中的数据,也就是让redis中的数据失效,然后再次保存、删除的时候之前的 redis中的数据已经不存在,所以相当于是将数据重新设置到redis中去,所以可以保证数据的一致性。

1.是什么?

Redis是一个完全开源免费的,遵循BSD协议,使用C语言编写的,支持网络交互的,内存中的Key-Value数据结构存储系统,它可以用作数据库、缓存和消息中间件。

2.特点

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash,等数据结构的存储。
  • Redis支持数据的备份,及master-slave模式的数据备份。

3.优势

  • 性能极高-读写速度快
  • 丰富的数据类型
  • 原子性-Redis所有操作都是原子性的,对数据的修改要么成功全部执行,要么失败全部不执行。
  • 丰富的特性-可用于缓存,消息,按key设置过期时间,过期后将会自动删除

4.Redis是单进程单线程的?

Redis是单进程单线程的,redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

5.关系型数据库与非关系性数据库

  • 关系型数据库

    采用关系模型来组织数据的数据库,关系模型就是二维表格模型。一张二维表的表名就是关系,二维表中的一行就是一条记录,二维表的一列就是一个字段。

    优点:

    • 易于维护:都是使用表结构,格式一致。
    • 使用方便:SQL语言通用。
    • 复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。

    缺点:

    • 读写性能比较差,尤其是海量数据的高效率读写。

    • 固定的表结构,灵活度稍欠。

    • 高并发读写需求,传统关系型数据库来说,磁盘I/O是并发的瓶颈。

    • 横向扩展困难,无法简单的通过添加硬件和服务节点来扩展性能和负载能力。

    • 当需要对数据库进行升级和扩展时,需要停机维护和数据迁移 。

    • 多表的关联查询以及复杂的数据分析类型的复杂 sq 查询,性能欠佳。因为要

      保证 ACID.

  • 非关系型数据库

    非关系型,分布式,一般不保证遵循ACID原则的数据存储系统。键值对存储,结构不固定。

    优点:

    • 格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,使用灵活,应用场景广泛。
    • 速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘。
    • 高扩展性。
    • 成本低:noslq数据库部署简单,基本都是开源软件。

    缺点:

    • 无事务处理。
    • 不适合复杂查询的数据,只适合存储简单数据。
    • 不适合持久存储海量数据。

6.Redis中过期的key是怎么被删除的?

过期key有三种删除方式:

  1. **定时删除:**在设置键的过期时间的同时,创建一个定时器。当键的过期时间来临时,立即执行对键的删除操作。

    **缺点:**对CPU不友好,当过期键比较多的时候,Redis线程用来删除过期键,会影响正常请求的响应

  2. **惰性删除:**每次获取键的时候,判断键是否过期,如果过期的话,就删除该键,如果没有过期,则返回该键。

    **优点:**对CPU比较友好

    **缺点:**会浪费大量的内存。如果一个key设置过期时间放到内存中,但是没有被访问到,那么它会一直存在内存中。

  3. **定期删除:**每隔一段时间,对键进行一次检查,删除里面的过期键。

    **优点:**对CPU和内存都比较友好。

Redis过期key的删除策略

  1. 惰性删除

    客户端在访问key的时候,对key的过期时间进行校验,如果过期了就立即删除

  2. 定期删除

    Redis会将设置了过期时间的key放在一个独立的字典中,定时遍历这个字典来删除过期的key,遍历策略如下:

    1. 每秒进行10次过期扫描,每次从过期字典中随机选出20个key。
    2. 删除20个key中已经过期的key。
    3. 如果过期key的比例超过1/4,则进行步骤一。
    4. 每次扫描时间的上限默认不超过25ms,避免线程卡死。

**注意:**因为Redis中过期的key是由主线程删除的,为了不阻塞用户的请求,所以删除过期key的时候是少量多次。

2.数据类型

1.string(字符串)

string是redis最基本的类型,一个key对应一个value。

string类型是二进制安全的。意思是redis的sring可以包含任何数据。比如jpg图片或者序列化的对象。

string类型的值最大能存储512MB

set key value
get key

实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9HL7bdM-1634353769879)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608597177.png)]

2.hash(哈希)

redis hash是一个键值(key>=value)对集合

redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

hset key field value
hget key field

实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Go2Y7HL4-1634353769880)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608605479.png)]

3.list(列表)

Redis列表是最简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的又不或者尾部。

lpush key element
rpush key element
lrange key start stop

实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5KDWVE3r-1634353769880)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608617694.png)]

4.set(集合)

redis的set是string类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是o(1)。

sadd命令

添加一个string元素到key对应的set集合中,成功返回1,如果元素已经在集合中返回0.

根据集合内元素的唯一性,第二次插入的元素将被忽略。

sadd key member
smembers key

实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oe43D9BQ-1634353769881)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608629962.png)]

5.zset(有序集合)

zset和set一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数却是可以重复的。

zadd命令  添加元素到集合,元素在集合中存在则更新对应score
zadd key score member
zrangebyscore key min max

实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lCwVKfTc-1634353769882)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608642490.png)]

6.设置失效时间

redis提供了一些命令,能够让我们对key设置过期时间,并且让key过期之后自动删除。

设置值时之间设置有效时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GG5YB202-1634353769883)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608653997.png)]

EX 表示以秒为单位
PX 表示以毫秒为单位 
EX,PX 不区分大小写 
set name jim EX 30 设置失效时间为 30 秒 
ttl 键 查看剩余时间(秒) 
pttl 键 查看剩余时间(毫秒)

设置值后设置有效时间

expire 键 时间(秒) 
pexpire 键 时间(毫秒)

3.springboot集成使用redis

Jedis是Redis官方推出的一款面向Java的客户端,提供了很多接口供Java语言调用。

Spring-data-redis 是 spring 大家族的一部分,提供了在 srping 应用中通过简单的配置访问 redis 服务,对 reids 底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate 提供了 redis 各种操作。

spring-data-redis 针对 jedis 提供了如下功能:

  1. 连接池自动管理,提供了一个高度封装的”RedisTemplate“类。

  2. 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口。

    ValueOperations:简单 K-V 操作

    SetOperations:set 类型数据操作

    ZSetOperations:zset 类型数据操作

    HashOperations:针对 map 类型的数据操作

    ListOperations:针对 list 类型的数据操作

  3. 将事务操作封装,有容器控制。

  4. 针对数据的”序列化/反序列化“,提供了多种可选择策略

    JdkSerializationRedisSerializer:POJO 对象的存取场景,使用 JDK 本身序列化机制。

    StringRedisSerializer:Key 或者 value 为字符串的场景,根据指定的charset 对数据的字节序列编码成 string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。

    JacksonJsonRedisSerializer:jackson-json 工具提供了 javabean 与 json 之间的转换能力,可以将 pojo 实例序列化成 json 格式存储在 redis 中,也可以将json 格式的数据转换成 pojo 实例。

4.主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。

使用一个Redis实例作为主机,其余的作为备份机。主机和备份机的数据完全一致,主机支持数据的写入和读取等各项操作,而从机则只支持与主机数据的同步和读取。也就是说,客户端可以将数据写入到主机,由主机自动将数据的写入操作同步到从机。主从模式很好的解决了数据备份问题,并且由于主从服务数据几乎是一致的,因而可以将写入数据的命令发送给主机执行,而读取数据的命令发送给不同的从机执行,从而达到读写分离的目的。

作用:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

5.哨兵机制

1、哨兵的介绍

sentinal,中文名是哨兵

哨兵是redis集群架构中非常重要的一个组件,主要功能如下:

(1)集群监控,负责监控redis master和slave进程是否正常工作。

(2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。

(3)故障转移,如果master node挂掉了,会自动转移到slave node上。

(4)配置中心,如果故障转移发生了,通知client客户端新的master地址。

哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作:

(1)故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。

(2)即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

目前采用的是sentinal 2版本,sentinal 2相对于sentinal 1来说,重写了很多代码,主要是让故障转移的机制和算法变得更加健壮和简单

2、哨兵的核心知识

(1)哨兵至少需要3个实例,来保证自己的健壮性。

(2)哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性。

(3)对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

3、为什么redis哨兵集群只有2个节点无法正常工作?

哨兵集群必须部署2个以上节点,如果哨兵集群仅仅部署了个2个哨兵实例,quorum=1

±—+ ±—+
| M1 |---------| R1 |
| S1 | | S2 |
±—+ ±—+
Configuration: quorum = 1

master宕机,s1和s2中只要有1个哨兵认为master宕机就可以进行切换,同时s1和s2中会选举出一个哨兵来执行故障转移,同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移。但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行。

4、经典的3节点哨兵集群

​ ±—+
​ | M1 |
​ | S1 |
​ ±—+
​ |
±—+ | ±—+
| R2 |----±—| R3 |
| S2 | | S3 |
±—+ ±—+
Configuration: quorum = 2,majority

如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和S3可以一致认为master宕机,然后选举出一个来执行故障转移,同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移。

5、redis两种数据丢失的情况

1.异步复制导致的数据丢失(主备切换的过程,可能会导致数据丢失)
因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtBQT0hh-1634353769885)(D:\桌面\笔记\1632105763965.png)]

2.脑裂导致的数据丢失
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VECdOu0Y-1634353769886)(D:\桌面\笔记\1632105788556.png)]

此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了,因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。

6、解决异步复制和脑裂导致的数据丢失

1.redis配置文件
上面两个配置可以减少异步复制和脑裂导致的数据丢失:

min-slaves-to-write 1# 要求至少有1个slave,数据复制和同步的延迟不能超过10秒
min-slaves-max-lag 10#如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了
2.减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GnO92Qi3-1634353769886)(D:\桌面\笔记\1632105807868.png)]

3.减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失10秒的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3NKTrE3-1634353769887)(D:\桌面\笔记\1632105823070.png)]

7、哨兵的自动切换机制

1.sdown和odown转换机制
sdown和odown两种失败状态sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机。odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机。

sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机。sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机。

2.哨兵集群的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的sentinel:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置。每个哨兵也会去监听自己监控的每个master+slaves对应的sentinel:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在。每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。

3.slave配置的自动纠正
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上。

4.slave->master选举算法
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来。会考虑slave的一些信息:

(1)跟master断开连接的时长。

(2)slave优先级。

(3)复制offset。

(4)run id。

如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master:

(down-after-milliseconds * 10) +milliseconds_since_master_is_in_SDOWN_state
接下来会对slave进行排序:

(1)按照slave优先级进行排序,slave priority越低,优先级就越高。

(2)如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高。

(3)如果上面两个条件都相同,那么选择一个run id比较小的那个slave。

5.quorum和majority
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换。如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换。但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换。

6.configuration epoch
哨兵会对一套redis master+slave进行监控,有相应的监控的配置。执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。

7.configuraiton传播
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制。这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的。其他的哨兵都是根据版本号的大小来更新自己的master配置的。

6.缓存穿透、缓存击穿、缓存雪崩

1.缓存处理流程

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没有取到,那直接返回空结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-76imGFw2-1634353769887)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608771108.png)]

2.缓存穿透

key对应的数据在数据库中并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据库,从而可能压垮数据库。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

key数据库没有,缓存没有

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jsuJDKPM-1634353769888)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608696253.png)]

解决办法:

  1. 使用布隆过滤器或者压缩filter提前拦截
  2. 将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间。
  3. 拉黑该IP地址。
  4. 对参数进行校验,不合法参数进行拦截。
布隆过滤器

**组成:**由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。

**作用:**用于检索一个元素是否在一个集合中。

**优点:**空间效率和查询时间都比一般算法要好的多。

**缺点:**有一定的误识别率和删除困难。

**基本思想:**它可以通过一个Hash函数将一个元素映射成一个位阵列中的一个点。我们只要看这个点是不是1就可以知道集合中有没有它了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QASGfdp5-1634353769889)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608785410.png)]

算法:

  1. 首先需要k个hash函数,每个函数可以把 key 散列成为 1 个整数 。
  2. 初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0 。
  3. 某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1。
  4. 判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是 1,认为在集合中。

3.缓存击穿

某个key对应的数据库中存在,但在redis中的某个时间节点过期了,此时若有大量并发请求过来,这些请求发现缓存过期,都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决办法:

  1. 热点数据设置永不过期
  2. 加上互斥锁:上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后将数据放到redis缓存起来。后面的线程进来发现已经有缓存了,就直接走缓存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNDsqhD2-1634353769890)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608795915.png)]

4.缓存雪崩

缓存雪崩是指,在高并发情况下,大量的缓存失效,或者缓存层出现故障。于是所有的请求都会达到数据库,数据库的调用量会暴增,造成数据库也会挂掉的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63icYI7f-1634353769890)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631608812202.png)]

解决办法:

  1. 随机设置key失效时间,避免大量key集体失效。
  2. 若是集群部署,可将热点数据均匀分布在不同的Redis库中也能够避免key全部失效问题。
  3. 不设置过期时间。
  4. 跑定时任务,在缓存生效前刷进新的缓存。

总结

雪崩是大面积的key缓存失效;穿透是redis里不存在这个缓存key;击穿是redis某个热点key突然失效,最终的受害者都是数据库。

对于”Redis宕机请求,全部走数据库“这种情况,我们可以有以下的思路:

**事发前:**实现Redis的高可用(主从架构+Sentinel(哨兵)),尽量避免Redis挂掉这种情况发送。

**事发中:**万一Redis真的挂了,我们可用设置本地缓存+限流,尽量避免我们的数据库被干掉。

**事发后:**redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

nginx

概述

1.Nginx是什么?

Nginx是高性能的HTTP和反向代理的Web服务器,处理高并发能力十分强大。

Nginx专为性能优化而开发,性能是服务器最重要的考量,实现上非常注重效率,能经受高负载的考验,有报告表明能支持高达50000个并发连接数。

在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

Nginx不仅能做反向代理,实现负载均衡;还可以作正向代理来进行上网等功能。

2.特点

  • 占有内存少
  • 并发能力强

3.代理服务器

所谓代理服务器就是位于发起请求的客户端与原始服务器端之间的一台跳板服务器,正向代理可以隐藏客户端,反向代理可以隐藏原始服务器。

4.正向代理

如果把局域网外的Internet想象成一个巨大的资源库,则局域网中的客户端要访问Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理

用户知道目标服务器地址,但由于网络限制等原因,无法直接访问。这时候需要先连接代理服务器,然后在由代理服务器访问目标服务器。

正向代理

5.反向代理

反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问。

我们只需要将请求发送到反向代理服务器,由反向代理服务器取选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。
反向代理

6.负载均衡

客户端发送多个请求到服务器,服务器处理请求,有些可能要访问数据库,服务器处理完毕后再将结果返回客户端。

这种架构模式单一,适合并发请求少的情况,但并发量大的时候如何解决?

  • 增加服务器的数量,然后将请求发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同服务器,也就是我们所说的负载均衡。

负载均衡

负载均衡的调度算法:

  1. 轮询

    按时间顺序逐一分配到不同的后端服务器。

  2. 加权轮询

    可再配置的server后面加个weight=number,number值越高,分配的概率越大。

  3. ip_hash

    每个请求按访问IP的hash分配,这样来自同一IP固定访问一个后台服务器。

  4. least_hash

    最少链接树,哪个机器连接数少就分发给哪个机器。

7.动静分离

Nginx是一个静态资源服务器,为了加快网站的解析速度,可以把动态页面和静态页面有不同的服务器来解析,减少服务器压力,加快解析速度。将java后端程序部署在独立的服务器上,nginx代理访问后端访问。

实现动态请求与静态请求分离,实现资源分类。

动静分离

Linux

一、目录操作

pwd				查看当前工作目录
clear 			清除屏幕
cd ~			当前用户目录
cd /			根目录
cd -			上一次访问的目录
cd ..			上一级目录

查看目录内信息

ll				查看当前目录下内容(LL的小写)

创建目录

mkdir aaa		在当前目录下创建aaa目录,相对路径;
mkdir ./bbb		在当前目录下创建bbb目录,相对路径;
mkdir /ccc		在根目录下创建ccc目录,绝对路径;

递归创建目录(会创建里面没有的目录文件夹)

mkdir -p temp/nginx 

搜索命令

find / -name 'b'		查询根目录下(包括子目录),名以b的目录和文件;
find / -name 'b*'		查询根目录下(包括子目录),名以b开头的目录和文件; 

重命名

mv 原先目录 文件的名称   mv tomcat001 tomcat 

剪切命令(有目录剪切到制定目录下,没有的话剪切为指定目录)

mv	/aaa /bbb			将根目录下的aaa目录,移动到bbb目录下,在bbb,麚也叫aaa目录;
mv	bbb usr/bbb			将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为bbb;

复制目录

cp -r /aaa /bbb			将/目录下的aaa目录复制到/bbb目录下,在/bbb目录下的名称为aaa
cp -r /aaa /bbb/aaa		将/目录下的aa目录复制到/bbb目录下,且修改名为aaa;

强制式删除指定目录

rm -rf /bbb			强制删除/目录下的bbb目录。如果bbb目录中还有子目录,也会被强制删除,不会提示;

删除目录

rm -r /bbb			普通删除。会询问你是否删除每一个文件

二、文件操作
删除

rm -r a.java		删除当前目录下的a.java文件(每次回询问是否删除y:同意)

强制删除

rm -rf a.java		强制删除当前目录下的a.java文件
rm -rf ./a*			强制删除当前目录下以a开头的所有文件;
rm -rf ./*			强制删除当前目录下所有文件(慎用);

创建文件

创建文件

touch testFile

递归删除.pyc格式的文件

find . -name '*.pyc' -exec rm -rf {} \;

打印当前文件夹下指定大小的文件

打印当前文件夹下指定大小的文件

find . -name "*" -size 145800c -print

递归删除指定大小的文件(145800)

find . -name "*" -size 145800c -exec rm -rf {} \;

递归删除指定大小的文件,并打印出来

find . -name "*" -size 145800c -print -exec rm -rf {} \;
"." 表示从当前目录开始递归查找
“ -name '*.exe' "根据名称来查找,要查找所有以.exe结尾的文件夹或者文件
" -type f "查找的类型为文件
"-print" 输出查找的文件目录名
-size 145800c 指定文件的大小
-exec rm -rf {} \; 递归删除(前面查询出来的结果)

三、文件内容操作(查看日志,更改配置文件)
修改文件内容

三、文件内容操作(查看日志,更改配置文件)
修改文件内容

vim a.java   	进入一般模式
i(按键)   		进入插入模式(编辑模式)
ESC(按键)  		退出
:wq 			保存退出(shift+:调起输入框)
:q!			不保存退出(shift+:调起输入框)(内容更改)
:q				不保存退出(shift+:调起输入框)(没有内容更改)

文件内容的查看

cat a.java		查看a.java文件的最后一页内容;
more a.java		从第一页开始查看a.java文件内容,按回车键一行一行进行查看,
                    按空格键一页一页进行查看,q退出;
less a.java		从第一页开始查看a.java文件内容,按回车键一行一行的看,
                    按空格键一页一页的看,支持使用PageDown和PageUp翻页,q退出;

总结下more 和 less的区别:

less可以按键盘上下方向键显示上下内容,more不能通过上下方向键控制显示
less不必读整个文件,加载速度会比more更快
less退出后shell不会留下刚显示的内容,而more退出后会在shell上留下刚显示的内容.
由于more不能后退.、

实时查看文件后几行(实时查看日志)

tail -f a.java			查看a.java文件的后10行内容;

前后几行查看

head a.java				查看a.java文件的前10行内容;
tail -f a.java			查看a.java文件的后10行内容;
head -n 7 a.java		查看a.java文件的前7行内容;
tail -n 7 a.java		查看a.java文件的后7行内容;

文件内部搜索指定的内容

grep under 123.txt			在123.txt文件中搜索under字符串,大小写敏感,显示行;
grep -n under 123.txt		在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;
grep -v under 123.txt		在123.txt文件中搜索under字符串,大小写敏感,显示没搜索到的行;
grep -i under 123.txt		在123.txt文件中搜索under字符串,大小写敏感,显示行;
grep -ni under 123.txt		在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;

终止当前操作

Ctrl+c和Ctrl+z都是中断命令,但是作用却不一样。

ctrl+z
ctrl+c

Ctrl+Z就扮演了类似的角色,将任务中断,但是任务并没有结束,在进程中只是维持挂起的状态,用户可以使用fg/bg操作前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行。
Ctrl+C也扮演类似的角色,强制中断程序的执行。

重定向功能
可以使用 > 或 < 将命令的输出的命令重定向到test.txt文件中(没有则创建一个)

echo 'Hello World' > /root/test.txt

四、系统日志位置

cat /etc/redhat-release		查看操作系统版本
/var/log/message			系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
/var/log/message			系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一 
/var/log/secure				与安全相关的日志信息 
/var/log/maillog			与邮件相关的日志信息 
/var/log/cron				与定时任务相关的日志信息 
/var/log/spooler			与UUCP和news设备相关的日志信息 
/var/log/boot.log			守护进程启动和停止相关的日志消息 

查看某文件下的用户操作日志
到达操作的目录下,执行下面的程序:

查看某文件下的用户操作日志
到达操作的目录下,执行下面的程序:

cat .bash_history

五、创建与删除软连接
1、创建软连接

ln -s /usr/local/app /data

注意:创建软连接时,data目录后不加 / (加上后是查找其下一级目录);

2、删除软连接

rm -rf /data

注意:取消软连接最后没有/,rm -rf 软连接。加上/是删除文件夹;

六、压缩和解压缩
tar

tar -zcvf start.tar.gz a.java b.java	将当前目录下a.java、b.java打包
tar -zcvf start.tar.gz ./*				将当前目录下的所欲文件打包压缩成haha.tar.gz文件
tar -xvf start.tar.gz				解压start.tar.gz压缩包,到当前文件夹下;
tar -xvf start.tar.gz -C usr/local(C为大写,中间无空格)
									解压start.tar.gz压缩包,到/usr/local目录下;

解压缩tar.xz文件

1
2
3
解压缩tar.xz文件

tar xf node-v12.18.1-linux-x64.tar.xz

unzip

unzip file1.zip  				解压一个zip格式压缩包
zip lib.zip tomcat.jar			将单个文件压缩(lib.zip)
zip -r lib.zip lib/				将目录进行压缩(lib.zip)
zip -r lib.zip tomcat-embed.jar xml-aps.jar		将多个文件压缩为zip文件(lib.zip)	

将english.zip包,解压到指定目录下/usr/app/

unzip -d /usr/app/com.lydms.english.zip

七、Linux下文件的详细信息

 R:Read  w:write  x: execute执行
-rw-r--r-- 1 root root  34942 Jan 19  2018 bootstrap.jar
前三位代表当前用户对文件权限:可以读/可以写/不能执行
中间三位代表当前组的其他用户对当前文件的操作权限:可以读/不能写/不能执行
后三位其他用户对当前文件权限:可以读/不能写/不能执行

七、Linux下文件的详细信息
R:Read w:write x: execute执行
-rw-r–r-- 1 root root 34942 Jan 19 2018 bootstrap.jar
前三位代表当前用户对文件权限:可以读/可以写/不能执行
中间三位代表当前组的其他用户对当前文件的操作权限:可以读/不能写/不能执行
后三位其他用户对当前文件权限:可以读/不能写/不能执行

更改文件的权限

chmod u+x web.xml (---x------)		为文件拥有者(user)添加执行权限;
chmod g+x web.xml (------x---)		为文件拥有者所在组(group)添加执行权限;
chmod 111 web.xml  (---x--x--x)	为所有用户分类,添加可执行权限;
chmod 222 web.xml (--w--w--w-)		为所有用户分类,添加可写入权限;	
chmod 444 web.xml (-r--r--r--)		为所有用户分类,添加可读取权限;

九、运维常用命令
1、查看服务器端口号是否可用
查看服务器是否可用

ping 49.32.587.164

查看服务器指定端口是否可用

telnet 49.32.587.164 8093

Telnet安装

1、shutdown(关闭计算机)

shutdown是最常用也是最安全的关机和重启命令,它会在关机之前调用fsck检查磁盘,其中-h和-r是最常用的参数:

-h:停止系统服务并关机  
-r: 停止系统服务后重启  

案例:

案例:

shutdown -h now  --立即关机  
shutdown -h 10:53  --到10:53关机,如果该时间小于当前时间,则到隔天  
shutdown -h +10  --10分钟后自动关机  
shutdown -r now  --立即重启  
shutdown -r +30 'The System Will Reboot in 30 Mins'   --30分钟后重启并并发送通知给其它在线用户  

2、查看处于各种连接状态数量(ESTABLISHED、CLOSE_WAIT、TIME_WAIT)

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

查看处于ESTABLISHED状态连接

netstat -nt | awk '{if($NF=="ESTABLISHED"){wait[$5]++}}END{for(i in wait) print i,wait[i]}'

查看处于CLOSE_WAIT状态连接

netstat -nt | awk '{if($NF=="CLOSE_WAIT"){wait[$5]++}}END{for(i in wait) print i,wait[i]}'

查看处于TIME_WAIT状态连接

netstat -nt | awk '{if($NF=="TIME_WAIT"){wait[$5]++}}END{for(i in wait) print i,wait[i]}'

3、ping命令
对 www.lydms.com 发送 4 个 ping 包, 检查与其是否联通

3、ping命令
对 www.lydms.com 发送 4 个 ping 包, 检查与其是否联通

ping -c 4 www.lydms.com

4、netstat 命令
netstat 命令用于显示各种网络相关信息,如网络连接, 路由表, 接口状态等等;
列出所有处于监听状态的tcp端口:

netstat -lt

查看所有的端口信息, 包括 PID 和进程名称

netstat -tulpn

5、查看当前端口号占用情况
1.用于查看某一端口的占用情况

lsof -i:8080

2.显示tcp,udp的端口和进程等相关情况

2.显示tcp,udp的端口和进程等相关情况

netstat -tunlp

3.指定端口号的进程情况

netstat -tunlp|grep 8080

4.查看PID进程信息

ps -aux |grep 28990

根据PID,查看JVM中各线程信息('0x9eb’为nid值)

jstack 2246|grep '0x9eb' -A 50

6、ps 命令
过滤得到当前系统中的 ssh 进程信息

ps aux | grep 'ssh'

7、管道命令
简单来说, Linux 中管道的作用是将上一个命令的输出作为下一个命令的输入, 像 pipe 一样将各个命令串联起来执行, 管道的操作符是 |
管道命令查看当前运行的程序中,名称为java的程序

ps -ef|grep java

查看/etc/passwd文件中的root内容

cat /etc/passwd | grep 'root'

查看当前系统的ip连接(Windows和Linux通用)

netstat -an

将sh test.sh任务放到后台,并将打印的日志输出到nohup.out文件中,终端不再能够接收任何输入(标准输入)

nohup sh test.sh  &

将sh test.sh任务放到后台,并将打印的日志输出到nohup.out文件中,终端能够接收任何输入

nohup sh test.sh  &

8、添加Host地址
打开配置文件

vim /etc/hosts

在打开的文件中添加

49.235.32.164 www.lydms.com

保存文件后,重启网络

/etc/init.d/network restart

重新加载成功:

十、yum常用命令

yum install iptables-services		下载并安装iptables
yum list					列出当前系统中安装的所有包
yum search package_name		在rpm仓库中搜寻软件包
yum update package_name.rpm		更新当前系统中所有安装的rpm包
yum update package_name		更新一个rpm包
yum remove package_name		删除一个rpm包
yum clean all				删除所有缓存的包和头文件

十一、其他命令
查看占用资源

ps -au		占用的资源是从进程启动开始,计算的平均占用资源,比如cpu等
top			实时占用的资源;

查看当前目录所占存储

du -lh			查看当前文件下各文件夹占用存储空间
du -sh			查看当前文件夹所占存储空间
du --max-depth=<目录层数> 	超过指定层数的目录后,予以忽略。
du --max-depth=1 			只查看当前目录下文件占用的存储空间

管道命令:
根据项目查看进程,更加PID查看项目,以及项目路径

ps -ef 						查看所有的进程
ps -ef | grep mysql			查看mysql相关的进程

通过进程PID查看所占用的端口号

netstat -nap |grep 进程ID(PID)

查看Linux下系统存储使用率

df -h			查看系统硬盘使用情况

杀死进程(根据PID)

kill -9 2630		进程pid

关闭防火墙

service iptables stop      临时关闭防火墙
chkconfig iptables off     防火墙开启不启动
service iptables status    查看防火墙状态

开机启动选项

msconfig					查看开机启动选项
chkconfig					查看开机启动服务列表

查看MySQL服务的程序的状态

service mysql start        开启MySQL    
service mysql status       查看MySQL的状态    
service mysql stop         关闭MySQL    

十二、Linux内核优化
打开配置文件

vim /etc/sysctl.conf

加载新的配置(需开启防火墙iptables,否则会报错)

sysctl -p

十三、用户权限操作
1、添加用户
添加用户sum:

useradd –d /usr/sum -m sum

关于useradd的某些参数:

-u: 指定 UID,这个 UID 必须是大于等于500,并没有其他用户占用的 UID

-g: 指定默认组,可以是 GID 或者 GROUPNAME,同样也必须真实存在

-G: 指定额外组

-c: 指定用户的注释信息

-d: 指定用户的家目录

已创建的用户sum设置密码

passwd sum

新建的用户在面显示

cat /etc/passwd

删除用户sum

userdel sum

删除用户文件夹

rm -rf /usr/sum

切换下刚才添加的用户

su sum

回到root用户

回到root用户

exit

2、添加组
添加用户组

groupadd groupname

删除用户组

groupdel groupname

可以看到自己的分组和分组id

可以看到自己的分组和分组id

cat /etc/group

sum: x:1000:1000:: /usr/sum :/bin/bash
sum: x:0:1000:: /usr/sum :/bin/bash

十四、TOP
实时占用的资源:

top

top命令执行结果分为两个区域:统计信息区和进程信息区

1、统计信息区
TOP:任务队列信息,与uptime命令执行结果相同.

15:33:39:系统时间
up 5:40:主机已运行时间
2 users:用户连接数(不是用户数,who命令)
load average: 1.09, 1.04, 0.98:系统平均负载,统计最近1,5,15分钟的系统平均负载
Tasks:进程信息

123 total:进程总数
3 running:正在运行的进程数
120 sleeping:睡眠的进程数
0 stopped:停止的进程数
0 zombie:僵尸进程数
%CPU(s):CPU信息(当有多个CPU时,这些内容可能会超过两行)

42.1 us:用户空间所占CPU百分比
2.0 sy:内核空间占用CPU百分比
0.0 ni:用户进程空间内改变过优先级的进程占用CPU百分比
49.2 id:空闲CPU百分比
0.0 wa:等待输入输出的CPU时间百分比
6.0 hi:硬件CPU终端占用百分比
0.7 si:软中断占用百分比
0.0 st:虚拟机占用百分比
KiB Mem:内存信息(与第五行的信息类似与free命令类似)

3780.9 total:物理内存总量
727.4 free:已使用的内存总量
668.8 used:空闲的内存总量(free + userd = total)
2384.7 buff/cache:用作内核缓存的内存量
KiB:swap信息

2048.0 total:交换分区总量
2046.0 free:已使用的交换分区总量
2.0 used:空闲交换分区总量
859.6 avail:缓冲的交换区总量,内存中的内容被换出到交换区,然后又被换入到内存,但是使用过的交换区没有被覆盖,交换区的这些内容已存在于内存中的交换区的大小,相应的内存再次被换出时可不必再对交换区写入。
2、进程信息区
PID:进程id

USER:进程所有者的用户名

PR:优先级

NI:nice值。负值表示高优先级,正值表示低优先级

RES:进程使用的、未被换出的物理内存的大小

%CPU:上次更新到现在的CPU时间占用百分比

%MEM:进程使用的物理内存百分比

TIME+:进程所使用的CPU时间总计,单位1/100秒

COMMAND:命令名/行

PPID:父进程id

RUSER:Real user name(看了好多,都是这样写,也不知道和user有什么区别,欢迎补充此处)

UID:进程所有者的id

VIRT:进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES

GROUP:进程所有者的组名

TTY:启动进程的终端名。不是从终端启动的进程则显示为?

NI:nice值。负值表示高优先级,正值表示低优先级

P:最后使用的CPU,仅在多CPU环境下有意义

TIME:进程使用的CPU时间总计,单位秒

SWAP:进程使用的虚拟内存中被被换出的大小

CODE:可执行代码占用的物理内存大小

DATA:可执行代码以外的部分(数据段+栈)占用的物理内存大小

SHR:共享内存大小

nFLT:页面错误次数

nDRT:最后一次写入到现在,被修改过的页面数

S:进程状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)

WCHAN:若该进程在睡眠,则显示睡眠中的系统函数名

Flags:任务标志

maven

1.概述

Maven是Apache软件基金会的一个开源项目,它用来帮助开发者管理项目中的jar,以及jar之间的依赖关系、完成项目的编译、测试、打包和开发等工作。

Maven 中的概念

Pom(Project Object Model 项目对象模型)

Maven管理的项目的根目录下都有一个 pom.xml 文件。pom.xml 文件指示 Maven如何工作。

在 pom.xml 文件中配置项目基本信息以及项目构建信息等。比如:项目坐标、项目依赖的 jar、插件、编译选项等。

一旦在 pom.xml 文件中配置了所依赖的 jar,Maven 会自动从构件仓库中下 载相应的构件

项目坐标

maven 给每个 jar 定义了唯一的标志,这个在 maven 中叫做项目的坐标,通过这个坐标可以找到你需要 用到的任何版本的 jar 包。

groupId、artifactId、packaging、version 的组合被称为项目的坐标,它们形成了项目的唯一标识,Maven通过坐标来精确定位构件。其中 groupId、artifactId、version 是必须的,且这三项的值必须唯一,packaging 是可选的(默认为 jar)。

仓库

中央仓库全球共享,先将 jar 从中央仓库下载到本地仓库,然后在项目中引用本地仓库的 jar.

2.maven 命令

  1. compile 编译

  2. clean 删除 target

  3. test test case junit/testNG

  4. package 打包

  5. install 把项目 install 到本地仓库

3.Maven父子工程依赖jar传递方式

  1. 如果父项目pom中使用的是:

    <dependencies>
     
         ....
     
    </dependencies>
    

    则子项目pom会自动使用pom中的jar包。

    如果你需要子类工程直接自动引用父类的jar包,可以使用这种管理方法

  2. 如果父项目pom使用

    <dependencyManagement>
     
         <dependencies>
     
              ....
     
         </dependencies>
     
    </dependencyManagement>
    

    则子项目pom不会自动使用父pom中的jar包,

    如果需要使用,就要给出groupId和artifactId,无需给出version

使用是为了统一管理版本信息

在子工程中使用时,还是需要引入坐标的,但是不需要给出version

在我们项目顶层的POM文件中,元素。

通过它元素来管理jar包的版本,

让子项目中引用一个依赖而不用显示的列出版本号。

Maven会沿着父子层次向上找,

直到找到一个拥有dependencyManagement元素的项目,

然后它就会使用在这个dependencyManagement元素中指定的版本号。

Git

什么是版本控制

版本控制是指对软件开发过程中各种程序代码、配置文件说明文档等文件变更的管理,是软件配置管理的核心思想之一。

版本控制最主要的功能就是追踪文件的变更。它将什么时候、什么人更改了文件的什么内容等信息记录下来。每一次文件的改变,文件的版本号都将增加。除了记录版本变更外,版本控制的另一个重要功能是并行开发。软件开发往往是多人协同作业,版本控制可以有效地解决版本的同步以及不同开发者之间的开发通信问题,提高协调开发的效率。

1.概念

Git是一个免费的开源分布式版本控制系统,旨在快速高效地处理从小型到大型项目的所有内容。

Git 易于学习, 占地面积小,具有闪电般的快速性能。它具有诸如 Subver sion,CVS 之类的版本控制工具,具有廉价的本地分支,便捷的暂存区域和 多个工作流等功能。

集中式版本控制

集中化的版本控制系统诸如CVS,SVN等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。

**好处:**每个人都可以一定程度上看到项目中的其他人正在做些什么。而管理员也可以轻松掌握每个开发者的权限。

**缺点:**中央服务器的单点故障,如果服务器宕机一小时,那么在这一小时内,谁都无法提交更新,无法协同工作。

分布式版本控制

客户端提取的不是最新版本的文件快照,而是把代码仓库完整地镜像下来(本地库),这样任何一处协同工作用的文件发生故障,事后都可以用其他客户端的本地仓库进行恢复。因为每个客户端的每一次文件提取操作,实际上都是一次对整个文件仓库的完整备份。分布式的版本控制系统出现之后,解决了集中式版本控制系统的缺陷。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxFJ7Obb-1634353769891)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631428572817.png)]

工作区:在本地磁盘创建项目

暂存区:一般存放在.git目录下的index文件中,添加工作区代码到暂存区,暂存区代码是临时存储,可以撤销的。

版本库:工作区有一个隐藏目录.git,是Git的版本库,提交暂存区代码到本地仓库,生成历史版本记录,历史版本记录不可删除,可以查看不同时期提交的历史记录,和其他版本作比较。

流程:

  1. 工作区代码添加到暂存区
  2. 暂存区代码提交到本地版本库
  3. 本地版本库代码推送到远程仓库
  4. 从远程仓库拉取代码到本地

2.GIT基本操作

  • 全局配置用户名

    git config – global user.name “nameVal”

  • 全局配置邮箱

    git config – global user.email “eamil”

  • 查看git配置信息

    git config – list

  • 创建仓库

    E:\gitTest 自己创建文件夹

  • 命令行窗口进入所在目录(同级)

  • 初始化

    git init 仓库名

  • 提交到暂存区:

    git add 文件名(提交指定文件)

    git add .(提交所有文件)

    git add -a(提交所有变化到暂存区)

  • 查看文件变化

    git add -p

  • 查看暂存区

    git ls -files

  • 恢复暂存区的指定文件到工作区

    git reset 文件名 撤销指定文件

    git reset . 撤销所有文件

  • 提交到本地仓库

    git commit -m注释

  • 查看操作日志

    git log

  • 提交本地仓库到远程仓库

    git remote add origin 地址

    git push -u origin master

  • 从远程仓库拉取修改的文件

    git pull origin master(分支名)

  • 查看仓库状态

    git status

  • 克隆项目

    git clone 地址

项目

没有项目实战经验

把知识点应用,技术服务业务.

生活----设计–>软件

​ 技术应用,业务设计,团队协作,遇到问题解决方法,将一些新的技术应用 redis

需求的诞生

物流 出租车

文章管理系统 ----- 发信息

切合校园生活— >有自己的想法融入;

需求分析

自主设计需求分析

使用到的技术

springboot mybatis-plus mysql redis vue elementUI tomcat nginx

任务分工

组长 负责项目搭建 任务分配 进度跟踪,积极讨论问题,请教老师

如何沟通

​ 有问题积极沟通,有问题10分钟搞不定,找别人.

你的职责

​ 开发的相关模块进行描述

如何解决问题

上网查看资料(官网 api ),博客,搜一些技术问题

组内,班级,老师请教

简历

照片

检查电话,邮箱

教育背景

专业课

技能

​ 熟练掌握java面向对象程序设计语言,具有良好编码规范

​ 了解JVM,并发编程,设计模式

​ 熟练掌握servlet,jsp等javaweb开发.

熟练掌握html,css.js,jquery,vue,elementUI等前端技术

spring,springboot,mybatis

idea ,hbuilder,maven,git项目管理工具

mysql,redis非关系数据库

数据结构算法

掌握linux常用命令,能在linux环境下部署项目

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值