JAVA知识点总结

基础模块

JavaOOP三大特征

封装:把某事物的属性和行为包装到对象中,这个对象只对外公布需要公开的属性和行为。

继承:继承是子类继承父类的非私有属性和方法,子类可以对父类方法进行重写拓展。能够起到复用父类代码的效果

多态:多态是指对象的多种表现形态,即不同对象对同一个消息有不同的表现形式。比如游泳,不同的人游泳的方式不一样,有蛙泳仰泳自由泳之类的

在java中多态分为运行时多态和编译时多态,方法重载为编译时多态(多个同名方法),方法重写为运行时多态(父类引用指向子类对象)

面向对象和面向过程的区别

  • 面向过程是针对过程步骤编程,依次调用

  • 面向对象是针对功能编程,对功能进行封装然后组合调用

面向过程的优缺点

  • 优点:有效地将一个较复杂的程序分解为多个易于控制的子程序,性能比面向对象高,因为类调用需要实例化。单片机、嵌入式等就调用面向过程,因为他们更注重性能
  • 缺点:没有面向对象易维护、易复用、易扩展

面向对象的优缺点

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
  • 缺点:性能比面向过程低

面向对象的七大原则

  • 单一职责原则

一个类只负责一件事,当这个类负责的事情多的时候,需要分解这个类,类的责职要单一

  • 里氏代换原则

子类可以代替父类,解决继承带来的耦合性问题,父类修改代码子类也会受影响问题

  • 依赖倒置原则

针对抽象类编程,不针对具体类编程

  • 开闭原则

对扩展开发,对修改关闭

  • 接口隔离原则

使用多个专门的接口来取代统一接口

  • 合成复用原则

在复合功能时,尽量多使用组合和聚合,少使用继承

  • 迪米特法则

一个软件实体应当尽可能少与其他实体发生交互

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

基本数据类型有哪些

byte 1个字节

char 2个字节

short 2个字节

int 4个字节

float 4个字节

long 8个字节

double 8个字节

boolean可以用1个bit来存储true和false,但具体大小没确定,因为jvm会在编译时期将boolean转化为int,1表示为true,0表示为false

new Integer(123)和Integer.valueof(123)的区别

区别在于前者是新建一个对象,开辟一个新的内存空间。

后者是用缓冲池中的对象

jdk和jre有什么区别

  • jdk,Java Development Kit,java开发工具包,包含了所有有关java的东西,JRE和JVM也包含在里面
  • jre,Java Runtime Environment,java运行环境,为java的运行提供了环境

jdk包含了jre,同时包含了编译java源码的编译器javac和其他工具。如果只需要运行java程序,即编译后的.class文件,安装jre就可以。如果要编译运行源码,也就.java文件,则需要jdk

在这里插入图片描述

==和equals的区别是什么

对于==

  • 如果是基本类型,则比较的是值。布尔类型不能用==比较
  • 如果是引用类型,则比较的是变量在栈中指向堆的内存地址,也就是比较两个对象是不是指向同一个内存空间

对于equals

  • 如果类中重写了Object类的equals方法,则比较的是内容的值,像String、包装类等重写了equals方法
  • 如果没有重写,就跟==一样,直接比较的是内存地址
//Object的equals
public boolean equals(Object obj) {
		return (this == obj);
}


//String重写后的equals
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

两个对象的hashcode相同代表equal为true吗

不一定,散列表的hashcode相同不代表键值对也相同

final的作用

  • final修饰的类为最终类,不能被继承
  • final修饰的方法不能被重写
  • final修饰的变量,如果为基本类型则为常量,常量必须初始化,初始化之后就不能被修改。如果为引用类型,final使得引用不变,但是对象本身的值还是可以修改

static的作用

static关键字主要有四个用法

  • static修饰的变量和方法

被static修饰的变量和方法他是属于类的,不与对象绑定,被该类中的所有对象共享,通过类名调用。静态变量存放于方法区中

  • 静态代码块

该类不管创建多少对象,静态代码块只执行一次,执行顺序为:静态代码块->非静态代码块->构造方法

  • 静态内部类

static修饰类的话只能修饰静态内部类,它的创建不需要依赖外围类的创建,依旧是只能使用外围类的静态方法

在继承情况下初始化顺序

父类静态代码块->子类静态代码块->父类非静态代码块->父类构造函数->子类非静态代码块->子类构造函数

String

String属于基本类型吗

不属于,String是引用类型

String可以用在switch中吗

jdk1.7后可以

String内部是什么

java8中String内部使用char数组去存储

java9之后是用byte[]数组,byte可以节省字符串占用的内存

String为什么是final

  • StringPool需要,不可变性可以使其构成StringPool
  • 安全性,String可以保证参数不可变,网络连接中连接参数不变可以保证安全性
  • 线程安全,并发使用String时其不可变性保证线程安全

String、StringBuffer、StringBuilder的区别

  • 可变性

String不可变,StringBuffer和StringBuilder可变,每次都会创建新的对象

  • 不可变性

String是final修饰,不可变,线程安全

StringBuilder不是线程安全

StringBuffer是线程安全的,内部使用synchronized进行同步

String str = new String(“abc”); 创建了几个对象,为什么?

这种方式一共创建两个对象(前提是String Pool中没有abc)

  • 在StringPool中创建一个字符串对象
  • new时在堆中创建一个

如何将字面量放进StringPool

  • new String(" ")
  • String s1=new String(“abc”)
  • s1.intern()

如何将字符串反转

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。

什么是浅拷贝、深拷贝

  • 浅拷贝

创建一个新对象对源对象进行拷贝,如果是基本类型,则直接拷贝它的值

如果是引用类型,则拷贝内存地址。

如果一个对象修改了引用类型,就会影响其他的拷贝对象,修改基本类型就不会

所以浅拷贝会带来数据安全的问题

实现:直接实现cloneabel接口,重写clone方法实现浅拷贝

  • 深拷贝

对于基本类型也是直接拷贝

对引用类型,会新建一个对象空间,然后拷贝其内容

所以一个拷贝对象修改了引用类型也不会对其他造成影响,能保证安全性。但是内存开销大

  • 实现:1、对每个对象都实现cloneable接口 2、通过序列化实现深拷贝和浅拷贝

什么是序列化和反序列化

  • 序列化就是把java对象转换为字节序列的过程,方便存储和运输,反序列化就是把字节序列转化为对象的过程

  • 不会对静态变量进行序列化,因为序列化的目标是对象,而静态变量是属于类变量

  • 序列化需要实现Serializable接口

  • 使用transient可以使对象不被序列化,ArrayList中的数组就是用该关键字修饰,因为ArrayList是动态拓展,不是所有的空间都需要序列化

redistemplate中需要序列化对象为json格式,然后再反序列化为对象出来操作

抽象类与接口的区别

抽象类:抽象类会用abstract声明,和普通类最大的区别是抽象类不能被实例化只能被继承,意思就是不能new

接口:接口是抽象类的延伸,java8之前是只能有抽象方法,java8开始接口可以有default类型的默认方法实现,java9开始方法不再需要都是public,可以为private。

比较:

  • 从设计层面来看,抽象类是is-a关系,认为子类需要覆盖基类的方法,接口是Like-a关系,实现类不需要完全覆盖接口的方法
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类
  • 接口的字段只能是static final类型的,但是抽象类的字段没有这种限制
  • java8以前,接口不能有其他非抽象方法,但是抽象类可以有
  • java9以前,接口的方法和字段只能是public,但是抽象类没有限制

什么时候使用接口或抽象类

接口更加灵活,支持多重继承

抽象类,需要继承非静态和非常量的方法和字段

重写和重载的区别

重写是指子类实现一个与父类同名的方法

  • 子类方法的访问权限必须大于等于父类的访问权限
  • 子类范围类型必须为父类类型或者其子类型
  • 子类抛出异常必须为父类类型或者其子类型

重载是指存在于同一个类中,方法名称相同,但是参数个数、类型、顺序至少一个不同

返回值不同,其他相同不算重载

有哪些异常

异常分为两种,error和exception

  • error表示jvm无法解决的错误
  • exception分为两种,受检异常和非受检异常

​ 受检异常:用try…catch…恢复

​ 非受检异常:程序运行时错误,无法恢复

java8新特性

  • Lambda表达式

匿名函数,允许函数作为参数传递到方法中,使代码更加简洁灵活

()->{ } 左侧是参数 右侧是表达式中需要执行的功能

顶替匿名内部类

可以Stream流式编程

  • 接口可以使用非抽象方法,也就是default方法
  • jdk1.8时,HashMap底层是数组+链表+红黑树
  • HotSpot虚拟机在JDK1.7中将字符串常量池移到了堆内存,并在JDK1.8中用元空间去掉了“永久代”。

java与C++的区别

  • java是oop,C++面向对象也面向过程
  • java没有指针,C++有
  • java支持垃圾自动回收,C++需要手动
  • java不支持多继承,C++支持

泛型

泛型是什么

泛型是jdk5引入的新特性,提供了编译时类型检查确认机制,保证编译期的类型安全,确保只能用传入的类型。这种参数可以用在类、接口和方法中,分别称为泛型类、泛型接口和泛型方法

泛型的优点

  • 保证类型的=安全性,会在编译器做类型检查,假如在集合中插入了错误的数据类型,会造成编译不通过
  • 可以消除强制转换
  • 可以避免不必要的装箱拆箱,提升性能,泛型固定了类型,使用时就知道是基本类型还是引用类型
  • 提高代码的重用性

泛型的原理(如何工作)

泛型的原理是类型擦除,在编译期间擦除了所有与类型相关的信息。

(用Object代替,当泛型使用到了extends和super的时候,用String代替)

重新说下泛型擦除,在List中定义了String之后,

  • java编译器首先是先检查泛型中的类型,然后再进行类型擦除,然后再编译
  • 在对象进入和离开jvm的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
  • 擦除用的是Object擦除,擦除完再强转为泛型传入的类型
  • java泛型是在编译器层次进行实现的,所以称为“伪泛型”。
  • 也就是说泛型的作用是起到类型检查机制,并无规定存放的只能是该类型。
  • 可以通过重新声明一个其他类型的List对象进行add,或者通过反射进行,因为在擦除的时候都是用Object进行擦除。

泛型的通配符

无边界通配符

  • <?>就是无边界通配符
  • 可以接受任何未知的数据类型

有边界通配符

  • 固定上界通配符,采用<? extends E>的形式,能够接受指定类型及其子类类型的数据
  • 固定下界通配符,采用<? super E>的形式,能够接受指定类型及其父类类型的数据

如何编写泛型

K:key

V:value

T:type

E:element

  • 泛型类
public class GenericClass<T> {
    
    private T value;
 
    public GenericClass(T value) {
        this.value = value;
    }
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
  • 泛型接口
public interface GenericInterface<T> {
	void show(T value);
}

public class StringShowImpl implements GenericInterface<String> {
	@Override
	public void show(String value) {
		System.out.println(value);
    }
}
 
public class NumberShowImpl implements GenericInterface<Integer> {
	@Override
	public void show(Integer value) {
		System.out.println(value);
    }
}
  • 泛型方法
/**
     *
     * @param t 传入泛型的参数
     * @param <T> 泛型的类型
     * @return T 返回值为T类型
     * 说明:
     *   1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     *   2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *   3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     *   4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
     */
    public <T> T genercMethod(T t){
        System.out.println(t.getClass());
        System.out.println(t);
        return t;
    }
 
 
public static void main(String[] args) {
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("helloGeneric"); //这里的泛型跟下面调用的泛型方法可以不一样。
    String str = genericString.genercMethod("hello");//传入的是String类型,返回的也是String类型
    Integer i = genericString.genercMethod(123);//传入的是Integer类型,返回的也是Integer类型
}
 
 
class java.lang.String
hello
 
 
class java.lang.Integer
123

Java容器

java的容器有哪些

分为collection和map两种

collection里有Set、List、Queue接口

  • Set实现类有TreeSet、HashSet、LinkedHashSet
  • List实现类有ArrayList、Vector、LinkedList
  • Queue实现类有LinkedList、PriorityQueuehttps://blog.csdn.net/qq_43170213/article/details/89384749

TreeSet基于红黑树实现,支持有序性操作,单一数据查找效率不高,O(logn),不如HashSetO(1)

HashSet基于hash表实现,可以快速查找,但是不支持有序操作

LinkedHashSet:具有hashset查找效率,并且内部使用双向链表维护元素插入顺序

什么是红黑树

  • 不严格的二叉查找树,增加和删除结点的效率比AVL高

  • 根节点为黑色,相邻父子结点不能都为红色

  • 任意一个结点到NULL结点的每条路径都具有相同数量的黑色结点

ArrayList基于动态数组实现,支持随机访问

Vector和ArrayList相似,但是是线程安全的

LinkedList基于双向链表实现,只能顺序访问,但是插入删除效率高只需要动前后结点,而且LinkedList还可以用作栈、队列和双向队列

LinkedList可以用来实现双向队列

PriorityQueue基于堆结构实现,可以用来实现优先队列

Map中包含了TreeMap、HashTable、LinkedHashMap和HashMap

TreeMap基于红黑树实现

HashMap基于哈希表实现

Hashtable和HashMap类似,但是它是线程安全的,但是相比之下ConcurrentHashMap的效率更高,因为采用了分段锁

LinkedHashMap基于双向链表维护元素的顺序,顺序为插入顺序或最近最少使用(LRU),跟HashMap差不多只不过有序

Collection和Collections有什么区别

Collection是一个接口,提供集合类接口像List、set和Queue

Collections是集合类的工具类,提供了一系列静态方法,可以对集合类进行排序等操作

ArrayList

什么是ArrayList

  • ArrayList是基于数组实现,所以支持随机访问
  • RandomAccess接口标识着支持快速随机访问
  • 数组的默认大小为10

ArrayList怎么实现扩容

  • ArrayList在添加元素的时候如果容量不够,需要采用grow()方法进行扩容,新容量大小为oldCapacity+(odlCapacity>>1),大概就是原容量的1.5倍
  • 扩容操作需要调用Arrays.copyOf()将原数组复制到新数组当中,代价很高,所以最好创建时指定大小

ArrayList怎么删除元素

需要调用System.arraycopy()把删除元素后面的元素复制上来,时间复杂度为O(N),删除代价很高

ArrayList怎么实现序列化

  • ArrayList保存元素的数组用transient修饰,默认数组不会被序列化
  • 实现writeObject()和readObject()来控制只序列化有元素的那部分
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

如何实现数组和List之间的转换

  • List转换为数组:调用ArrayList的toArray方法
  • 数组转为List:调用Arrays的asList方法

为什么Vector是线程安全的

因为vector使用了synchronized进行同步

vector是怎么扩容的

  • 默认为10
  • 每次扩容为原先的两倍

Vector与ArrayList的比较

  • Vector是线程安全的,用Synchronized修饰,但是加了锁开销大,要比ArrayList慢
  • Vector每次扩容是2倍,ArrayList大概是1.5倍

ArrayList怎么保证线程安全

  • 可以使用Collections.synchronizedList()得到一个线程安全的ArrayList
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
  • 也可以使用concurrent并发包下的CopyOnWriteArrayList类
List<String> list = new CopyOnWriteArrayList<>();

什么是CopyOnWriteArrayList

  • CopyOnWriteArrayList实现的是数组的读写分离,写操作在复制的数组上,读操作在原数组中,互不影响
  • 写操作加了ReentrantLock,防止并发写入时导致数据丢失
  • 写操作之后会把原始数组指向复制数组
CopyOnWriteArrayList的优点

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

CopyOnWriteArrayList的缺点

  • 占用内存,因为复制数组需要比原先多出一倍的空间
  • 会造成数据不一致:因为读写速度不一致导致读操作读不出实时的数据
  • 所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

什么是LinkedList

  • 基于双向链表实现

LinkedList和ArrayList的区别

  • LinkedList是双向链表,ArrayList是动态数组
  • ArrayList支持随机访问,但是插入删除代价高,需要移动大量元素
  • LinkedList插入删除只需要修改指针,但是不支持随机查询

HashMap

HashMap原理

jdk1.7时,HashMap底层是数组+链表

jdk1.8时,HashMap底层是数组+链表+红黑树

  • 内部包含一个Entry类型的数组,Entry存储着键值对,数组的每个位置会被当成一个桶,每个桶存放着一个链表。
  • 使用拉链法来解决冲突,同一个链表存放着哈希值以及哈希值取模结果(hash(key)%数组大小)相同的Entry

HashMap的put过程

  • 首先有键值对put时会先判断entry数组是否为空,如果为空则进行第一次扩容,也就是初始化为16
  • 如果key为null,因为计算不了哈希值所以hashmap将其放在第0个桶里
  • 如果key不会null,第一步先hash(key)%数组大小得到索引值(或者是哈希值&数组长度-1)
  • 如果该位置为null,则直接插入
  • 如果不为空,首先判断hash出来的值和key是否一样,(需要遍历)如果一样则直接覆盖value
  • 如果key不一样,就得判断是不是红黑树的结点,如果是,就直接插入键值对
  • 如果不是红黑树,那就是链表,用头插法插入链表
  • 插入链表后如果发现大于8,并且数组长度大于64,则会转换为红黑树
  • 如果大于8,但是数组长度小于64,则进行扩容

HashMap的扩容机制

  • 初始容量为16,每次都是以2的次方扩充的,因为2的倍数在计算索引位置时用位运算代替取余的效率更高
  • 所以发生扩容的时候有两种情况,一种是元素达到阀值(table数组的长度乘上负载因子0.75)
  • 一种是HashMap准备树形化但又发现数组太短,这两种情况均可能发生扩容。

HashMap什么时候用链表,什么时候用红黑树

  • 对于插入

默认情况下是使用链表结点,当新增后的链表结点的个数超过了8个(阈值)时。此时如果数组长度大于64,则会触发链表转为红黑树;如果数组长度小于64,则触发扩容,不转红黑树,因为数据量还太少

  • 对于移除

红黑树结点移除后是6个时,将触发红黑树转链表

HashMap的get操作

①计算key的hash值i&(length-1),找到key在数组中的位置

②如果该位置为null,就直接返回null

③否则,根据equals()判断key与当前位置的值是否相等,如果相等就直接返回。

④如果不等,再判断当前元素是否为树节点,如果是树节点就按红黑树进行查找。

⑤否则,按照链表的方式进行查找。

为什么jdk1.8要把链表改为红黑树

主要是为了提升在哈希冲突严重的时候的查找性能,

使用红黑树的查找性能是O(logn),而使用链表的查找性能是O(n)

为什么红黑树转链表阈值为8

  • 选择红黑树为8是时间和空间上的权衡,因为红黑树的每个结点大小大约是链表结点的两倍,而红黑的搜索时间复杂度为logn
  • 结点发生哈希碰撞的频率是跟泊松分布有关,根据泊松分布计算,链表中结点个数为8的概率小于百万分之一,几乎不存在的概率。所以如果出现了,那就说明可能是用户自己实现的hash函数有误,造成了大概率的哈希碰撞,后续会发生哈希碰撞的概率增加,所以这个时候就应该转为红黑去减少检索性能了。

HashMap 的容量必须是 2 的 N 次方,这是为什么?

可以把索引取余的计算转化为与运算,效率更高。hash(key)%len --> hash(key)&(n-1)

HashMap怎么解决哈希冲突

哈希冲突是指哈希key后取模映射到同个位置

拉链法(hashmap所用)

  • HashMap在put元素的时候,会先hash(key)再取模得到相应的索引位置,进行元素插入。
  • 当发现有冲突时,就会插入到链表头部,拉链法使用的是头插法。

解决哈希冲突有几种方式

  • 开放地址法,一个数值固定一个索引位置,非链式。冲突了就延后添加
  • 拉链法,用链表解决哈希冲突
  • 建立公共溢出区,建立公共溢出区存储所有哈希冲突的数据
  • 再哈希法,对于冲突的哈希再继续哈希下去直到不冲突

HashMap为什么线程不安全

  • jdk1.7因为头插法,在扩容重新放回数组的过程中多线程状态下会出现环状链表死循环和数据丢失
  • jdk1.8尾插法解决了死循环问题,但是多线程情况下会出现数据覆盖问题,导致数据丢失

HashMap为什么使用红黑树而不是平衡二叉树AVL或二叉查找树?

  • 二叉查找在极端情况下会出现线性结构,查找效率回到O(n)
  • AVL虽然查询比红黑好一点,但是增加和删除结点要维持平衡的开销要大于红黑

HashMap和Hashtable的异同

  • HashMap是线程不安全,Hashtable有用Synchronized修饰是线程安全
  • HashMap的键值对允许是null,但Hashtable不允许
  • 一个16 ,一个11
  • 一个数组+链表+红黑,一个数组+链表

什么是红黑树

  • 不严格的二叉查找树,增加和删除结点的效率比AVL高

  • 根节点为黑色,相邻父子结点不能都为红色

  • 从任意一个结点(包括根结点)到其任何后代 NULL 结点(默认是黑色的)的每条路径都具有相同数量的黑色结点。

ConcurrentHashMap

ConcurrentHashMap原理

jdk1.7时 Segments数组+Entry数组+链表,采用分段锁来保证线程安全

  • 一个ConcurrentHashMap里有一个Segments数组,一个Segments存储着一个Entry数组,每个Entry是一个链表结构
  • Segments继承自ReentrantLock
  • 在多线程情况下,当一个线程访问其中的一段数据时,其他段的数据也能同时被访问,实现了并发访问

在这里插入图片描述

jdk1.8时 Node数组+链表+红黑树 ,采用Synchronized和CAS来保证线程安全

在这里插入图片描述

简述下ConcurrentHashMap的put操作

jdk1.7

  • 根据hash(key)找到相对应的Segments段
  • 然后在Segments段中进行put操作,需要加lock(可以防止数据丢失和环状链表死循环)
  • 再次hash确定位置
  • 如果key和hash值相同覆盖,如果不同链表插入

jdk1.8

  • 根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
  • 如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
  • 如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
  • 如果是红黑树,就按照红黑树的结构进行插入。

简述下ConcurrentHashMap的get操作

jdk1.7

  • HashEntry中的value和next是用volatile修饰的,保证了可见性,每次获得的都是最新值,所以get过程不需要加锁

  • 根据哈希值找到相应的Segments段

  • 在Segments中再次hash等到位置

  • 最后在链表中根据判断key是否相同去查找

jdk1.8

get操作依旧是全程无锁,因为Node数组里的元素都是用volatile修饰,保证了可见性

  • 计算hash值,定位到node数组的位置
  • 如果该位置为空则直接返回null
  • 如果不为空,则根据key去遍历链表或者红黑树找到对应的值

为什么jdk1.8要用Synchronized替换可重入锁

  • 因为1.8Synchronized只锁住了发生哈希冲突的链表头结点或者红黑树根节点,减小锁的范围,提高性能
  • jdk1.6开始Synchronized就产生了优化,会从偏向锁->轻量级锁->重量级锁一步步转换
  • 减小内存开销,锁住只想锁的

LinkedHashMap

LRU

Java并发

线程

并行和并发有什么区别

  • 并行是指两个或多个事件在同一时刻发生,并发是指两个或多个事件在同一时间有间隔地发生
  • 并行是指不同实体的多个事件,并发是指同个实体的多个事件
  • 并发是指一个处理器同时处理多个任务,而并行是指多个处理器或者多核处理器同时处理一个任务

进程和线程的区别

  • 进程是操作系统资源分配的基本单位,一个在内存中运行的应用程序就算是一个进程,每个进程都有自己独立的一块内存空间,像QQ就是一个进程
  • 线程是CPU任务调度和执行的基本单位,进程中的一个执行任务,是进程的实体,类似于使用qq发送信息就是一个线程
  • 一个程序至少有一个进程,一个进程至少有一个线程
  • 一个进程中可以有多个线程,多个线程共享进程的和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器**、**虚拟机栈 和本地方法栈。
  • 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

什么是线程死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于相互竞争而阻塞造成最后都在相互等待的现象。无外力作用,他们都将无法推进下去。

比如说线程A拥有资源1,线程B拥有资源2,他们同时都想申请对方的资源,但是又不肯送出自己的资源,所以两个线程就会互相等待进入死锁状态

形成死锁的四个必要条件是什么

  • 互斥条件:一个资源只能被一个线程占用,直到该资源被线程释放
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程占用的资源在未使用完之前不能强行剥夺
  • 循环等待条件:当发生死锁时,所等待的线程形成一个环路,造成永久阻塞

如何避免死锁

我们只要破坏产生死锁的四个条件之一就好了

  • 破坏互斥条件

这个没法破坏因为锁本来就是互斥的

  • 破坏请求与保持条件

一次性申请所有资源

  • 破坏不剥夺条件

占用资源的线程在进一步申请其他资源时,如果申请不到,可以主动释放它所占有的资源

  • 破坏循环等待条件

靠按顺序申请资源来预防死锁。申请按顺序申请,释放资源反序释放,破坏循环等待条件。

Java中用到的线程调度算法是什么

  • 分时调度:分时调度是指让所有线程轮流获得CPU的使用权
  • 抢占式调度:java虚拟机采用的是抢占式调度,让优先级高的线程去抢占CPU。如果优先级相同就随机选一个

守护线程跟用户线程有什么区别

  • 用户线程:运行在前台,执行具体的任务,如main函数
  • 守护线程:运行在后台,为前台线程服务,如垃圾回收线程。一旦用户线程结束运行,守护线程会随着JVM一起结束工作

main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。

而守护线程不会影响 JVM 的退出。

创建线程的几种方式

  • 实现Runnable接口,重写run方法然后使用Runnable实例创建一个Thread实例,调用Thread实例的start()方法来启动线程
  • 实现Callable接口,与Runnable相比,Callable可以有返回值。重写call方法后,返回值通过FutureTask进行封装。然后依旧是使用FutureTask的实例去创建一个Thread实例,调用start()方法启动线程
  • 继承Thread类,重写run方法然后创建Thread实例调用start()启动线程
  • 使用 Executors 工具类创建线程池

实现接口会好一点,因为Java不支持多继承,而且继承整个Thread开销大

Runnable和Callable有什么区别

  • Runnable接口的run方法返回值是void
  • 而Callable接口的call方法是可以有返回值的,并且由Future、FutureTask封装

线程有哪些状态(生命周期)

  • 创建:刚new出Thread对象,但没有调用start方法时,线程处于创建状态
  • 就绪:调用了start()方法后就处于就绪状态,但是还抢占不到cpu,还没被设置为当前线程。从等待或者睡眠中醒来的线程,也处于就绪状态。
  • 运行:抢占到cpu的线程,也就是当前线程,开始运行run函数中的代码
  • 阻塞:线程正在运行的时候被暂停,sleep和wait都可以导致线程阻塞
  • 死亡:run方法执行结束或执行stop就会死亡,死亡情况下的线程无法再start

请说出与线程同步以及线程调度相关的方法

(1)wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁,进入该对象的等待池,需要用notify/notifyall来唤醒

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,不会释放锁,休眠一段时间后自己会恢复。

(3)notify():随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

(4)notityAll():会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

(5)yield():让线程由运行状态转为就绪状态

(6) join():在线程A中调用线程B的join()方法,会将当前线程挂起,A线程会等B执行完再执行

在这里插入图片描述

sleep和wait有什么区别

  • sleep是Thread的静态方法,wait是Object的方法
  • sleep可以在任何地方使用,但是wait只能在同步方法或者同步代码块中使用
  • sleep不会释放锁,wait会释放锁
  • sleep()会让线程暂停执行,进入睡眠状态,让出执行机会给其他线程。但如果调用sleep的是一个占有锁的线程,如当一个Synchronized块调用了sleep,线程虽然进入睡眠,但是锁未释放,所以其他对象依旧访问不了该对象
  • 当调用wait()时,线程会放弃掉锁,然后进入一个与该对象相关的等待池,其他线程可以访问该对象,等到notify或者notifyall才唤醒该线程

Java如何实现多线程之间的通讯和协作?

Java中线程通信协作的最常见的两种方式:

一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

如何停止一个正在运行的线程?

在java中有以下3种方法可以终止正在运行的线程:

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
使用interrupt方法中断线程。

run和start有什么区别

  • 每次线程都是通过某个特定Thread对象所对应的方法run()来完成操作,方法run()称为线程体,通过调用Thread的start()方法来启动线程
  • run只是线程体,是一个线程的方法,并不是多线程,启动start才是实现多线程

线程池

创建线程池有几种方式(重点)

通过工厂类Executors可以创建线程池

  • newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,空闲时不会释放线程,占用一定的空间

  • newCachedThreadPool()

不限制线程池里的线程数量

空闲线程的存活时间为60s

  • newSingleThreadExecutor()

单线程的线程池,相当于单线程串行执行任务。如果这个线程因为异常结束,那么就会有一个新的线程替代他,保证任务顺利执行

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}

线程池有几种状态

  • Running执行状态
  • Shutdown关闭状态,不能接收新任务,但是可以处理已添加的任务
  • Stop停止状态,会中断正在执行的任务
  • Tidying所有任务终止
  • Terminated线程池彻底终止

使用线程池的优点

  • 减少创建和销毁线程上时间和资源的开销
  • 减少线程切换时消耗资源
  • 方便管理,实现线程复用,提高并发数

线程池的原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VW0VGsYo-1665909885561)(java.assets/image-20220916233423873-16633424658581.png)]

线程池的7种参数(重点)

  • corePoolSize:核心线程数,刚开始数量为0随着任务的增加,会达到定义的数量,之后线程池就会一直保持这个数量,不会回收。但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize:最大线程数,当阻塞队列放不下任务时,会继续创建线程来执行任务,但不能超过这个最大线程数
  • keepAliveTime:空闲线程存活时间,一个线程若处于空闲状态,且为非核心线程,则会在指定时间内被销毁
  • unit:存活时间的单位,小时分钟秒毫秒等
  • workQueue:工作队列,若线程数达到核心线程数后,新任务会被提交后会先到工作队列当中,任务调度时再拿出来,jdk提供了四种工作队列

ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序,创建时需要指定大小

LinkedBlockingQueue:基于链表的无界阻塞队列(其实最大容量是Integer_MAXVALUE),按FIFO排序。因为它近似无界,所以线程数达到核心线程数后,再有新任务进来会一直存放在队列中,而不会去创建新线程。但同时也存在OOM的隐患

SynchronousQueue:同步队列,一个任务必须等待其被取出后才能往里面放另外的任务。

PriorityBlockingQueue:具有优先级别的阻塞队列,优先级通过Comparator实现

  • ThreadFactory:线程工厂
  • handler:拒绝策略,当线程总数大于最大线程数时会执行拒绝策略。共有四种

AbortPolicy(默认):超出最大承载,丢弃任务,并抛出异常。(最大承载为:队列容量大小+最大线程数)

CallerRunsPolicy:只要线程池没有关闭的话,用调用者所在的线程来处理任务。(main函数来处理)

DiscardPolicy:满了直接丢弃,不会产生异常

DiscardOldestPolicy:当满了会丢弃阻塞队列中最老的任务,并将新任务加入。
在这里插入图片描述
在这里插入图片描述

线程池中的submit和execute有什么区别

  • 参数不一样
  • submit有返回值,execute没有
  • submit方便处理异常

JMM

谈一谈JMM(重点)

在这里插入图片描述

JMM是一种规范,目的是解决多线程通过共享内存通信时,存在的本地内存数据不一致、编译器指令重排序等问题

  • JMM规定所有的变量都在主内存中,主内存是共享内存区域
  • 每条线程都有自己的工作内存,工作内存中保存了该线程要使用到的变量在主内存中的拷贝
  • 线程对变量的读写都在工作内存中实现,不能直接读写主内存
  • 首先将变量从主内存中读取到工作内存,然后对变量进行操作,然后再写回主内存
  • 不同线程直接不能访问各自的工作内存,线程之间变量值的传递靠的是主内存实现

Java程序中怎么保证多线程安全(JMM特性)(重点)

线程的安全性在这三方面体现

  • 原子性:一个操作不能被中断,要么全部成功要么全部失败

通过atomic类实现,或者synchronized互斥实现

  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,保证主内存和工作内存的一致性

volatile

synchronized、lock:

1、加锁时要清空工作内存中的值,保证会去主内存中读取新值。

2、对一个变量实行unlock之前,必须把变量值同步到主内存

final:

其它线程就能看见 final 字段的值

  • 有序性:程序的执行顺序按照代码的先后顺序执行,防止指令重排序

volatile

synchronized,保证每个时刻只有一个线程执行同步代码,相当于让线程顺序执行代码

happens-before原则,让一个操作无需控制就能先于另一个操作完成

会改变结果的重排序JMM会禁止,不会改变的结果的不禁止

什么是重排序

编译器在不改变语义的情况下,会改变语句的执行顺序,提高性能

虽说两个操作存在happen—before关系,但JMM允许不改变执行结果的重排序

什么是happens-before原则

  • 单一线程原则:在一个线程内,程序前面的操作先行与后面的操作
  • 管程锁定原则:一个unlock先发生于后面同一个锁的lock
  • volatile变量规则:对一个volatile变量的写操作先行于后面对这个对象的读操作
  • start()规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
  • join()规则:Thread 对象的结束先行发生于 join() 方法返回。
  • 程序中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生

引用类型

  • 强引用

通过new一个新对象的方式来创建强引用,被强引用关联的对象不会被回收

  • 软引用

使用SoftReference类来创建软引用,被软引用关联的对象只有在内存不够的时候才会被回收

  • 弱引用

使用WeakReference类来创建弱引用,被弱引用关联的对象一定会被回收。

  • 虚拟引用

使用PhantomReference来创建虚拟引用,为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

ThreadLocal

ThreadLocal是什么

ThreadLocal主要功能就是给每个线程创建变量副本,这样就可以保证一个线程对某个变量的修改不会影响到其他线程对该变量的使用,实现线程间的数据隔离。

ThreadLocal的原理

  • 内部是一个ThreadLocalMap(说白了就是一个Map对象)。它以ThreadLocal本身作为键值,副本对象作为value存储。而ThreadLocalMap通过当前线程Thread中的一个threadlocals变量获取的
public class Thread implements Runnable {
      ……

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
     ……

  • 每个线程Thread里都有维护一个threadlocals变量,所以在每个线程在创建ThreadLocal的时候,实际上是存储在自己线程的threadlocals变量里面,别人没办法拿到,所以实现了数据隔离。
  • ThreadLocalMap的底层是一个类似于哈希表的结构,是一个Entry数组,无链表,将key设置为弱引用。哈希冲突是使用线性探测法去解决。
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        //在构造方法中将key设置为弱引用
        super(k);
        value = v;
    }
}

ThreadLocalMap的key为什么设置为弱引用

  • ThreadLocalMap是和线程绑定在一起的,如果线程没有被销毁并且某个ThreadLocal不再使用到,那么这个k-v的键值对就会一直在map中存在,那就会造成内存泄露。
  • 为了避免这种情况,就把key设置为弱引用,使得每次发生gc回收的时候key都会被回收,然后key就会变为null,解绑当前线程和ThreadLocal对象的生命周期
  • ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的value,起到了惰性删除释放内存的作用。

在这里插入图片描述

ThreadLocalMap怎么解决哈希冲突

线性探测法,如果不会空那就找下一个空的位置

ThreadLocal为什么会出现内存泄露

  • 由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说(线程池),如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.
  • 解决途径是使用完ThreadLocal之后手动remove掉

ThreadLocal的使用场景

  • 共享变量:像自己的项目中是用ThreadLocal去存储用户信息,在拦截器中通过cookie获取登录凭证,验证用户安全后就set进ThreadLocal中。相当于该线程的全局变量,让不同方法直接使用,避免参数传递麻烦

    • 在拦截器配置渲染完页面后会主动remove掉
  • 独享变量:用ThreadLocal包装SimpleDataFormat,使得每个用户都有自己独有的时间

什么是阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。分别是:

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

什么是乐观锁、悲观锁

悲观锁(Synchronized)

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁(CAS)

  • 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

2、CAS

什么是公平锁、非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

Synchronized

说一下Synchronized

synchronized是Java中的同步关键字,一般叫它同步锁,可以使用在普通方法上、静态方法上以及代码块上,主要是锁定代码块,使得同一时刻只能有一个线程去访问synchronized修饰的方法或代码块,以此保证线程安全。

  • 用在普通方法上会以this作为锁,即当前对象
  • 用在静态方法上,锁的是类
  • 代码块可以锁任何对象(看括号里写了什么)
  • synchronized不能直接加在构造方法上,但是可以在构造方法里使用synchronized的代码块。因为Synchronized是以对象为锁的,在构造方法使用时对象还未产生,构造函数的作用是初始化对象。

什么是对象头

java对象可以分为三部分,对象头、实例数据和对齐填充

synchronized用的锁在存在于对象头里的

当使用synchronized作用于一个object时,这个object的Mark Word就会存储指向Monitor。

在这里插入图片描述

Synchronized原理

  • 早期的Synchronized是基于monitor实现,获取锁的过程是monitorenter,释放锁是monitorexit。而monitor监视器锁底层是依赖于操作系统的Mutex Lock实现,是属于重量级锁。每次线程切换都要从用户态转为内核态,性能和时间成本高

  • 而Synchronized的锁是在对象头里,当Synchronized作用于一个对象时,对象头的Mark word就会指向Monitor

  • Jdk1.6之后对锁进行了优化,如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,提升了效率。

什么是Monitor

  • Monitor类似于一个监听器,每一个对象都会关联一个Monitor

  • Monitor由WaitSet、EntryList、Owner三个结构组成。

    • Owner表示当前拥有这把锁的线程。
    • EntryList是阻塞队列,当Owner不为空时,其他线程阻塞等待的地方。
    • WaitSet是当Owner调用wait方法时就会进入到这里(如果不是Owner就调用wait,就会报错,所以wait需要放在synchronized同步中),当其他线程调用notify/notifyall时,线程就会进入EntryList与其他线程竞争。

Synchronized的上锁过程

任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。(计数器)

  • 如果monitor的进入数为0,则该线程进入monitor,然后将设置为1,并且该线程为monitor的所有者owner
  • 如果线程已经获取到该monitor,重新进入,则monitor的进入数加1 (可重入锁)
  • 线程执行monitorexitmonitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit
  • 如果其他线程已经占用monitor,则该线程进入阻塞状态(EntryList),直到monitor的进入数为0,再重新尝试获取monitor的所有权

什么是可重入锁

  • 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

  • 加锁时判断锁是否已经被获取,如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。如果不是当前线程,则需要等待。

  • 释放锁时,需要给锁的获取次数减1,然后判断,次数是否为0了。如果次数为0了,则需要调用锁的唤醒方法,让锁上阻塞的其他线程得到执行的机会。

  • ReentrantLock和synchronized都是可重入锁。

介绍jdk1.6的锁优化

引入了自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减小锁操作的开销,提高效率

  • 自旋锁

让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。

  • 锁消除

当资源不存在竞争问题的时候,可以取消锁。

  • 锁粗化

通过扩大锁的范围,避免反复加锁和释放锁。例如for循环中的加锁操作可以放在for循环外面进行加锁操作。

  • 偏向锁

在大多数情况下,锁总是由同一线程多次获得,不一定存在多线程竞争,所以出现了偏向锁,其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候,进入偏向锁状态,同时使用 CAS 操作将线程 ID 记录到 对象头的Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入和退出这个锁相关的同步块就不需要CAS来加锁和解锁。

偏向锁使用了一种等到竞争出现才释放的锁机制

当有另外一个线程去尝试获取这个锁对象时,如果本线程依旧活着,那么本线程将继续执行

否则偏向锁状态就宣告结束,此时撤销偏向后恢复到无锁状态或者轻量级锁状态。

  • 轻量级锁

轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销

其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

使用轻量级锁时,不需要申请互斥量,仅仅将对象头中的Mark Word中的部分字节用CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;

轻量锁适合于俩个线程交替运行,但是没有产生实质上得竞争,如果发生了锁竞争,接下来轻量锁将膨胀为重量级锁。

Synchronized锁升级的过程

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

CAS

什么是CAS

  • cas也叫比较交换,是一种无锁原子性算法。让CPU将内存值更新为新值,但是内存值必须要与期望值相同。
  • CAS操作无需用户态与内核态的切换,直接在用户态进行读写操作,意味着不会阻塞/上下文切换
  • CAS包含三个参数(V,E,N),V表示待更新的内存值,E表示预期值,N表示新值,当 V值等于E值时,才会将V值更新成N值,如果V值和E值不等,不做更新,这就是一次CAS的操作。

CAS的优点

无锁原子性算法,无需用户态到内核态的切换,直接用户态对内存进行读写操作,不会产生阻塞、上下文切换、死锁等问题,拥有比锁更优越的性能

CAS的缺点

  • 只是一个变量的原子性操作,不能保证代码块的原子性(AtomicReference可以多个对象
  • 如果自旋时间过长会消耗CPU
  • ABA问题(追加版本号可以解决)

什么是ABA问题

进行CAS操作时,线程1读取到A,然后被线程2抢占,线程2将A改为B再改为A,接着线程1恢复运行,以为还是原来的那个A,于是进行CAS,但其实此时的A状态或属性已经发生了一些变化。

解决方案:追加版本号

CAS怎么保证原子性

  • 总线锁定:

    通过CPU的总线LOCK指令来锁请求,其他的请求将被阻塞。

    虽然保证了原子性,但是在锁的过程中会造成大量的阻塞。

  • 缓存锁定

​ 用的更多的一种方式,CPU对缓存行进行锁定,缩小锁定范围,提高性能

atomic

什么是原子操作类

  • 原子性操作是指不可被中断的操作。java中通过Synchronized的互斥或者循环CAS(自旋)来保证原子性
  • JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

说一下 atomic 的原理

  • CAS,当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功(类似于轻量级锁)

  • Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法保证原子性。

    我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

为什么i++和++i不安全

  1. i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
  2. ++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。

之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

volatile

什么是volatile

volatile能保证可见性和有序性,禁止指令重排,提供happens-before的保证

(计算机在执行程序的过程中,编译器和处理器通常会对指令进行重排序,这样做的目的是为了提高性能。)

volatile怎么实现可见性

  • 导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致

  • volatile可见性的实现就是借助了CPU的lock指令,使得

    (1)修改volatile变量时会强制将修改后的值刷新的主内存中。

    (2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

    通过这两个操作,就可以解决volatile变量的可见性问题。

volatile怎么实现有序性

那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
  4. 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

在这里插入图片描述

Java内存模型对编译器指定的volatile重排序规则为:

  • 当第一个操作是volatile读时,无论第二个操作是什么都不能进行重排序。
  • 当第二个操作是volatile写时,无论第一个操作是什么都不能进行重排序。
  • 当第一个操作是volatile写,第二个操作为volatile读时,不能进行重排序。

Synchronized和volatile的区别

  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

能创建 volatile 数组吗?

能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

Synchronized和Lock的区别

  • synchronized是java内置关键字,在jvm层面,Lock是个java类
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
  • synchronized会自动释放锁,Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

Synchronized和ReentrantLock的区别

  • synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit)ReentrantLock 是API层面的锁,实现则是通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性有序性以实现锁的功能。

  • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

  • synchronized是不可中断类型的锁; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者<将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

  • synchronized为非公平锁。 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

  • ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。一个 ReentrantLock 可以同时绑定多个 Condition 对象。

    java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

    相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

  • synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据 进入的线程和int类型的state 标识锁的获得/争抢。

  • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

什么是AQS

  • AQS是队列同步器,Reentrantlock和Semaphore都是基于AQS实现

  • AQS 有一个state 标记位,值为1 时表示有线程占用,其他线程需要进入到CLH虚拟双向队列等待

  • volatile修饰共享资源state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒

  • AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,将暂时获取不到锁的线程加入到CLH队列中。

  • CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性
  • AQS 对资源的共享方式

    AQS定义两种资源共享方式

    Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。
    在这里插入图片描述
    在这里插入图片描述

并发工具CountDownLatch/CyclicBarrier/Semaphore

  • CountDownLatch:类似于一个减法计数器,每次调用countDown()方法计数器的值减一,减到0之后执行后面的操作

  • CyclicBarrier:和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

  • Semaphore:Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

    semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

    semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!

Java虚拟机

JVM

JVM是什么

java虚拟机,由两个子系统和两个组件组成

两个子系统为:①Class loader(类加载系统) ②Execution engine(执行引擎)

两个组件为:①Runtime data area(运行时数据区域) ②Native Interface(本地接口)

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

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

  • Native Interface(本地接口):与本地方法库交互,是其它编程语言交互的接口。

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

java运行流程

  • 首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内
  • 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
  • 而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行
  • 因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行
  • 而这个过程中需要调用其他语言的本地库接口(Native Interface)来调用本地方法库来实现整个程序的功能。
    在这里插入图片描述

在这里插入图片描述

说一下JVM运行时数据区域

程序计数器(Program Counter Register):

当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令

Java 虚拟机栈(Java Virtual Machine Stacks)

  • 每个 Java 方法在执行的同时会创建一个栈帧
  • 用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

局部变量表存放了对象引用等信息。

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。

(i++解释)

本地方法栈(Native Method Stack)

与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

Java 堆(Java Heap)

Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms1M -Xmx2M HackTheJava

方法区(Methed Area):

  • 用于存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息)、常量、静态变量
  • 编译后的代码等数据。
  • 方法区与堆内存一样是所有线程共享的一块区域
  • HotSpot虚拟机在JDK1.7中将字符串常量池移到了堆内存,并在JDK1.8中用元空间去掉了方法区。

方法区内存不足时会抛出OutOfMemoryError异常

在这里插入图片描述

说一下堆栈的区别

  • 物理地址:

堆的物理地址分配对象是不连续的,性能慢些。GC回收也要考虑不连续,有标记清除、标记整理、复制、分代收集算法

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

  • 内存确认时期

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

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

  • 存放内容

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

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

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

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

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

创建对象的几种方式

  • 使用new,调用构造函数
  • 使用放射的newInstance,调用构造函数
  • 使用clone,没有调用构造函数
  • 使用反序列化,没有调用构造函数

创建对象的主要流程

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

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

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

选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

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

  • 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);

  • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。

Java会存在内存泄漏吗?请简单描述

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

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

内存溢出的原因(jhat检查)

https://blog.csdn.net/qq_37935909/article/details/109202787

1、栈溢出

StackOverFlowError

线程栈内存溢出

原因:线程数量太多导致溢出,如方法递归调用、大量循环/死循环、全局变量过多、数组/List/map数据过大

解决方案:

  1. 增大堆栈的值
  2. 用static对象代替局部对象
  3. 把递归转换为非递归

2、OOM

https://blog.csdn.net/weixin_45264992/article/details/120106681

https://blog.csdn.net/baobei0921/article/details/123712725

OutOfMemoryError:Java heap memory

堆内存溢出

原因:大量对象创建撑爆堆区

解决方案:

  1. 使用Xmx参数指定一个更大的堆空间
  2. 优化堆空间中的对象

OutOfMemoryError:GC overhead limit exceeded

频繁GC且GC无效果

原因:大量对象创建且无法回收,GC时间超过98%并且回收了不到2%的内存

解决方案:

  1. 增加堆的大小
  2. 优化对象和代码

OutOfMemoryError:Direct buffer memory

直接内存溢出

原因:因为直接内存不属于GC回收范围,所以假如内存不够他就会直接崩溃

解决方案

  1. 增大内存大小
  2. 优化对象和代码

OutOfMemoryError:unable to create new native thread

解释:无法创建更多线程
原因:应用进程创建线程数超过系统承载极限。Linux系统默认允许单个进程创建不超过1024个线程

OutOfMemoryError:Metaspace

解释:方法区溢出
原因:大量类被加载

解决方案:

解决方案:
1、修改JVM启动参数,直接增加内存。
JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx参数一定不要忘记加。

2、找出可能发生内存溢出的位置,并解决:
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询,修改成分页查询。
检查List、Map等集合对象是否有使用完后,未清除的问题。
使用内存查看工具动态查看内存使用情况。

Java有哪些引用类型

  • 强引用

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();
  • 软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

  • 弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

  • 虚引用

又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

GC

简述下垃圾回收机制

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

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

GC回收算法有哪些

1、标记-清除算法

对于标记清除,分为2部分,先进行标记,然后把没有标记到的清除掉。可达性分析法标记存活对象

缺点:清除垃圾后会造成内存碎片化,遇到大对象会因找不到合适的内存造成垃圾回收失败

2、标记-整理算法

标记清除算法的改进,标记清除中只是把可回收的对象进行垃圾回收,不会对剩余的内存空间进行整理,而标记整理则会对存活的对象进行整理。(用可达性分析法去标记)

缺点:虽然避免了碎片化问题,但是因为整理存活对象会让效率降低

3、复制算法

所谓复制算法,就是把内存分为2块等同大小的内存空间(A和B),每次只使用一块,内存回收时就把A中存活的对象放到另一块B中,然后把A中的对象全部清除。这样做效率会提升很多

缺点:只用了一半的内存

4、分代收集算法(现在在用)

在这里插入图片描述

  • 针对堆的新生代和老年代进行回收,young:old=1:2。
  • 新生代是采用复制算法进行垃圾回收,因为年轻代一般都是存活时间不长的对象,复制的对象数量不多,在第一次进行垃圾回收的时候,会把大部分的对象清除掉,这种情况下使用复制算法,只需要把少量存活的对象放入到另一块闲置的内存块中即可。
  • 而老年代中,一般对象的存活比例会很高,这种情况下,使用复制算法不能很好的提高性能和效率,把存活的对象移到另一个内存块时,会由于对象存活多而消耗的时间多,从而影响效率,这种情况下,使用标记整理或者标记清除<比较合适。
  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,标记整理只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记一整理”算法来进行回收

简述分代垃圾回收

  • 堆中有两个分区,新生代和老年代,比例是1:2
  • 新生代使用的是复制算法,新生代里有三个区,Eden、From Survivor、To Survivor比例是8:1:1
  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
    清空 Eden 和 From Survivor 分区;
    From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
  • 每次gc存活的对象gc年龄都会加1,达到阈值(CMS默认6,G1默认15)时进入老年代
  • 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。

如何判断一个对象是否可以被回收

1、引用计数算法(java不用)

为对象添加一个引用计数器,引用时的+1,引用失效的就-1,计数器为0就可以被回收。

缺点:当两个对象出现循环引用时计数器就永不为0,所以java虚拟机并不会使用这个方法。

2、可达性分析算法(java用)

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

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

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

说一下JVM有哪些垃圾收集器

垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。以串行模式工作的收集器,称为Serial Collector,即串行收集器;与之相对的是以并行模式工作的收集器,称为Paraller Collector,即并行收集器。

有7个收集器,可以分为新生代收集器和老年代收集器,以及回收整个堆区的收集器

  • Serial收集器: 新生代单线程收集器,采用的是复制算法,优点是单线程简单高效;

  • ParNew收集器: 新生代收并行集器,采用复制算法,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

  • Parallel Scavenge收集器: 新生代并行收集器,追求高吞吐量,采用复制算法,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间)

    • 如虚拟机总运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%
  • Serial Old收集器: 老年代单线程收集器,采用标记整理算法,Serial收集器的老年代版本;

  • Parallel Old收集器:老年代并行收集器,采用标记整理算法,吞吐量优先,Parallel Scavenge收集器的老年代版本;

  • CMS收集器:老年代并行收集器,采用标记清除算法,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

  • G1收集器:Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

详细介绍一下CMS垃圾回收器

  • CMS 是英文Concurrent Mark-Sweep的简称,老年代收集器,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。

  • 对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

  • CMS标记清除垃圾的过程

    • 初始标记:标记一下GC Roots能关联到的对象,速度很快,需要停顿(停止工作线程运行)
    • 并发标记:耗时最长
    • 重新标记:为了修正在并发标记期间,因为用户程序的继续运行而导致的标记产生的变动,需要停顿
    • 并发清除:不需要停顿
  • 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一块工作,不需要进行停顿

  • 优点:并发收集,低停顿

  • 缺点:

    • 吞吐量低:停顿时间是以牺牲吞吐量为代价实现的
    • 无法处理浮动垃圾(浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾)
    • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象

在这里插入图片描述

详细介绍一下G1垃圾收集器

  • G1回收器重新定义了堆空间,打破原有的分代模型,将堆划分为一个一个区,分别为E、S、O。对整个堆一起回收
  • 分区的好处是,带来了灵活性,每个分区都可独立进行垃圾回收,使得停顿时间可预测,用户可以指定操作在多长时间内完成
  • G1回收步骤:
    • 初始标记
    • 并发标记
    • 最终标记:修正并发标记时用户程序继续运行导致的标记变动,停顿
    • 筛选回收:首先对每个区的回收价值和成本进行排序,根据用户期望的停顿时间制定回收方案
  • 特点:
    • 空间整合:整体看是基于标记整理算法实现,局部看是基于复制算法实现,这意味着运行期间不会产生内存碎片
    • 可以指定回收操作的时间

jdk默认使用的是什么收集器:

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

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

什么是Minor GC,什么是Full GC

Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

说一下内存分配策略

1. 对象优先在 Eden 分配

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

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

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

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

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

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

4. 动态对象年龄判定

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

在这里插入图片描述

类加载过程/类的生命周期

类装载分为以下 5 个步骤:

  • 加载

加载指的是将类的class文件读入到方法区中,并为之在堆区创建一个java.lang.Class对象来封装方法区中类的数据结构

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

1、从本地文件系统加载class文件
2、从JAR包加载class文件
3、通过网络加载class文件。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

  • 连接

    • 验证:检查加载的 class 文件的正确性;

    • 准备:给类中的静态变量分配内存空间并赋予默认值0,使用的是方法区的内存;

    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用是指向目标的指针,直接指向内存中的地址;

  • 初始化:对静态变量和静态代码块执行初始化工作,给静态变量赋予正确的值。

    (private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。)

类加载时机

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(“com.lyj.load”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

类加载器

什么是类加载器

  • 类加载器就是把类文件加载到虚拟机中
  • 也就是说通过一个类的全限定名(包名+类名 java.lang.object)来获取描述该类的二进制字节流。

类加载器有哪些

  • 启动类加载器(Bootstrap ClassLoader):

    由C++实现,负责加载Java的核心库(JAVA HOME/jre/lib/rt.jar里的所有class)因为涉及到虚拟机的本地细节,所以开发者不能直接引用该加载器

  • 扩展类加载器(extensions class loader):

    它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。

  • 应用程序类加载器(Application ClassLoader):

    负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
    在这里插入图片描述

类加载机制

jvm类加载机制有三种

  • 全盘负责:当一个类加载器负责加载某个class时,该class所依赖的其他class也由该加载器负责
  • 双亲委派:一个类加载器首先将类加载器请求转发给父类加载器,当父类加载器无法完成时才自己加载
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

双亲委派机制的好处

  • java类跟他的加载器一起具备了一种优先级的层次关系,当父类加载了这个类,就没必要再加载一次,避免重复加载。

  • 其次,这种层级关系可以保证核心类不会被随意替换。例如用户手动编写了一个java.lang.object的类,jvm通过双亲委派机制找到启动类已加载过,就不会再去加载。保证了核心类不被替代的安全性。当用户编写一个与rt.jar类库中重名的java类。能够正常编译,但没法运行

new一个对象的过程

加载并初始化类创建对象。

  • Java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。加载并初始化类完成后,
  • 再进行对象的创建工作。

1、加载并初始化类(方法区)

  • 加载:

类加载器根据类的全限定名将此类的class文件加载到jvm方法区中

  • 连接

    验证:

​ 检查class文件的规范性

​ 准备:

​ 为类中的静态变量分配内存空间,并赋予初始值

​ 解析:

​ 把常量池中的符号引用转换为直接引用。符号引用仅仅是一个标识,而直接引用是指能直接指向目标的指针。

  • 初始化:

为静态变量赋值,执行静态代码块

(继承对象初始化顺序:父类静态变量->父类静态语句块->子类静态变量->子类静态代码块->父类实例变量->父类普通语句块->父类构造函数->子类实例变量->子类普通语句块->构造函数)

2、创建对象

  • 在堆区中分配对象需要的内存,包括本类和父类的所有实例变量,不包括静态变量
  • 把方法区中的实例变量的定义信息拷贝一份到堆中,然后赋默认值
  • 执行初始化代码,先初始化父类再初始化子类,先执行实例代码块然后再是构造方法
  • 如果有类似于Student s=new Student()形式的s引用的话,在栈区定义Student类型引用变量s,然后将堆区对象的地址赋值给它
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值