首页 > 编程语言 >2023JAVA面试重点

2023JAVA面试重点

时间:2023-02-05 14:58:04浏览次数:61  
标签:Java 收集器 对象 重点 面试 2023JAVA 线程 内存 方法

JAVA基础

== 和 equals 比较有什么区别?

基本数据类型
4 种整数类型:int、long、byte、short  
2 种浮点数类型:float、double  
1 种字符类型:char  
1 种布尔类型:boolean
引用数据类型

接口
数组

1、==
基本类型:对比它们的值是否相等
引用类型:对比它们的内存地址是否相等

2、equals()
如果没有对 equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如 String、Date 等类对 equals 方法进行了重写的话,比较的是所指向的对象的内容。
但是要注意:equals 方法不能作用于基本数据类型的变量,这是因为基本数据类型非对象的缘故,没有方法。

equals 和 hashCode 的区别和联系?

equals 和 hashcode 的关系
如果 equals 为 true,hashcode 一定相等;
如果 equals 为 false,hashcode 不一定不相等;
如果 hashcode 值相等,equals 不一定相等;
如果 hashcode 值不等,equals 一定不等;

重写 equals 方法时,一定要重写 hashcode 方法

Java 到底是值传递还是引用传递?

Java 是值传递。

当传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;当传的是引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值;当传的是 String 类型时,虽然拷贝的也是引用地址,指向的是同一个数据,但是 String 的值不能被修改,因此无法修改原变量中的值。

首先来解释一下什么是引用传递,什么是值传递。

引用传递(pass by reference)是指在调用方法时将实际参数的地址直接传递到方法中,那么在方法中对参数所进行的修改,将影响到实际参数。

值传递(pass by value)是指在调用方法时将实际参数拷贝一份传递到方法中,这样在方法中如果对参数进行修改,将不会影响到实际参数。

那在 Java 中到底是引用传递还是值传递呢?其实这个问题也一直是争论不断,而且官方也没给个确切答案。但是就我个人理解,Java 是值转递。

在 Java 程序中,实际上是值传递,只不过传递的是引用对象的内存地址。是将引用对象的内存地址复制了一份传递了过去。

this 和 super 有什么区别?

super: 它引用父类中的成员
super. 成员函数据名(实参)

this:它代表当前对象名
super () 和 this () 类似,区别是,super () 在子类中调用父类的构造方法,this () 在本类内调用本类的其它构造方法。

super () 和 this () 均需放在构造方法内第一行。 尽管可以用 this 调用一个构造器,但却不能调用两个。
this 和 super 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
this () 和 super () 都指的是调用构造方法,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。

Switch Case 支持哪几种数据类型?

switch 表达式后面的数据类型只能是 byte,short,char,int 四种整形类型,枚举类型和 java.lang.String 类型(从 java 7 才允许),不能是 boolean 类型。

为什么不能用 + 拼接字符串?

String 在调用 + 之前都会 new StringBuilder,然后才 append,这是比较耗时间的。

方式一:+ 最常见的方式

        String aa = "今天";
        String bb = "明天";
        System.out.println(aa+bb);

方式二:StringBuilder.append () 和 StringBuffer.append()
先有 StringBuffer 后有 StringBuilder,两者就像是孪生双胞胎,该有的都有,只不过大哥 StringBuffer,大部分方法都经过 synchronized 修饰,所以 StringBuffer 是线程安全的,但是它效率就相对 StringBuilder 较低

        String aa = "今天";
        String bb = "明天";
        StringBuilder sber = new StringBuilder();
        StringBuffer sbf = new StringBuffer();
        sber.append(aa).append(bb);
        System.out.println(sber.toString());
        sbf.append(aa).append(bb);
        System.out.println(sbf.toString());

方式三:String 类下的 cocat () 方法
如果拼接的字符串是 null,concat 会抛出 NullPointerException。如果拼接的字符串是一个空字符串(“”),那么 concat 的效率要更高。如果拼接的字符串非常多,concat 的效率就会下降,因为创建的字符串对象越多,开销越大。

        String aa = "今天";
        String bb = "明天";
        String concat = aa.concat(bb);
        System.out.println(concat);

方式四:String 类下的 join () 方法
JDK1.8 提供了一种新的字符串拼接姿势:String 类增加了一个静态方法 join,第一个参数为字符串连接符

		String aa = "今天";
        String bb = "明天";
        String join = String.join("-", aa, bb);
        System.out.println(join);

方式五:StringJoiner
StringJoiner 是 JDK1.8,java.util 包中的一个类,用于构造一个由分隔符重新连接的字符序列

		String aa = "今天";
        String bb = "明天";
        StringJoiner sj = new StringJoiner(":", "[", "]");
        sj.add(aa).add(bb);
        System.out.println(sj.toString());

方式六:StringUtils.join ()
实战项目中,我们处理字符串的时候,经常会用到这个类.org.apache.commons.lang3.StringUtils 包下,该方法更善于拼接数组中的字符串,并且不用担心 NullPointerException。

		String aa = "今天";
        String bb = "明天";
        String ids[] = {"1","2","3"};
        System.out.println(StringUtils.join(aa,bb,"-","124"));
        String join1 = StringUtils.join(ids,",");
        System.out.println(join1);

性能比较:
StringBuilder < StringBuffer < concat < + < StringUtils.join

StringBuffer 和 StringBuilder 的区别?

1、线程安全
StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 synchronized 修饰。

2、缓冲区
StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个 toString 方法仍然是同步的。

3、性能
既然 StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder 的性能要远大于 StringBuffer。

String 与 StringBuffer 的区别

String:
1、String 创建的对象是不可变的,一旦创建不可改变
2、对象值可以改变其实是创建了一个新的对象,然后把新的值保存进去
3、String 类被 final 修饰,不可以被继承
4、String 创建的对象的值存在于常量池,不用的时候不会被销毁
5、String 运行时间较长
6、String 适用于比较短而小的字符串

StringBuffer:
1、StringBuffer 创建的对象是可变的
2、它的改变不像 String 那样重新创建对象,而是通过 构造方法
3、StringBuffer 创建的对象的值存在于栈区,不用的时候会被销毁
4、StringBuffer 运行时间较短
5、StringBuffer 适用于比较长的字符串、比较多的字符串

StringJoiner 有什么用?

StringJoiner 是 JDK1.8,java.util 包中的一个类,用于构造一个由分隔符重新连接的字符序列

String aa = "今天";
String bb = "明天";
StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add(aa).add(bb);
System.out.println(sj.toString());

static 关键字有什么用?

static 关键字有什么作用
static 关键字主要有两种作用:第一,为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。第二,实现某个方法或属性与类而不是对象关联在一起,也就是说,在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。具体而言,在 Java 语言中,static 主要有 4 种使用情况:成员变量、成员方法、代码块和内部类。

(1) static 成员变量
虽然 Java 语言中没有全局的概念,但可以通过 statie 关键字来达到全局的效果。Java 类提供了两种类型的变量:用 static 关键字修饰的静态变量和不用 static 关键字修饰的实例变量。静态变量属于类,在内存中只有一个复制(所有实例都指向同一个内存地址),只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此就可以被使用了。对静态变量的引用有两种方式,分别为 “类。静态变量” 和 “对象。静态变量”。实例变量属于对象,只有对象被创建后,实例变量才会被分配空间,才能被使用,它在内存中存在多个复制。只能用 “对象。实例变量” 的方式来引用。以下是静态变量与实例变量的使用示例。

public class demo1 {
    public static int staticInt = 0;
    public int  nonStaticInt= 0;
    public static void main(String[] args) {
        demo1 d = new demo1();
        System.out.println("d.staticInt="+d.staticInt);
        System.out.println("demo1.staticInt="+demo1.staticInt);
        System.out.println("d.nonStaticInt=" + d.nonStaticInt);
        System.out.println("对静态变量和实例变量分别+1");
        d.staticInt++;
        d.nonStaticInt++;
        demo1 d1=new demo1();
        System.out.println("d1.staticInt="+d1.staticInt);
        System.out.println("demo1.staticInt="+demo1.staticInt);
        System.out.println("d1.nonStaticInt=" + d1.nonStaticInt);
    }
}
运行结果;

d.staticInt=0
demo1.staticInt=0
d.nonStaticInt=0
对静态变量和实例变量分别+1
d1.staticInt=1
demo1.staticInt=1
d1.nonStaticInt=0

从上例可以看出,静态变量只有一个,被类拥有,所有对象都共享这个静态变量,而实例对象是与具体对象相关的。需要注意的是,与 C ++ 语言不同的是,在 Java 语言中,不能在方法体中定义 static 变量。

(2) static 成员方法
与变量类似,Java 类同时也提供了 static 方法与非 static 方法。static 方法是类的方法,不需要创建对象就可以被调用,而非 static 方法是对象的方法,只有象被创建出来后才可以被使用。static 方法中不能使用 this 和 super 关键字,不能调用非 static 方法,只能访问所属类的静态成员变量和成员方法,因为当 static 方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。同理,static 方法也不能访问非 static 类型的变量。static 一个很重要的用途是实现单例模式。单例模式的特点是该类只能有一个实例,为了实现这一功能,必须隐藏类的构造函数,即把构造函数声明为 private,并提供一个创建对象的方法,由于构造对象被声明为 private,外界无法直接创建这个类型的对象,只能通过该类提供的方法来获取类的对象,要达到这样的目的只能把创建对象的方法声明为 static,程序示例如下:

class Singleton{
    private static Singleton instance=null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return  instance;
    }
}

用 public 修饰的 static 变量和方法本质上都是全局的,若在 statie 变量前用 private 修饰,则表示这个变量可以在类的静态代码块或者类的其他静态成员方法中使用,但是不能在其他类中通过类名来直接引用。

(3) static 代码块
static 代码块(静态代码块)在类中是独立于成员变量和成员函数的代码块的。它不在任何一个方法体内,JVM 在加载类时会执行 static 代码块,如果有多个 static 代码块,JVM 将会按顺序来执行。static 代码块经常被用来初始化静态变量。需要注意的是,这些 static 代码块只会被执行一次,示例如下:

public class demo1 {
    private static int a;
    static {
        demo1.a=4;
        System.out.println(a);
        System.out.println("static block is called");
    }
    public static void main(String[] args) {
 
    }
}
运行结果:

4
static block is called

(4)static 内部类
static 内部类是指被声明为 static 的内部类,它可以不依赖于外部类实例对象而被实例化,而通常的内部类需要在外部类实例化后才能实例化。静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型) 示例如下:

public class Outer {
    static int n=5;
    static class Inner{
        void accessAttrFromOuter(){
            System.out.println("Inner:Outer.n="+n);
        }
    }
    public static void main(String[] args) {
        Outer.Inner nest = new Outer.Inner();
        nest.accessAttrFromOuter();
    }
}
运行结果:

Inner:Outer.n=5

需要注意的是,只有内部类才能被定义为 static。

引申:
1、什么是实例变量?什么是局部变量?什么是类变量?什么是 final 变量?

实例变量:变量归对象所有(只有在实例化对象后才可以)。每当实例化一个对象时,会创建一个副本并初始化,如果没有显示初始化,那么会初始化一个默认值。各个对象中的实例变量互不影响。局部变量:在方法中定义的变量,在使用前必须初始化。类变量:用 static 可修饰的属性、变量归类所有,只要类被加载,这个变量就可以被使用(类名。变量名)。所有实例化的对象共享类变量。final 变量:表示这个变量为常量,不能被修改。

2、static 与 final 结合使用表示什么意思?

在 Java 语言中,static 关键字常与 final 关键字结合使用,用来修饰成员变量与成员有点类似于 C/C ++ 语言中的 “全局常量”。对于变量,若使用 statie final 修饰,则表示一值,就不可修改,并且通过类名可以访问。对于方法,若使用 statie final 修饰,则表示该不可覆盖,并且可以通过类名直接访问。
特别注意:在 Java 语言中,不能在成员函数内部定义 static 变量

static 变量和普通变量的区别?

1、所属目标不同
静态变量属于类的变量,普通变量属于对象的变量。

2、存储区域不同
静态变量存储在方法区的静态区,普通变量存储在堆区。

3、加载时间不同
静态变量是随时类的加载而加载的,随着类的消失而消失。
普通变量是随着对象的加载而加载,随着对象的消失而消失。

4、调用方式不同
静态变量只能通过类名,对象调用。
普通变量只能通过对象调用。

static 可以修饰局部变量么?

不能是局部变量,可以是内部类,全局变量,方法,代码块。

final、finally、finalize 有什么区别?

type 区别1 区别2
final 修饰符(修饰 变量,方法,类不可改变) final 变量 :表示常量,只能被赋值一次,赋值后值不再改变。final 方法 :不能(被子类的方法)覆盖,但可以被继承。final 类 :不能被继承,没有子类,final 类中的方法默认是 final。final 不能用于修饰构造方法。
finally 异常处理中的程序块 在异常处理时,使用 finally 块来进行必要的清理工作,不管是否发生异常
finalize 方法名 在垃圾回收器将内存中的对象进行清空之前,使用 finalize () 方法做清理工作

1、final 修饰符(关键字)。被 final 修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被 abstract 声明,又被 final 声明。将变量或方法声明为 final,可以保证他们在使用的过程中不被修改。被声明为 final 的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被 final 声明的方法也同样只能使用,即不能方法重写。

public  class  finalTest{
 	final   int  a=6;//final成员变量不能被更改
    final   int  b;//在声明final成员变量时没有赋值,称为空白final
 
    public finalTest(){
        b=8;//在构造方法中为空白final赋值
    }
 
    int do(final x){//设置final参数,不可以修改参数x的值
 
        return x+1;
    }
 
    void  doit(){
        final int i = 7;//局部变量定义为final,不可改变i的值
    }
}

2、finally 是在异常处理时提供 finally 块来执行任何清除操作。不管有没有异常被抛出、捕获,finally 块都会被执行。try 块中的内容是在无异常时执行到结束。catch 块中的内容,是在 try 块内容发生 catch 所声明的异常时,跳转到 catch 块中执行。finally 块则是无论异常是否发生,都会执行 finally 块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在 finally 块中。

3、finalize 是方法名。java 技术允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

Java 中是否可以覆盖 (override) 一个 private 或者是 static 的方法?

  1. override:子类重写父类的方法(返回值,方法名,参数都相同)以实现多态。

  2. private 只能够被自身类访问,子类不能访问 private 修饰的成员,所有不能 override 一个 private 方法

  3. static 方法是与类绑定的与任何实例都无关,随着类的加载而加载, static 是编译时静态绑定的,override 是运行时动态绑定的。形式上 static 可以 override,但是实际上并不能被 override。

普通类和抽象类有什么区别?

1、抽象类的存在时为了被继承,不能实例化,而普通类存在是为了实例化一个对象
2、抽象类的子类必须重写抽象类中的抽象方法,而普通类可以选择重写父类的方法,也可以直接调用父类的方法
3、抽象类必须用 abstract 来修饰,普通类则不用
4、普通类和抽象类都可以含有普通成员属性和普通方法
5、普通类和抽象类都可以继承别的类或者被别的类继承
6、普通类和抽象类的属性和方法都可以通过子类对象来调用

静态内部类和普通内部类有什么区别?

1、普通内部类和静态内部类的区别:

a) 普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b) 普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c) 一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。

2、区别还有

a) 第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b) 第二,可以定义内部接口 ,且可以定义另外一个内部类实现这个内部接口
c) 第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d) 第四,内部类不能定义 static 元素
e) 第五,内部类可以多嵌套
f) static 内部类是内部类中一个比较特殊的情况,Java 文档中是这样描述 static 内部类的:一旦内部类使用 static 修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static 内部类具备所有外部类的特性(和外部类完全一样)。

静态方法可以直接调用非静态方法吗?

静态 static 方法中不能直接调用非静态 non-static 方法,但可以通过将对象引用传入静态方法内,进而再调用该对象非静态(non-static)方法;
在主函数 (static 方法) 中,我们经常需要创建某个类的实例,再引用其非静态方法。

静态变量和实例变量有什么区别?

(1) 实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。
(2) 静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
(3) 总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
(4) 举例:对于下面的程序,无论创建多少个实例对象,永远都只分配了一个 staticVar 变量,并且每创建一个实例对象,这个 staticVar 就会加 1;但是,每创建一个实例对象,就会分配一个 instanceVar,即可能分配多个 instanceVar,并且每个 instanceVar 的值都只自加了 1 次。

内部类可以访问其外部类的成员吗?

内部类可以访问所在外部类的成员。
但有一点需要注意:静态成员不能访问非静态成员,因此静态内部类(属于静态成员)就不能访问外部类的非静态成员。

内部类可以引用他包含类的成员吗?有没有什么限制?

完全可以。如果不是静态内部类,那没有什么限制! 一个内部类对象可以访问创建它的外部类对象的成员包括私有成员。
如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员。

接口和抽象类有什么区别?

1、抽象类和接口都不能直接实例化。如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,实现接口的时候,如不能全部实现接口 方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现。
7、抽象类里可以没有抽象方法
8、如果 — 个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可以继承接口,并且可多继承接口,但类只能单 — 继承。
11、接口可以通过匿名内部类实例化。接口是对动作的抽象,抽象类是对根源的抽象。抽象类表示的是,这个对象是什么。而接口表示的是,这个对象能做什么。

扩展说明设计层面区别如下

1,抽象类是对事物的抽象,即对类抽象;接口是对行为抽象,即局部抽象。抽象类对整体形为进行抽象,包括形为和属性。接口只对行为进行抽象。
例子:举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类 Airplane,将鸟设计为一个类 Bird,但是不能将飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口 Fly,包含方法 fly (),然后 Airplane 和 Bird 分别根据自己的需要实现 Fly 这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承 Airplane 即可,对于鸟也是类似的,不同种类的鸟直接继承 Bird 类即可。从这里可以看出,继承是一个 "是不是" 的关系,而 接口 实现则是 "有没有" 的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

2,抽象类是多个子类的像类,是一种模板式设计;接口是一种形为规范,是一种辐射式设计。
例子:最简单例子,大家都用过 ppt 里面的模板,如果用模板 A 设计了 ppt B 和 ppt C,ppt B 和 pptC 公共的部分就是模板 A 了,如果它们的公共部分需要改动,则只需要改动模板 A 就可以了,不需要重新对 ppt B 和 pptC 进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

接口是否可以继承接口?

1、接口可以继承接口,抽象类不可以继承接口,但可以实现接口。
2、抽象类可以继承实体类。抽象类可以实现 (implements) 接口,抽象类是否可继承实体类,取决于实体类必须是否有明确的构造函数。
3、抽象类可以继承实体类,这是因为抽象类可继承性且有方法。
4、一个接口可以继承多个接口. interface C extends A, B {} 是可以的;
5、 一个类可以实现多个接口: class D implements A,B,C {}, 但是一个类只能继承一个类,不能继承多个类 class B extends A {}

在继承类的同时,也可以实现多个接口: class E extends D implements A,B,C {} 这也正是选择用接口而不是抽象类的原因。

接口与类的区别:
1、接口不能用于实例化对象。
2、接口没有构造方法。
3、接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
4、接口不能包含成员变量,除了 static 和 final 变量。
5、接口不是被类继承了,而是要被类实现。
6、接口支持多继承。

抽象类和接口的区别
1、抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
2、抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
3、接口中不能含有静态代码块以及静态方法 (用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
4、一个类只能继承一个抽象类,而一个类却可以实现多个接口。

注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
注:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为 "默认方法",默认方法使用 default 关键字修饰。
注:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。

接口里面可以写方法实现吗?

接口之所以成为接口,就在于它没有实现,只是声明。但后来一切都变了,Java 里出现了默认方法,C# 也出现了默认方法。接口已经不像传统意义上的接口,其概念开始向抽象类靠近,一个纯抽象的东西,突然出现了实体,于是开始傻傻分不清了。
在 jdk8 之前,接口里的方法都是定义的方法,没有方法体。需要实现类去实现。
那在 JDK8,就出现了默认方法,就是接口里面方法,有方法体,只需要在方法上增加个 default

抽象类必须要有抽象方法吗?

抽象类中不一定要有抽象方法。

在编程语句中用 abstract 修饰的类是抽象类。抽象类是不完整的,它只能用作基类,不能生成对象。抽象类可以包含抽象方法、非抽象方法和抽象访问器。可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。

扩展资料:
抽象类不能直接实例化,并且对抽象类使用 new 运算符会导致编译时错误。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须或者为 null,或者含有对非抽象类的实例的引用

抽象类提供多个派生类共享基类的公共定义,它既可以提供抽象方法,也可以提供非抽象方法。如果派生类没有实现所有的抽象方法,则该派生类也必须声明为抽象类。另外,实现抽象方法由 overriding 方法来实现。

抽象类能使用 final 修饰吗?

不能

抽象类是否可以继承具体类?

抽象类是可以继承实体类,但前提是实体类必须有明确的构造函数

抽象类是否可以实现接口?

可以的,而且这是抽象类的一个重要作用。
当一个类需要去实现一个接口时,如果该类实现了接口中的所有方法,则该类既可以定义为实体类也可以定义为抽象类;
如果该类实现了接口中的部分方法,还有部分方法没有实现,没有实现的部分方法只能继续以抽象方法的形式存在该类中,则该类必须定义为抽象类。
这么做的目的是:当我们需要定义一个类去实现接口中的部分方法时,我们可以先通过抽象类来实现其它部分的方法,然后去继承这个抽象类,就可以达到只实现接口中部分方法的目的;
试想如果是需要定义多个类都需要去实现接口中的部分方法,这时抽象类的作用就突出了,可以降低实现类实现接口的难度,同时解决了代码冗余的问题,所以这种情况在实际开发中的应用场景也是很多的。

Java 类初始化顺序是怎样的?

父类的静态字段 > 父类静态代码块 > 子类静态字段 > 子类静态代码块 > 父类成员变量 > 父类构造代码块 > 父类构造器 > 子类成员变量 > 子类构造代码块 > 子类构造器。

为什么 byte 取值范围为 -128~127?

01111111=127
10000000=128
当 01111111+1=10000000 时,这个时候最高位从全是 0 变化到全是 1,根据这个条件,我们恰好可以放入区分正负的条件 0 和 1,也就是 —— 最高位代表符号位。
通过观察最高位为 0 即为正数,所以正数的最大为:127,根据同模概念,负数最大就是 - 128.

  1. 原码
    原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。这里真值表示最高位不存放符号且参与到计算中。

    [+1] (原) = [0000 0001]
    [-1] (原) = [1000 0001]
    因为第一位是符号位,所以 8 位二进制数的取值范围就是:[1111 1111, 0111 1111] 即 [-127, 127]
    原码是人脑最容易理解的和计算的表达方式。

  2. 反码
    反码的表示方式是:正数的反码是其本身
    负数的反码是在其原码的基础上,符号位不变,其他依次取反

    [+1] = [0000 0001] (原) = [0000 0001] (反)
    [-1] = [1000 0001] (原) = [1111 1110] (反)

  3. 补码
    补码的表示方式是:正数的补码就是原码本身
    负数的补码就是其反码加 1
    [+1] = [0000 0001] (原) = [0000 0001] (反) = [0000 0001] (补)
    [-1] = [1000 0001] (原) = [1111 1110] (反) = [1111 1111] (补)

char 类型可以存储中文汉字吗?

char 型变量是用来存储 unicode 编码的字符的,unicode 编码字符集中包含了汉字,所以 char 型变量中当然可以存储汉字。

但是,如果某个特殊的汉字没有被包含在 unicode 编码字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。

补充:unicode 编码占用两个字节,所以,char 类型的变量也是占用两个字节。

重载和重写有什么区别?

一、定义上的区别:

  1. 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。
  2. 覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。

二、规则上的不同:

  1. 重载的规则:
    必须具有不同的参数列表。
    可以有不同的访问修饰符。
    可以抛出不同的异常。
  2. 重写方法的规则:
    参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。
    返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
    访问修饰符的限制一定要大于被重写方法的访问修饰符。
    重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。

三、类的关系上的区别:
重写是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。

构造器可以被重写和重载吗?

构造器是不可以被重写的,因为构造器是不会被继承的,所有就不可能会被重写。但是构造器可以重载,根据不同的构造器构建不同的对象。

main 方法可以被重写和重载吗?

是可以被重载的
不可以被重写的,因为 main 方法是静态方法,静态方法是一个类方法,在使用的时候不需要实例化,是直接使用类名来调用方法的,而在 Java 中静态方法在编译时会结合在一起,所以不能覆盖静态方法,覆盖是针对于实例方法而言的。

私有方法能被重载或者重写吗?

  1. 它可以重载,不能重写。
  2. 重载:也就是说,可以在一个类中创建多个方法,这些方法的名称相同,但参数和定义不同。例如,公共类
class dog {
	//bark()方法是重载方法
	private void bark(){}
	private void bark(int a){} 
}
  1. 重写:类与子类之间的多态性,重新定义父类的函数。在子类中定义方法与其父类具有相同的名称和参数。
  2. 私有方法不能被重写,因为它们不能被子类访问。

为什么 java 需要 getter/setter 来获取私有属性?

首先,通过 g/s 来获取私有属性的值,是 javaBean 规范中的一条,主要是为了把对象私有的那点小秘密藏起来,避免被坏人看到。

举例说明,张三是一个对象,张三的钱包是他的一个属性,当然张三为了安全起见,会把钱包藏起来,只有自己能看到,也就是说,钱包是 private 的。张三还有个坑爹儿子:小三子。因为有了小三子,张三就需要提供一个供小三子领生活费的方法,而不是直接把钱包暴露给小三子。因为,直接暴露给小三子的话,会有以下几个问题:

  1. 张三控制不住小三子拿钱,万一拿去买了游戏皮肤就不好了;
  2. 张三有多少钱,都可以被小三子看到,但是很多时候,张三是不希望被小三子看到的(例如私房钱);
  3. 小三子长大以后给张三生活费,给了多少张三也不知道,就好像得了老年痴呆一样。

总之,通过方法来操作属性的根本目的就是为了保护自己的私有属性,不被外部直接访问。

Java 中的断言(assert)是什么?

assertion (断言) 在软件开发中是一种常用的调试方式,assertion 就是在程序中的一条语句,它对一个 boolean 表达式进行检查,一个正确程序必须保证这个 boolean 表达式的值为 true;如果该值为 false,说明程序已经处于不正确的状态下,系统将给出警告并且退出。一般来说,assertion 用于保证程序最基本、关键的正确性。assertion 检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion 检查通常是关闭的。

Java 异常有哪些分类?

java 程序设计语言提供了三种可抛出结构:受检查时异常(checked exception),运行时(run-time exception)和错误 (error)

  1. checkedException:我们比较熟悉的 Checked 异常有
    Java.lang.ClassNotFoundException
    Java.lang.NoSuchMetodException
    java.io.IOException

  2. RuntimeException:我们比较熟悉的 RumtimeException 类的子类有
    Java.lang.ArithmeticException
    Java.lang.ArrayStoreExcetpion
    Java.lang.ClassCastException
    Java.lang.IndexOutOfBoundsException
    Java.lang.NullPointerException

Error 和 Exception 有什么区别?

Error:Error 属于程序无法处理的错误,是 JVM 需要负担的责任,无法通过 try-catch 来进行捕获。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。

比如 StackOverFlowError;VirtualMachineError;OutofMemoryError;ThreadDeath

Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception 又可以分为运行时异常(RuntimeException,又叫非受检查异常 unchecked Exception)和非运行时异常(又叫受检查异常 checked Exception)。

Java 中常见的异常有哪些?

  1. NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
  2. SQLException:提供关于数据库访问错误或其他错误信息的异常。
  3. IndexOutOfBoundsException:指示某排序索引 (例如对数组、字符串或向量的排序) 超出范围时抛出。
  4. NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
  5. FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
  6. IOException:当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通用类。
  7. ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
  8. ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常。
  9. IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数。
  10. ArithmeticException:当出现异常的运算条件时,抛出此异常。例如,一个整数 “除以零” 时,抛出此类的一个实例。
  11. NegativeArraySizeException:如果应用程序试图创建大小为负的数组,则抛出该异常。
  12. NoSuchMethodException:无法找到某一特定方法时,抛出该异常。
  13. SecurityException:由安全管理器抛出的异常,指示存在安全侵犯。
  14. UnsupportedOperationException:当不支持请求的操作时,抛出该异常。
  15. RuntimeExceptionRuntimeException:是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。

Java 中常见的运行时异常有哪些?

  1. java.lang.NullPointerException
    调用 null. 属性 或者 null. 方法 就会报空指针异常
  2. java.lang.ClassNotFoundException
    异常的解释是 "指定的类不存在",这里主要考虑一下类的名称和路径是否正确即可
  3. java.lang.ArrayIndexOutOfBoundsException
    数组下标越界 ,"隐式(即用变量表示下标)调用经常出错.
  4. java.lang.NoSuchMethodError
    方法不存在错误。
  5. java.lang.IndexOutOfBoundsException
    索引越界异常。
  6. java.lang.NumberFormatException
    数字格式异常。转换格式时,抛出该异常。
  7. java.sql.SQLException
    Sql 语句执行异常
  8. java.io.IOException
    输入输出异常
  9. java.lang.IllegalArgumentException
    方法参数错误
  10. java.lang.IllegalAccessException
    无访问权限异常

运行时异常与受检查异常有什么区别?

运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。Java 编译器不会检查运行时异常。

受检异常是 Exception 中除 RuntimeException 及其子类之外的异常。Java 编译器会检查受检异常。

RuntimeException 异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常 (RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用 RuntimeException 异常。

你知道有哪些避免空指针的方法?

1、断言
2、Optional
jdk8 提供 Optional,一个可以包含 null 值的容器对象,可以用来代替 xx != null 的判断。
其中就可以使用 isPresent 方法

throw 和 throws 的区别?

  1. 作用不同: throw 用于程序员自行产生并抛出异常,throws 用于声明该方法内抛出了异常;
  2. 使用的位置不同: throw 位于方法体内部,可以作为单独语句使用;throws 必须跟在方法参数列表的后面,不能单独使用;
  3. 内容不同: throw 抛出一个异常对象,且只能是一个;throws 后面跟异常类,且可以跟多个异常类;
  4. 如果异常抛给了 main () 方法,主方法不处理任何异常,而交给 Java 中最大头 JVM,所以如果在 main 方法使用了 throws 关键字,则表示一切异常交给 JVM 进行处理。默认处理方式也是 JVM 完成。

try-catch-finally 中哪个部分可以省略?

catch 和 finally 语句块可以省略其中一个。

什么是自动装厢、拆厢?

自动装箱 :将原始类型自动转换为相应包装类的对象称为自动装箱。例如 - 将 int 转换为 Integer,将 long 转换为 Long,将 double 转换为 Double 等。
自动拆箱 :这只是自动装箱的逆过程。自动将包装类的对象转换为其对应的原始类型类型称为拆箱。例如 - 将 Integer 转换为 int,Long 转换为 long,将 Double 转换为 double 等。

你怎么理解 Java 中的自动类型转换和强制类型转换?

1、数据类型只会自动提升,不能自动降低
2、Java 中整数默认的数据类型是 int 类型
3、赋值 | 运算时 ,两边数据类型不一致时就会发生类型转换
4、自动类型转换 (隐式类型转换) :
(1)规则:从小到大 ,低字节向高字节自动提升
(2)顺序:
byte (1 字节) – > short (2 字节)-- > int (4 字节) – > long (8 字节) --> float (4 字节) – > double (8 字节)
char (2 字节)-- > int (4 字节) – > long (8 字节) --> float (4 字节) – > double (8 字节)
5、强制类型转换 (显式类型转换) 强制类型转换有数据丢失,一般不建议使用
规则:从大到小,高字节向低字节手动强制转换
顺序:
double(8 字节) – > float(4 字节) – > long(8 字节) – > int (4 字节) – > short (2 字节)-- > byte (1 字节)
double(8 字节) – > float(4 字节) – > long(8 字节) – > int (4 字节) – > char (2 字节)

Class.forName 和 ClassLoader 的区别?

相同点:
Java 中 Class.forName 和 classloader 都可以用来对类进行加载。

不同点:
a).Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
b).而classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
c).Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。

Java 8 都新增了哪些新特性?

1、Lambda 表达式
Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。

2、方法引用
方法引用提供了非常有用的语法,可以直接引用已有 Java 类或对象(实例)的方法或构造器。与 lambda 联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

3、默认方法
默认方法就是一个在接口里面有了一个实现的方法。

4、新工具
新的编译工具,如:Nashorn 引擎 jjs、 类依赖分析器 jdeps。

5、 Stream API
新添加的 Stream API(java.util.stream) 把真正的函数式编程风格引入到 Java 中。

6、Date Time API
加强对日期与时间的处理。

7、Optional 类
Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

8、Nashorn JavaScript 引擎
Java 8 提供了一个新的 Nashorn javascript 引擎,它允许我们在 JVM 上运行特定的 javascript 应用。

怎么创建一个 Stream 流?

  1. Collection.stream()
@Test
void collection_stream_test() {
    List<String> list = Arrays.asList("a", "b", "c", "d");
    Stream<String> stream = list.stream();
}
  1. Arrays.stream(T[] array)
@Test
void arrays_stream_test() {
    int[] array = {1,2,3,4,5,6};
    IntStream stream = Arrays.stream(array);
}
  1. Stream.of
@Test
void stream_of_test() {
    Stream<Integer> stream = Stream.of(1, 2, 3);
}
  1. Stream.iterate
/**
 * 指定一个常量seed,生成从seed到常量 f(由 UnaryOperator返回的值得到)的流。
 * 如下:
 * 根据起始值seed(0),每次生成一个指定递增值(n+1)的数,limit(5)用于截断流的长度,即只获取前5个元素。
 */
@Test
void stream_iterate_test() {
    Stream<Integer> stream = Stream.iterate(0, n -> n + 1).limit(5);
    stream.forEach(System.out::println);
}
  1. Stream.generate
/**
 * 返回一个新的无限顺序无序的流
 */
@Test
void stream_generate_test() {
    Stream<Double> stream = Stream.generate(Math::random).limit(3);
    stream.forEach(System.out::println);
}

List、Set、Map 之间的区别是什么?

  1. List:
    有序、可重复。通过索引查找快,增删速度慢 (操作时后续的数据需要移动)。

  2. Set:
    无序、不可重复的集合。

  3. Map:
    键值对、键唯一、值不唯一。Map 集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对 map 集合遍历时先得到键的 set 集合,对 set 集合进行遍历,得到相应的值。

具体对比:

  1. List
    ArrayList 底层是数组,查询快,增删慢,线程不安全,效率高;
    Vector 底层是数组,查询快,增删慢,线程安全,效率低;
    LinkedList 底层是双向循环链表,查询慢,增删快,线程不安全,效率高;
    对于随机访问 get 和 set,ArrayList 较优,
    因为 LinkedList 要移动指针;对于新增和删除操作 add 和 remove,LinedList 较优,因为 ArrayList 要移动数据。

  2. Set
    HashSet:(无序,唯一)底层是哈希表,通过 hashCode()和 eques()保证元素唯一;
    LinkedHashSet:(有序,唯一)底层是链表和哈希表 。 由链表保证元素的排序 , 由哈希表证元素的唯一性;
    TreeSet:(有序,唯一)底层是红黑树,排序通过自然排序和比较强排序;

  3. Map
    HashMap 底层是哈希表,允许 null 键和 null 值,线程不安全的,效率高(通过 hashCode()和 equals()保证元素唯一);
    HashTable 底层是哈希表,不允许 null 键和 null 值,线程安全的,效率低,内部的方法基本都经过 synchronized 修饰;
    LinkedHashMap: 底层是哈希表和链表;
    Hashtable 和 HashMap 都实现了 Map 接口,但是 Hashtable 的实现是基于 Dictionary 抽象类的。Java5 提供了 ConcurrentHashMap,它是 HashTable 的替代,比 HashTable 的扩展性更好;
    ConcurrentHashMap 提供了与 Hashtable 和 SynchronizedMap 不同的锁机制。Hashtable 中采用的锁机制是一次锁住整个 hash 表,从而在同一时刻只能由一个线程对其进行操作;而 ConcurrentHashMap 中则是一次锁住一个桶。

常用的线程安全的 List 集合有哪些?

1、Vector
这个是最常听到的线程安全的 List 实现,但是已经不常用了。
内部实现直接使用 synchronized 关键字对 一些操作的方法加锁。性能很慢。
2、SynchronizedList
使用 Collections.synchronizedList (list); 将 list 包装成 SynchronizedList
需要注意的是 SynchronizedList 的 add 等操作加了锁,但是 iterator () 方法没有加锁,如果使用迭代器遍历的时候需要在外面手动加锁。
适用场景:当不需要使用 iterator () 并且对性能要求不高的场景。

SynchronizedList 和 Vector 区别
1> SynchronizedList 有较好的扩展性,可以将 ArrayList ,LinkedList 等都改成同步的,而 Vector 底层只有数组的结构。
2> SynchronizedList 并没有对 Iterator 方法进行加锁,遍历时需要手动同步处理,Vector 加锁了。
3> SynchronizedList 可以指定锁定的对象。
4> 扩容机制不一样 SynchronizedList 1.5 倍 ,Vector 2 倍。
5、SynchronizedList 使用同步代码块,锁的范围更小。Vector 锁的方法。

3、CopyOnWriteArrayList
在写的时候加锁(ReentrantLock 锁),读的时候不加锁,大大提升了读的速度。
添加元素的时候,先加锁,再复制替换操作,再释放锁。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

适用场景:适用于读多写少的场景。

CopyOnWriteArraySet 这个集合也是在 add 时加锁,不过在增加元素前会先判断元素是否存在,不存在才会调 add 方法。

常用的线程安全的 Map 有哪些?

  1. hashtable
    Map<String,Object> hashtable=new Hashtable<String,Object>();

2.synchronizedMap:
Map<String,Object> synchronizedMap= Collections.synchronizedMap(new Hashtable<String,Object>());
它其实就是加了一个对象锁,每次操作 hashmap 都需要先获取这个对象锁,这个对象锁有加了 synchronized 修饰,锁性能跟 hashtable 差不多。

SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}

    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) {return m.containsKey(key);}
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) {return m.containsValue(value);}
    }
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }

3、ConcurrentHashMap
Map<String,Object> concurrentHashMap=new ConcurrentHashMap<String,Object>();

这个是目前使用最多,而且也是最推荐的一个集合,实现也是比较复杂的一个。我们看源码其实是可以发现里面的线程安全是通过 cas+synchronized+volatile 来实现的,其中也可看出它的锁是分段锁,所以它的性能相对来说是比较好的。整体实现还是比较复杂的。

HashMap 与 Hashtable 的区别?

1、hash 值不同
HashTable:直接使用对象的 hashCode
HashMap:重新计算 hash 值

2、两个遍历方式的内部实现不同
Hashtable、HashMap 两者都是使用了 Iterator,但是,因为一些历史原因,Hashtable 除了使用了 Iterator 之外,还使用了 Enumeration。

3、是否提供 contains 方法
Hashtable:Hashtable 和 HashMap 不同,它保留了 contains、containsValue 以及 containsKey3 个方法
HashMap: 它去掉了 Hashtable 的 contains 方法,改为了 containsKey 和 containsValue

4、内部实现使用的数组初始化和扩容方式不同
HashTable:在不指定容量的情况下的默认容量为 11; 不要求底层数组的容量一定要为 2 的整数次幂;扩容时将容量变为原来的 2 倍加 1。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有 Spring,MyBatis,Netty 源码分析,高并发、高性能、分布式、微服务架构的原理,JVM 性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
HashMap:在不指定容量的情况下的默认容量为 16; 要求一定为 2 的整数次幂;扩容时,将容量变为原来的 2 倍
HashTable 中 hash 数组默认大小是 11,增加的方式是 old*2+1

5、key 和 value 是否允许 null 值
Hashtable:key 和 value 都不允许出现 null 值
HashMap:null 能够作为键,这样的键只有一个,能够有一个或者是多个键所对应的值为 null

6、线程安全性不同
Hashtable:Synchronize; 在多线程并发的情况下,能够直接使用 Hashtable,不要自己为它的方法实现同步
HashMap:在缺省情况下是非 Synchronize 的;使用 HashMap 的时候就需要自己增加同步处理;HashMap 是线程不安全的

7、继承的父类不同
Hashtable:继承 Dictionary 类
HashMap:继承 AbstractMap 类

HashMap 在 JDK 8 中有哪些改变?

1、如果链表的长度超过了 8,那么链表将转换为红黑树。(桶的数量必须大于 64,小于 64 的时候只会扩容)
2、发生 hash 碰撞时,java 1.7 会在链表的头部插入,而 java 1.8 会在链表的尾部插入
3、Entry 被 Node 替代 (换了一个马甲)。

HashMap 的 put 方法逻辑?

  1. 判断当前是否初始化,进行初始化
  2. 判断当前位置有无节点,没有的话直接新 new 一个节点放入
  3. 当前位置有节点需要判断 key 是否相同,相同的话需要新值覆盖旧值
  4. 不相同的话判断当前节点是否是红黑树节点,是的话按照红黑树的插入方法插入
  5. 不是的话进行链表的遍历,查看是否有 key 相同的节点,有的话新值覆盖旧值,没有的话正常来说走到最后一个节点,使用尾插法插入即可

HashMap 的 get 方法逻辑?

1、当你传递一个 key 从 hashmap 总获取 value 的时候:
2、对 key 进行 null 检查。如果 key 是 null,table [0] 这个位置的元素将被返回。
3、key 的 hashcode () 方法被调用,然后计算 hash 值。
4、indexFor (hash,table.length) 用来计算要获取的 Entry 对象在 table 数组中的精确的位置,使用刚才计算的 hash 值。
5、在获取了 table 数组的索引之后,会迭代链表,调用 equals () 方法检查 key 的相等性,如果 equals () 方法返回 true,get 方法返回 Entry 对象的 value,否则,返回 null。

HashMap 是线程安全的吗?

首先 HashMap 是线程不安全的。JDK1.7 的时候采用头插法,多线程同时插入的时候,A 线程在插入节点 B,B 线程也在插入,遇到容量不够开始扩容,重新 hash,放置元素,采用头插法,后遍历到的 B 节点放入了头部,这样形成了环。JDK1.8 采用尾插法,会造成两种情况两个线程同时插入只有一个成功插入,还有就是可能会造成两次 resize (++size>threshold) 。
解决的方案:一、使用 HashTable 效率比较差。二、使用 ConcurrentHashMap 比较常用的。三、使用 Collections.synchronizedMap ()
以上三种 线程安全。

HashMap 是怎么解决 hash 冲突的?

1、开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从 Hash 表找到一个空闲位置然后把发生冲突的元素存入到这个位置,而在 java 中,ThreadLocal 就用到了线性探测法来解决 Hash 冲突

2、链式寻址法,这是一种常见的方法,简单理解就是把存在 Hash 冲突的 key,以单向链表来进行存储,比如 HashMap如图存在冲突的 key 直接以单向链表的方式去进行存储

3、再 Hash 法,就是通过某个 Hash 函数计算的 key,存在冲突的时候,再用另外一个 Hash 函数对这个可以进行 Hash,一直运算,直到不再产生冲突为止,这种方式会增加计算的一个时间,性能上呢会有一些影响

4、建立公共移除区,就是把 Hash 表分为基本表和益处表两个部分,凡是存在冲突的元素,一律放到益处表中

HashMap 是怎么扩容的?

一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的 2 倍。 HashMap 的容量是有上限的,必须小于 1<<30,即 1073741824。如果容量超出了这个

数,则不再增长,且阈值会被设置为 Integer.MAX_VALUE。

JDK7 中的扩容机制

  1. 空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数 组。

  2. 有参构造函数:根据参数确定容量、负载因子、阈值等。

第一次 put 时会初始化数组,其容量变为不小于指定容量的 2 的幂数,然后根据负载因

子确定阈值。

  1. 如果不是第一次扩容,则 新容量 = 旧容量 x 2 ,新阈值 = 新容量 x 负载因子 。

JDK8 的扩容机制

  1. 空参数的构造函数:实例化的 HashMap 默认内部数组是 null,即没有实例化。第一次 调用 put 方法时,则会开始第一次初始化扩容,长度为 16。

  2. 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的 2 的幂数, 将这个数设置赋值给阈值 (threshold)。第一次调用 put 方法时,会将阈值赋值给容量, 然后让 阈值 = 容量 x 负载因子。

  3. 如果不是第一次扩容,则容量变为原来的 2 倍,阈值也变为原来的 2 倍。(容量和阈值 都变为原来的 2 倍时,负载因子还是不变)。

此外还有几个细节需要注意:

首次 put 时,先会触发扩容 (算是初始化),然后存入数据,然后判断是否需要扩容;l 不是首次 put,则不再初始化,直接存入数据,然后判断是否需要扩容;

HashMap 如何实现同步?

使用 Collections.synchronizedMap () 方法
使用 ConcurrentHashMap;
对操作 Map 的方法实现一个对象锁;

ConcurrentHashMap 的数据结构?

jdk1.8 之前的数据结构:

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结合组成。默认有 16 个区段。Segment 是一种可重入锁 (ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Seqment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得与它对应的 Segment 锁.

不同线程操作不同的 Segment 就不会出现安全问题,性能上大提升。在 jdk1.8 之前,多线程操作同一个 Segment 不能并发,在 jdk1.8 优化了.

jdk1.8 之后的数据结构:

改进:没有 Segment 区段了,和 HashMap 一致了,数组 + 链表 + 红黑树 + 乐观锁 + synchronized

ConcurrentHashMap 数据结构与 1.8 中的 HashMap 保持一致,均为数组 + 链表 + 红黑树,是通过乐观锁 + Synchroni zed 来保证线程安全的。当多线程并发向同一个散列桶添加元素时。

若散列桶为空 , 此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。

若散列桶不为空,此时使用 Synchronized 来保证线程安全,先访问到的线程会给桶中的头节点加锁,从而保证线程安全。

ArrayList 和 Vector 的区别?

  1. ArrayList 是线程不安全的,Vector 是线程安全的

  2. ArrayList 使用默认构造器创建对象时是在调用 add() 方法时对 ArrayList 的默认容量进行初始化的,Vector 在调用构造器时就对容量进行了初始化

  3. ArrayList 存储数据的 Object 数组使用了 transient 关键字,Vector 的 Object 数组没有
    transient 解释:
    transient:Java 语言的关键字,变量修饰符,如果用 transient 声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java 的 serialization 提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient 型变量的值不包括在序列化的表示中,然而非 transient 型的变量是被包括进去的。使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,可以在这个域前加上关键字 transient。

简单的说,就是被 transient 修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。

  1. ArrayList 和 Vector 的扩容机制不同

什么是 CopyOnWriteArrayList?

CopyOnWriteArrayList 它是 ArrayList 的线程安全的变体,大概原理就是:初始化的时候只有一个容器,很长一段时间,这个容器数据,数量等没有发生变化的时候,大家(大多数线程)都是读取(假设这段时间里只发生读取操作)同一个容器中的数据,这样大家读取到数据都是唯一,一致,安全的,但是后来有人往里面增加了一个数据,这个时候 CopyOnWriteArrayList 底层实现添加的原理是先 copy 出一个容器(简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给之前旧的容器地址,但是在添加这个数据期间,其他线程如果要读取数据,仍然是读取旧的容器里的数据。

优点:解决开发工作中的多线程并发问题
缺点:

  1. 内存占用问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以使用 ConcurrentHashMap 来代替。
  2. 数据一致性:CopyOnWriteArrayList 容器只能保证数据的最终已执行,不能保证数据的实时一致性,所以如果希望写入的数据,马上能读取到,就不能使用 CopyOnWriteArrayList。

什么是 fail-safe?

② 安全失败(fail-safe):
现象:在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则不会抛出 Concurrent Modification Exception。

原理:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

什么是 fail-fast?

快速失败(fail—fast):
现象:在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception (并发修改异常)。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext ()/next () 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。(源码很清晰)

场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

fail-fast 与 fail-safe 有什么区别?

Fail Fast Fail Safe
抛出 ConcurrentModificationException 异常 x
拷贝原集合 x
内存开销 x
例子 HashMap、Vector、ArrayList、HashList CopyOnWriteArrayList、ConcurrentHashMap

HashSet 的底层实现原理是什么?

(1)基于 HashMap 实现的,默认构造函数是构建一个初始容量为 16,负载因子为 0.75 的 HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

(2)当我们试图把某个类的对象当成 HashMap 的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的 equals (Object obj) 方法和 hashCode () 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode () 返回值相同时,它们通过 equals () 方法比较也应该返回 true。通常来说,所有参与计算 hashCode () 返回值的关键属性,都应该用于作为 equals () 比较的标准。

(3)HashSet 的其他操作都是基于 HashMap 的。

怎么确保一个集合不能被修改?

我们可以采用 Collections 包下的 unmodifiableMap 方法,通过这个方法返回的 map, 是不可以修改的。他会报 java.lang.UnsupportedOperationException 错。

同理:Collections 包也提供了对 list 和 set 集合的方法。
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)

JVM

Java 8 中的永久代为什么被移除了?

Java 7 及以前版本的 Hotspot 方法区位于永久代,同时,永久代和堆虽然是相互隔离的,但它们使用的物理内存是连续的。而 Java 8 中的方法区存在于元空间中,同时,元空间不再与堆连续,而是存在于本地内存(Native memory)。

什么是类加载器?

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

类加载器的分类及作用?

启动类加载器 (引导类加载器,BootStrap ClassLoader)
这个类加载器是由 c/c++ 语言实现的,嵌套在 JVM 内部
它用来加载 Java 的核心库 (JAVA_HOME/jre/lib/rt.jar,resources.jar 或 sun.boot.class.path 路径下的内容), 用于提供 JVM 自身需要的类
并不继承自 java.lang.ClassLoader, 没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,BootStrap 启动类加载器只加载包名为 java,javax,sun 等开头的类
当获取 一个 Class 的类加载器时,返回 null, 说明它是由引导类加载器加载的,所以扩展类加载器 getParent 返回 null
扩展类加载器 (Extension ClassLoader)
java 语言编写,由 sun.misc.Launcher\(ExtClassLoader 实现 派生于 ClassLoader 类 父类加载器为启动类加载器 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录 (扩展目录) 下加载类库。如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载 应用程序类加载器 (系统类加载器 AppClassLoader) java 语言编写,由 sun.misc.Launcher\)AppClassLoader 实现
派生于 ClassLoader 类
父类加载器为扩展类加载器
它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
该类加载器是程序中默认的类加载器,一般来说,java 应用都是由它来完成加载的
通过 ClassLoader.getSystemClassLoader () 来获取该类加载器
用户自定义类加载器
在 java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式
为什么要自定义加载器
隔离加载器
修改类加载的方式
扩展加载源
防止源码泄露
自定义类加载器实现步骤
继承抽象类 java.lang.ClassLoader
在 jdk1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass () 方法,从而实现自定义的类加载器,但在 jdk1.2 之后已不再建议用户去覆盖 loadClass () 方法,而是建议把自定义的类加载逻辑编写再 findClass () 方法中
oadClass () 方法,而是建议把自定义的类加载逻辑编写再 findClass () 方法中
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样可以避免自己去编写 findClass () 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁

什么是双亲委派模型?好处?

如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,因此所有的类加载请求最终都会传送到顶端的启动类加载器。
只有当父加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子加载器,子加载器会尝试去自己加载。
说白了:父加载器能加载的绝不给子加载器加载,只有父加载器找不到所需的类才让子加载器尝试加载。
双亲委派模型的好处主要在于 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。
例:
类 java.lang.Object,它存在在 rt.jar 当中,不管是哪一个类加载器要加载这个类,最后都会是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,所以,Object 类在程序的各种类加载器环境中都是同一个类。
相反的来说,假如没有双亲委派模型,而是由各个类加载器自行加载的话,假如,用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那么,系统当中将会出现多个不同的 Object 类,程序将混乱。
所以,假如开发者尝试编写一个与 rt.jar 类库中重名的 Java 类,能够正常编译,可是却永远也不能够被加载运行。

为什么要打破双亲委派模型?

由于加载范围的限制,顶层的 ClassLoader 无法访问底层 ClassLoader 所加载的类,此时需要破坏双亲委派模型。
1:什么情况下要打破双亲委派模型
实现类的动态加载
2:如何打破
自定义类加载器继承 ClassLoader ,然后重写 ClassLoader 中的 loadClass 方法,并采用
自己定义的类机制对类进行加载实现。
代码如下所示

/**
* ClassLoader 没有抽象方法为什么还要将此类定义为抽象类?外界不允许直接构建此
类对象
*/
public class BreakDoubleParentAppClassLoader extends ClassLoader {
private String baseDir ;
public BreakDoubleParentAppClassLoader ( String baseDir) {
this.baseDir = baseDir;}
/**
* 重写 loadClass 方法,打破双亲委派模型
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class <?> loadClass ( String name) throws ClassNotFoundException{
try {
// 我们自己先去加载,加载不了 ( 例如 java.lang.Object) 则抛出异常
return findClass(name);
} catch ( Exception e){
// 自己加载不了,再交给 parent 加载器对加载
return super.loadClass(name);
}}}

可以自定义一个 java.lang.String 吗?

可以,但在应用的时候,需要用自己的类加载器去加载,否则,系统的类加载器永远只是去加载 jre.jar 包中的那个 java.lang.String。
但在 Tomcat 的 web 应用程序中,都是由 webapp 自己的类加载器先自己加载 WEB-INF/classess 目录中的类,然后才委托上级的类加载器加载,如果我们在 Tomcat 的 web 应用程序中写一个 java.lang.String,这时候 Servlet 程序加载的就是我们自己写的 java.lang.String,但是这么干就会出很多潜在的问题,原来所有用了 java.lang.String 类的都将出现问题。

内存屏障是什么?

内存屏障就是一类同步屏障指令,是 CPU 或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。

什么是 Happens-Before 原则?

先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生是 Java 内存,模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,那么操作 A 产生的影响能够被操作 B 观察到。
Java 内存模型中存在的天然的先行发生关系:

  1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
  2. 管程锁定规则:一个 unlock 操作先行发生于后面(时间上)对同一个锁的 lock 操作。
  3. volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面(时间上)对这个变量的读操作。
  4. 线程启动规则:Thread 的 start ( ) 方法先行发生于这个线程的每一个操作。
  5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过 Thread.join () 方法结束、Thread.isAlive () 的返回值等手段检测线程的终止。
  6. 线程中断规则:对线程 interrupt () 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupt () 方法检测线程是否中断
  7. 对象终结规则:一个对象的初始化完成先行于发生它的 finalize()方法的开始。
  8. 传递性:如果操作 A 先行于操作 B,操作 B 先行于操作 C,那么操作 A 先行于操作 C。
    总结:一个操作 “时间上的先发生” 不代表这个操作先行发生;一个操作先行发生也不代表这个操作在时间上是先发生的(重排序的出现)。
    时间上的先后顺序对先行发生没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准。

什么是 MinorGC 和 FullGC?

MinorGC:
也可以叫作新生代 GC,指的是发生在新生代的垃圾收集动作。因为新生代中对象大部分的生命周期都很短,都是朝生暮死,所以 MinorGC 十分频繁,但因为需要移动的对象比较少及采用了 “复制” 回收算法,所以回收速度非常快。

FullGC:
也叫 MajorGC,指发生在老年代的 GC。由于老年代中存活的对象很多,且老年代一般都采用 “标记 - 整理” 回收算法,所以垃圾收集速度非常慢,耗费时间一般是 MinorGC 十倍以上。另外出现 FullGC 的时候一般会伴随至少一次的 MinorGC。

一次完整的 GC 流程是怎样的?

对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。
新生代 GC:Minor GC;老年代 GC:Full GC,比 Minor GC 慢 10 倍。
【总结】:内存区域不够用了,就会引发 GC,JVM 会 “stop the world”(简称STW),严重影响性能。Minor GC 避免不了,Full GC 尽量避免。
【处理方式】:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾回收器等。

常用的 JVM 参数有哪些?

堆设置
-Xms: 初始堆大小
-Xmx: 最大堆大小
-Xmn: 新生代大小
-XX:NewRatio: 设置新生代和老年代的比值。如:为 3,表示年轻代与老年代比值为 1:3
-XX:SurvivorRatio: 新生代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:为 3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个新生代的 1/5
-XX:MaxTenuringThreshold: 设置转入老年代的存活次数。如果是 0,则直接跳过新生代进入老年代
-XX:PermSize、-XX:MaxPermSize: 分别设置永久代最小大小与最大大小(Java8 以前)
-XX:MetaspaceSize、-XX:MaxMetaspaceSize: 分别设置元空间最小大小与最大大小(Java8 以后)
收集器设置
-XX:+UseSerialGC: 设置串行收集器
-XX:+UseParallelGC: 设置并行收集器
-XX:+UseParalledlOldGC: 设置并行老年代收集器
-XX:+UseConcMarkSweepGC: 设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数。
-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间
-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。
-XX:ParallelGCThreads=n: 设置并发收集器新生代收集方式为并行收集时,使用的 CPU 数。并行收集线程数。

新生代 Eden 与两个 Survivor 区的解释

1、为什么会有年轻代
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化 GC 性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC 的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当 GC 的时候先把这块存 “朝生夕死” 对象的区域进行回收,这样就会腾出很大的空间出来。

2、年轻代中的 GC
HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫 from 和 to)。默认比例为 8:1, 为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到 Eden 区 (一些大对象特殊处理), 这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的 (80% 以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在 GC 开始的时候,对象只会存在于 Eden 区和名为 “From” 的 Survivor 区,Survivor 区 “To” 是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值 (年龄阈值,可以通过 - XX:MaxTenuringThreshold 来设置) 的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To” 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From” 和 “To” 会交换他们的角色,也就是新的 “To” 就是上次 GC 前的 “From”,新的 “From” 就是上次 GC 前的 “To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 “To” 区被填满,“To” 区被填满之后,会将所有对象移动到年老代中。

  1. 一个对象的这一辈子
    我是一个普通的 java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区,居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了 20 年 (每次 GC 加一岁),然后被回收。

  2. 有关年轻代的 JVM 参数
    1)-XX:NewSize 和 - XX:MaxNewSize
    用于设置年轻代的大小,建议设为整个堆大小的 1/3 或者 1/4, 两个值设为一样大。
    2)-XX:SurvivorRatio
    用于设置 Eden 和其中一个 Survivor 的比值,这个值也比较重要。
    3)-XX:+PrintTenuringDistribution
    这个参数用于显示每次 Minor GC 时 Survivor 区中各个年龄段的对象的大小。
    4).-XX:InitialTenuringThreshol 和 - XX:MaxTenuringThreshold
    用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次 Minor GC 之后,年龄就加 1。

JVM 垃圾回收算法及回收器详解

一、GC Roots
我们先来了解一下在 Java 中是如何判断一个对象的生死的,有些语言比如 Python 是采用引用计数来统计的,但是这种做法可能会遇见循环引用的问题,在 Java 以及 C# 等语言中是采用 GC Roots 来解决这个问题。如果一个对象和 GC Roots 之间没有链接,那么这个对象也可以被视作是一个可回收的对象。

Java 中可以被作为 GC Roots 中的对象有:

虚拟机栈中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈(jni)即一般说的 Native 的引用对象。
回到顶部
垃圾收集算法
标记 - 清除
标记 - 清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发 full gc。

1、标记清除标记清除

复制算法
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

2、复制算法复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 研究表明新生代中的对象 98% 是朝夕生死的,所以并不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中的一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地拷贝到另外一个 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 的空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (可以通过 - SurvivorRattio 来配置),也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

3、标记 - 整理
标记 - 整理算法是一种老年代的回收算法,它在标记 - 清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

4、分代收集算法
当前商业虚拟机的垃圾收集都采用 “分代收集”(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清理” 或 “标记 - 整理” 算法来进行回收。

5、增量算法
增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

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

1、Serial 收集器
Serial 收集器是最古老的单线程的收集器,它的缺点是当 Serial 收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即 stop the world。到现在为止,它依然是虚拟机运行在 client 模式下的默认新生代收集器,与其他收集器相比,对于限定在单个 CPU 的运行环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。

2、Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用” 标记-整理 “算法。这个收集器的主要意义也是被 Client 模式下的虚拟机使用。在 Server 模式下,它主要还有两大用途:一个是在 JDK1.5 及以前的版本中与 Parallel Scanvenge 收集器搭配使用,另外一个就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 的时候使用。

通过指定 -UseSerialGC 参数,使用 Serial + Serial Old 的串行收集器组合进行内存回收。

3、ParNew 收集器
ParNew 收集器是 Serial 收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会 stop the world,只是相比较 Serial 收集器而言它会运行多条进程进行垃圾回收。

ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百的保证能超越 Serial 收集器。当然,随着可以使用的 CPU 的数量增加,它对于 GC 时系统资源的利用还是很有好处的。它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(譬如 32 个,现在 CPU 动辄 4 核加超线程,服务器超过 32 个逻辑 CPU 的情况越来越多了)的环境下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

-UseParNewGC: 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。

4、Parallel Scavenge 收集器
Parallel 是采用复制算法的多线程新生代垃圾回收器,似乎和 ParNew 收集器有很多的相似的地方。但是 Parallel Scanvenge 收集器的一个特点是它所关注的目标是吞吐量 (Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

5、Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和” 标记-整理” 算法。这个收集器是在 jdk1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是如果新生代 Parallel Scavenge 收集器,那么老年代除了 Serial Old (PS MarkSweep) 收集器外别无选择。由于单线程的老年代 Serial Old 收集器在服务端应用性能上的” 拖累 “,即使使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合” 给力 “。直到 Parallel Old 收集器出现后,” 吞吐量优先 “收集器终于有了比较名副其实的应用,在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

-UseParallelGC: 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行垃圾回收

6、CMS 收集器
CMS (Concurrent Mark Swep) 收集器是一个比较重要的回收器,现在应用非常广泛,我们重点来看一下,CMS 是一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字 (Mark Swep) 就可以看出,CMS 收集器是基于标记清除算法实现的。它的收集过程分为四个步骤:

初始标记 (initial mark)
并发标记 (concurrent mark)
重新标记 (remark)
并发清除 (concurrent sweep)
注意初始标记和重新标记还是会 stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。

不过由于 CMS 收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次 Full GC。为了解决这个问题,CMS 收集器默认提供了一个 - XX:+UseCMSCompactAtFullCollection 收集开关参数(默认就是开启的),用于在 CMS 收集器进行 FullGC 完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数 - XX:CMSFullGCsBeforeCompaction 参数用于设置执行多少次不压缩的 FULL GC 后跟着来一次带压缩的(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。

不幸的是,它作为老年代的收集器,却无法与 jdk1.4 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 jdk1.5 中使用 cms 来收集老年代的时候,新生代只能选择 ParNew 或 Serial 收集器中的一个。ParNew 收集器是使用 - XX:+UseConcMarkSweepGC 选项启用 CMS 收集器之后的默认新生代收集器,也可以使用 - XX:+UseParNewGC 选项来强制指定它。

由于 CMS 收集器现在比较常用,下面我们再额外了解一下 CMS 算法的几个常用参数:

UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
CMS 默认启动的回收线程数目是 (ParallelGCThreads+3)/4,如果你需要明确设定,可以通过 - XX:+ParallelCMSThreads 来设定,其中 - XX:+ParallelGCThreads 代表的年轻代的并发收集线程数目。
CMSClassUnloadingEnabled: 允许对元类数据进行回收。
CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是 - XX:+CMSClassUnloadingEnabled 激活了)。
CMSIncrementalMode:使用增量模式,比较适合单 CPU。
UseCMSCompactAtFullCollection 参数可以使 CMS 在垃圾收集完成后,进行一次内存碎片整理。内存碎片的整理并不是并发进行的。
UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
一些建议
对于 Native Memory:

使用了 NIO 或者 NIO 框架(Mina/Netty)
使用了 DirectByteBuffer 分配字节缓冲区
使用了 MappedByteBuffer 做内存映射
由于 Native Memory 只能通过 FullGC 回收,所以除非你非常清楚这时真的有必要,否则不要轻易调用 System.gc ()。
另外为了防止某些框架中的 System.gc 调用(例如 NIO 框架、Java RMI),建议在启动参数中加上 - XX:+DisableExplicitGC 来禁用显式 GC。这个参数有个巨大的坑,如果你禁用了 System.gc (),那么上面的 3 种场景下的内存就无法回收,可能造成 OOM,如果你使用了 CMS GC,那么可以用这个参数替代:-XX:+ExplicitGCInvokesConcurrent。

此外除了 CMS 的 GC,其实其他针对 old gen 的回收器都会在对 old gen 回收的同时回收 young gen。

7、G1 收集器
G1 收集器是一款面向服务端应用的垃圾收集器。HotSpot 团队赋予它的使命是在未来替换掉 JDK1.5 中发布的 CMS 收集器。与其他 GC 收集器相比,G1 具备如下特点:

并行与并发:G1 能更充分的利用 CPU,多核环境下的硬件优势来缩短 stop the world 的停顿时间。
分代收集:和其他收集器一样,分代的概念在 G1 中依然存在,不过 G1 不需要其他的垃圾回收器的配合就可以独自管理整个 GC 堆。
空间整合:G1 收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次 GC。
可预测的非停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
在使用 G1 收集器时,Java 堆的内存布局和其他收集器有很大的差别,它将这个 Java 堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。
虽然 G1 看起来有很多优点,实际上 CMS 还是主流。

GC 相关的常用参数:
Xmx: 设置堆内存的最大值。
Xms: 设置堆内存的初始值。
Xmn: 设置新生代的大小。
Xss: 设置栈的大小。
PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次 Minor GC 之后,年龄就会加 1,当超过这个参数值时就进入老年代。
UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。
SurvivorRattio: 新生代 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden: Suvivor= 8: 1。
XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
XX:GCTimeRatio: 设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
NewRatio: 设置新生代(包括 Eden 和两个 Survivor 区)与老年代的比值(除去持久代),设置为 3,则新生代与老年代所占比值为 1:3,新生代占整个堆栈的 1/4。

g1 和 cms 介绍 (适合面试)

CMS 垃圾回收器:
CMS 是作用于老年代的并发垃圾回收器 , 使用标记清除算法,工作流程是:初始标记,并发标记,再次标记,并发清除

优点
耗时最长的并发标记和并发清除阶段 gc 线程和用户线程是并发执行的,因此其 STW 时间短,适合对延迟有要求的任务

缺点:
CMS 在老年代使用的是标记清除算法,会产生大量内存碎片
GC 线程与用户线程并发执行,二者会抢占 cpu, 并且会产生浮动垃圾

初始标记阶段会发生短暂的 stw, 用于标记 GCRoot 对象能够直接到达的对象
并发标记阶段 gc 线程根据 GCRoot 对象标记可到到的存活对象,应用程序可以和 gc 线程并行进行,不需要 stw
再次标记阶段会进行 stw, 目的是为了修正因为并发标记阶段应用程序和 gc 线程并发执行产生的浮动垃圾
并发清除阶段 gc 线程清除垃圾对象,gc 线程和应用线程并发执行因此会产生浮动垃圾,在下一次 gc 清理该浮动垃圾

G1 垃圾回收器:
G1 垃圾回收器是一款可以同时管理新生代和老年代,在老年代使用标记整理算法,其最大的特点是将内存划分为多个大小相等 region, 每个 region 都可以作为伊甸区,survivor 区,老年代

优点:
老年代使用标记整理算法,不会产生内存碎片
使用 region, 不会出现新生代或者老年代分配空间过大而造成浪费
每次只选择垃圾对象多的 region, 而不是整个堆,大幅减少了 STW 时间 (但 region 与 region 之间是有依赖关系的,g1 维护了一个 Remembered Set 记忆集记录了 region 的依赖关系,只需要扫描关联的 region, 而不是整个堆)
用于可预测停顿的模型,可以指定 STW 时间 (也就是可预测停顿), 比如在一小时内垃圾回收导致的 "stop the world" 时间不超过一分钟。
G1 垃圾回收过程主要包含三个阶段,

当伊甸区慢时,年轻代使用标记复制算法进行回收
当堆空间的内存占用达到阈值时,老年代使用标记整理算法进行回收,前三个过程和 cms 类似,都为初始标记,并发标记,并发清除,区别在于最终清除阶段,CMS 是并发的,而 G1 会进行 STW, 不是并发的
当老年代占比达到阈值,触发混合回收,为了防止堆内存耗尽,会回收所有年轻代和部分老年代
CMS 和 G1 比较:

G1 和 CMS 都分为 4 个阶段,前三个阶段基本相同都为初始标记,并发标记,再次标记,区别在于最后清除阶段 CMS 是并发的,G1 不是并发的,因此 CMS 最终会产生浮动垃圾,只能等待下次 gc 才能清除
G1 可以管理整个堆,而 CMS 只能作用于老年代,并且 CMS 在老年代使用的是标记清除算法,会产生内存碎片,而 G1 使用标记整理算法,不会产生内存碎片
G1 相比于 CMS 最大的区别是 G1 将内存划分为大小相等的 Region, 可以选择垃圾对象多的 Region 而不是整个堆从而减少 STW, 同时使用 Region 可以更精确控制收集,我们可以手动明确一个垃圾回收的最大时间
补充:

因为耗时最长的并发标记和并发清除 gc 线程和应用线程都是并发执行的,所以总体来看 cms 收集器的 gc 线程和应用线程是并发执行的

G1 之所以能做到回收时间可控,主要是得益于 Region 这个结构。G1 会记录每个 Region 里的对象有多少是垃圾对象,如果要对某个 Region 进行垃圾回收,他会计算出对该 Region 回收的时间,可以回收多少垃圾。

实际上除了 CMS 收集器,其他都不存在只针对老年代的收集。

但是每个 region 之间是有互相引用的依赖关系的!这导致在 MinorGC 的时候会同时对老年代进行扫描(甚至是整个堆扫描),那就会导致 MinorGC 的效率低下,时间变长!
解决方法是:维护一个 Remebered Set 集合来存放各个 Region 之间的引用关系!当进行 GC Roots Tracing 的时候就可以只扫描 set 里的关联 region!而不用全堆扫描啦!!! 在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏了。

JVM 调优

通过并发进行老年代和年轻代的垃圾回收,减少由于内存不足反复 GC 的次数。原因在于 FullGC 会使得虚拟机短暂停顿,如果是串行进行垃圾回收,且回收次数过多,那么必然会导致虚拟机更长的停顿时长,应用在这段时间内的可用性大大降低,即应用响应慢了,极端点就可能造成系统奔溃。对于流量大的应用内存很快不足,那么自然 GC 次数也会随之增大。合理地设置 JVM 中的参数是能很好解决这类问题的,当然前提是代码编写也考虑了内存分配的机制,保证在代码层面上不会造成内存泄漏,否则单纯靠设置 JVM 参数就能解决,那肯定是不可靠的。

这里注意下 UseParNewGC 和 UseParallelGC 的区别,关注 JVM 参数之间配合使用,避免达到不可预期的效果:

(1)UseParNewGC:并发串行收集器,它是工作在新生代的垃圾收集器,它只是将串行收集器多线程化,除了这个并没有太多创新之处,而且它们共用了相当多的代码。它与串行收集器一样,也是独占式收集器,在收集过程中,应用程序会全部暂停。但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。

(2)UseParallelGC:并行收集器,同时运行在多个 cpu 之间,物理上的并行收集器,跟上面的收集器一样也是独占式的,但是它最大化的提高程序吞吐量,同时缩短程序停顿时间,另外它不能与 CMS 收集器配合工作。

JVM 性能调优总结
1、一般来说,当 survivor 区不够大或者占用量达到 50%,就会把一些对象放到老年区。通过设置合理的 Eden 区、survivor 区及使用率,可以将年轻对象保存在年轻代,从而避免 fullGC,使用 - Xmn 设置年轻代的大小。

2、对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在 Eden 区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致 fullGC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为 B,标明对象大小超过 1M 时,在老年代(tenured)分配内存空间。

3、一般情况下,年轻对象放在 Eden 区,当第一次 GC 后,如果对象还存活,则放在 survivor 区。此后,没 GC 一次,年龄增加 1,当对象的年龄达到阈值,就被放到 tenured 老年区。这个阈值可以通过
-XX:MaxTenringThreshold 设置。如果想让对象留在年轻代,可以设置比较大的阈值。

4、设置最小堆和最大堆:-Xmx 和 - Xms。稳定的堆大小对垃圾回收是有利的,获得一个稳定的堆大小的方法是设置 - Xms 和 - Xmx 的值一样,即最大堆和最小堆一样。如果这样设置,系统在运行时,堆大小理论上是
恒定的,稳定的堆空间可以减少 GC 次数。因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少 GC 次数,但是增加每次 GC 的时间,因为每次 GC 要把堆的大小维持在一个区间内。

5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得 GC 每次应对一个较小的堆空间,从而加快单次 GC 次数。基于这种考虑,JVM 提供两个参数,用于压缩和扩展堆空间。
(1)-XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认是 40,当堆空间的空闲内存比率小于 40,JVM 便会扩展堆空间。
(2)-XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空间比率。默认是 70,当堆空间的空闲内存比率大于 70,JVM 便会压缩堆空间。
(3)当 - Xmx 和 - Xms 相等时,上面两个参数无效。

6、通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
(1)-XX:+UseParallelGC: 年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少垃圾回收时间。
(2)-XX:+UseParallelOldGC: 设置老年代使用并行垃圾回收收集器。

7、尝试使用大的内存分页:使用大的内存分页增加 CPU 的内存寻址能力,从而提高系统性能。-XX:+LargePageSizeInBytes 这种内存页的大小。

8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC 老年代使用 CMS 收集器降低停顿。详细请参看:https://blog.csdn.net/u010013573/article/details/88782757

9、-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3

10、JVM 性能调优工具
(1)jps(Java Process Status):输出 JVM 中运行的进程状态信息(现在一般使用 jconsole)
(2)jstack:查看 java 进行内线程的堆栈信息。
(3)jmap:用于生成堆转存快照。
(4)jhat:用于分析 jmap 生成的堆转存快照(一般不推荐使用,而是使用 ecplise Memory Analyzer)
(5)jstat:JVM 统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
(6)VisualVM:故障处理工具

Java 虚拟机 —Java8 内存模型 JVM(整理版)

1、 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 Java 字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一 一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 情况的区域。

2、 Java 虚拟机栈
  与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  经常有人把 Java 内存区分为堆内存(Heap)和栈内存(Stack),其中所指的 “堆” 就是 Java 堆,而所指的 “栈” 就是现在所讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

  其中 64 为长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

3、本地方法栈
  本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定。HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

  参数设置:

-Xss 设置每个线程的栈大小。JDK1.5+ 每个线程栈大小为 1M,一般来说如果栈不是很深的话,1M 是绝对够用的啦。
  参数含义解析:

以 - X 开头的参数是和实现有关的,第一个 s 表示 stack,第二个 s 表示 size;
  注意:

    在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。

4、Java 堆
  对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展以及逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么 “绝对” 了。

  Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做 “GC 堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的,新生代可以有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

  根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 - Xmx 和 - Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

  参数设置:
-Xms 设置堆的最小空间大小;通常为操作系统可用内存的 1/64 大小即可。
-Xmx 设置堆的最大空间大小;通常为操作系统可用内存的 1/4 大小。
-Xmn 设置新生代大小,是对 - XX:newSize、-XX:MaxnewSize 两个参数的同时配置,这个参数是在 JDK1.4 版本以后出现的;通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间 = Eden + 1 个 Survivor,即 90%。
-XX:NewSize 设置新生代最小空间大小;
-XX:MaxNewSize 设置新生代最大空间大小;
-XX:NewRatio 新生代与老年代的比例,如 - XX:NewRatio=2,则新生代占整个堆空间的 1/3,老年代占 2/3。
-XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8 。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10。
  参数含义解析:

以 - X 开头的参数是和实现有关的,并不是适用于所有的参数;
最开始只有 -Xms 的参数,表示‘初始’ memory size,m 表示 memory,s 表示 size;
紧接是参数 -Xmx,为了对齐三字符,压缩了其表示形式,采用计算机中约定表示方式:用 x 表示 “” 大 “ (可以联想到衣服的号码大小,S、M、L、XL、XXL),因此 -Xmx 中的 m 应当还是 memory。既然有了最大内存的概念,那么一开始的 -Xms 所表示的” 初始 “内存也就有了一个” 最小 “内存的概念(其实常用的做法中初始内存采用的也就是最小内存)。如果不对齐参数长度的话,其表示应当是 - Xmsx。
  注意:

    开发过程中,通常会将 - Xms 与 - Xmx 两个参数的配置相同的值,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。

5、方法区(永久代)
  (1)方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,即存放静态文件,如 Java 类、方法等。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

  对于习惯在 HotSpot 虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为 “永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样 HotSpot 的垃圾收集器可以像管理 Java 堆一样管理这部分内存、能够省去专门为方法区编写内存管理代码的工作。

  根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
  方法区在不同虚拟机中有不同的实现,HotSpot 在 1.7 版本以前和 1.7 版本,1.7 版本后都有变化。
  ② 在目前已经发布的 JDK1.7 的 HotSpot 中,已经把原本放在永久代的字符串常量池移到了 Java 堆中。
  ③ jdk8 版本中则把永久代给完全删除了,取而代之的是 MetaSpace
   运行时常量池和静态变量都存储到了堆中,MetaSpace 存储类的元数据,MetaSpace 直接在本地内存中(Native memory),这样类的元数据分配只受本地内存大小的限制,OOM 问题就不存在了。

   参数设置:
-XX:PermSize 设置永久代最小空间大小;
-XX:MaxPermSize 设置永久代最大空间大小;
  参数含义解析:

PermSize,表示永久代初始设置大小,这里初始大小表示最小大小,Perm 是 permanent 永久的意思;
  注意:

JDK8 没有这个参数设置。
非堆内存不会被 Java 垃圾回收机制进行处理,在配置之前一定要慎重考虑下自身软件所需要的非堆区内存大小。
(2)运行时常量池
  运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  Java 虚拟机对 Class 文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求。不过,一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

  运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern () 方法。

  既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

6、直接内存(堆外内存)
  直接内存(Direct Memory),也叫堆外内存,它并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,而是 Java 虚拟机的堆以外的内存,直接受操作系统管理。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。使用堆外内存有两个优势,一是减少了垃圾回收,二是提升复制速度,如 NIO 就是采用堆外内存。可以使用未公开的 Unsafe 和 NIO 包下 ByteBuffer 来创建堆外内存。

  在 JDK1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

  显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 - Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

  参数设置:可以通过 -XX:MaxDirectMemorySize 参数来设置最大可用直接内存,如果 Java 虚拟机启动时未设置则默认为最大堆内存大小,即与 -Xmx 相同。即假如最大堆内存为 1G,则默认直接内存也为 1G,那么 JVM 最大需要的内存大小为 2G 多一些。当直接内存达到最大限制时就会触发 GC,如果回收失败则会引起 OutOfMemoryError。

7、JMM
  Java 内存模型(Java Memory Model,JMM)主要是为了规定线程和内存之间的一些关系。根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

8、JVM 和 JMM 之间的关系
  jmm 中的主内存、工作内存与 jvm 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

9、JVM 内存参数

默认堆中年轻代 (Young) 占 1/3,老年代 (Old) 占 2/3,年轻代中包含 Eden 区和 Survivor 区,Survivor 区包含 From (S0) 区和 To (区),默认新生代中 Eden 区、From 区、To 区的比例为 8:1:1,当 Eden 区内存不足时会触发 Minor gc,没有被回收的对象进入到 Survivor 区,同时分代年龄 + 1,当再次触发 Minor gc 时,From 区中的对象会移动到 To 区,Minor gc 会回收 Eden 区和 From 区中的垃圾对象,对象的分代年龄会一次次的增加,当分代年龄增加到 15 以后,对象会进入到老年代。

当老年代内存不足时,会触发 Full gc,如果 Full gc 无法释放足够的空间,会触发 OOM 内存溢出,在进行 Minor gc 或 Full gc 时,会触发 STW(Stop The World),即停止用户线程。
启动直接加在 bin 目录下 catalina.sh 文件里):

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的 1/64
-Xmx:设置堆的最大可用大小,默认物理内存的 1/4
-Xmn:新生代大小
-XX:NewRatio:默认 2 表示新生代占年老代的 1/2,占整个堆内存的 1/3。
-XX:SurvivorRatio:默认 8 表示一个 survivor 区占用 1/8 的 Eden 内存,即 1/10 的新生代内存。

关于元空间的 JVM 参数有两个:-XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 元空间最大值, 默认 - 1, 即不限制,或者说只受限于本地内存大小。

-XX:MetaspaceSize: 指定元空间触发 Fullgc 的初始阈值 (元空间无固定初始大小), 以字节为单位,默认是 21M 左右,达到该值就会触发 full gc 进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间,会适当提高该值( 如果设置了 - XX:MaxMetaspaceSize,不会超过其最大值 )。这个跟早期 jdk 版本的 - XX:PermSize 参数意思不一样,-XX:PermSize 代表永久代的初始容量。

由于调整元空间的大小需要 Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量 Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在 JVM 参数中将 MetaspaceSize 和 MaxMetaspaceSize 设置成一样的值,并设置得比初始值要大,对于 8G 物理内存的机器来说,这两个值可以都设置为 256M。

String.intern ()溢出

是 class 字节码文件加载到内存时,会把一些常量数据,类文件信息,静态变量,java 即时编译的代码等数据,是线程共享的数据。运行时常量池是方法区的一部分 (和 jdk 的版本有关系,后续会有说明),

常量池中的内容不一定都是编译的时候产生的,比如 String.intern () 方法,可以在运行时产生字符串放到常量池中,当常量池无法再申请到内存时,会抛出 OutOfMemoryError 异常。

多线程

JVM 锁:synchronized 原理详解

synchronized-----非公平锁
一个对象的对象头中除了指向对象类内存地址的 Class Address 之外,还有两个属性,分别是 Array Length, Mark Word。
Array Length 用于存储对象为数组对象时数组的长度,Mark Word 是一段内存,内存长度为 32 位或 64 位,这个由系统决定,如果系统是 32 位的那么 Mark Word 内存长度就是 32 位,反之 64 位系统,它的长度就是 64 位。
那么 Mark Word 是用来做什么的呢?其实他就是用来存储对象状态的,即锁的状态。

对象的锁状态可以分为:未锁定,轻量级锁,重量级锁,锁解锁,以及偏向锁。其中偏向锁又分为两种状态,即偏向锁开启未锁定,偏向锁开启已锁定。

线程对对象加锁的过程其实也就是对象 Mark Word 修改的一个过程:
step1. 首先线程会虚拟机栈中创建一个 Lock Record,用于存储对象 Mark Word 的当前值。
step2. 通过 CAS 操作将对象 Mark Word 的值进行修改,改为当前线程的地址,也就是 Lock record address.
在这个时候对象的锁也就加上了,也就是轻量级锁。
step3. 如果进行加锁的操作的时候存在多个线程一起操作,但是能加锁成功的线程只能是一个,那么其他线程在抢锁失败后,并不会进行阻塞,而是会进行自旋,不断的去抢锁,直到能抢到锁才会结束。
step4. 但是这样不断自旋 CAS 抢锁的行为其实是非常损耗性能的,所以 jvm 就又提出了当自旋的次数达到一个的阈值后,便会再次进行锁升级,升级为重量级锁。
step5. 当升级为重量级锁时,MarkWord 的内容就有发生了变化,为了保障性能,jvm 会对每个对象生成一个 Object Monitor 用于存储对象锁的相关信息,在 Object Monitor 中会存储对象 owner 的地址引用,也就是当前对象锁的拥有者的地址(即 Lock record address),此外,会将抢锁失败的线程,放入到锁池中(entryList), 并让相关线程进行挂起,线程阻塞,防止继续自旋消耗性能。
step6. 当拥有锁权限的线程结束了相关操作,则在锁池中的线程会再次进行抢锁。
step7. 若锁的 owner 线程调用 wait 方法,则会释放锁,同时会进入 Object Monitor 的等待池(waitSet)中,等待被下一位 owner 唤醒(notify),从而从等待池中唤醒,进入锁池再次进行抢锁操作。
step8. 锁只存在升级,不存在降级的情况,因此重量级锁之后,便是解锁操作了。(一个锁的闭环也就完成了)

偏向锁是可以通过配置参数进行开启和关闭的。知道了这个,也就可以知道为什么在之前的锁升级的图中会出现两种 “未锁定” 的状态了。那么偏向锁是怎么达到优化性能的目的的呢。其实在 jvm 中开启偏向锁后,如果只有一个线程进行加锁操作,那么我们可以认为其实这个锁是没有必要的,毕竟锁的存在的意义也就是为了保证在多线程并发的情况下保证资源操作的原子性,那么对一个资源的不断的加锁解锁操作也就没有必要了,毕竟加锁解锁的操作也是消耗性能的。因此当偏向锁开启后,如果没有其他线程进行抢锁的操作的话,当前线程 t1 在完成了锁的操作后,对象的锁也不会自动解锁掉,而是会继续持续一段时间,避免 t1 再次调用时,又要进行加锁操作,这样也就节省了部分性能。但是当有其他线程来抢锁时,偏向锁就会进行锁升级,变为轻量级锁,之后的锁升级也就和前文说的一样了。

锁类型 优点 缺点 适用场景
偏向锁 加锁、解锁不需要额外资源消耗,效率较高 如果线程间存在锁竞争,会带来额外的解锁消耗 适用只有一个线程访问同步块的情景
轻量级锁 竞争的线程不会阻塞,提高了程序响应速度 如果获取锁失败,会进入自旋消耗 cpu 针对锁占用时间短,对响应时间比较敏感的情况
重量级锁 线程竞争不使用自旋,不消耗 cpu 线程会被阻塞,影响响应时间 锁占用时间较长,对吞吐量要求较高

Synchronized 与 ReentrantLock 的区别?

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 块中释放锁

比读写锁 (ReadWriteLock) 更快的锁 (StampedLock)

1、JDK1.8 以前有那么多锁了,为什么还要 StampedLock?
一般应用,都是读多写少,ReentrantReadWriteLock 因读写互斥,故读时阻塞写,因而性能上上不去。可能会使写线程饥饿,StampedLock 营运而生。

2、StampedLock 原理:
StampedLockd 的内部实现是基于 CLH 锁的,一种自旋锁,保证没有饥饿且 FIFO。

3、特征
StampedLock 支持三种模式:写锁、悲观读和乐观读。
允许多 个线程同时获取乐观锁和悲观读锁。
只允许一个线程获取写锁,写锁和悲观读锁是互斥的。
使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly () 和写锁 writeLockInterruptibly ()。
StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。
StampedLock 不支持重入(ReadWriteLock 支持)
StampedLock 的悲观读锁、写锁都不支持条件变量。
StampedLock 支持锁的降级(通过 tryConvertToReadLock () 方法实现)和升级(通过 tryConvertToWriteLock () 方法实现),但是建议你要慎重使用。

4、优点:
相比于 ReentrantReadWriteLock,吞吐量大幅提升
缺点:
api 相对复杂,容易用错 内部实现相比于 ReentrantReadWriteLock 复杂得多

5、总结
synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock 四种锁,最稳定是内置 synchronized 锁(并不是完全被替代),当 并发量大且读远大于写的情况下最快的的是 StampedLock 锁(乐观读。近似于无锁)。建议大家采用。

有哪些锁优化的方式?

锁优化就是一些提高锁的效率的策略,下面以 synchronized 为例,来介绍优化操作。

  1. 锁消除: 编译器 + JVM 会根据代码运行的情况智能判定当前的锁是否必要,如果不必要,就直接把加锁的代码忽略。

  2. 偏向锁:第一个尝试加锁的线程,不会真的加锁,而是进入偏向锁状态(很轻量的标记),直到其他线程也来竞争这把锁,才会取消偏向锁状态,真正进行加锁

  3. 自旋锁:真的有多个线程竞争锁的时候,偏向锁状态被消除,此时线程使用自旋锁的方式来尝试进行获取锁。自旋锁能保证让其他想竞争锁的状态尽快获取到锁,付出了一定的 CPU 资源

  4. 膨胀锁 (无奈之举,严格上说,不能算优化)
    当锁竞争更加激烈,此时就会从自旋锁状态膨胀成重量级锁(挂起等待锁)

  5. 锁粗化
    如果一段逻辑中,需要多次加锁解锁,并且加锁解锁的时候没有其他线程来竞争,此时就会把多组加锁操作,合并到一起。
    粗化就是把 多组加锁解锁 操作合并成一组。每次加锁解锁操作,都有开销,减少加锁的次数,就能提高效率了。

创建一个线程池有哪些核心参数?

一、 corePoolSize 线程池核心线程大小
二、maximumPoolSize 线程池最大线程数量
三、keepAliveTime 多余的空闲线程存活时间
四、unit 空闲线程存活时间单位
五、workQueue 工作队列
1. ArrayBlockingQueue(数组的有界阻塞队列)
ArrayBlockingQueue 在创建时必须设置大小,按 FIFO 排序(先进先出)。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到 corePoolSize 后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到 maxPoolSize,则会执行拒绝策略。

2. LinkedBlockingQueue(链表的无界阻塞队列)
按 FIFO 排序任务,可以设置容量 (有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量 (无界队列)。该队列的吞吐量高于 ArrayBlockingQueue。由于该队列的近似无界性,当线程池中线程数量达到 corePoolSize 后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到 maxPoolSize,因此使用该工作队列时,参数 maxPoolSize 其实是不起作用的。有两个快捷创建线程池的工厂方法 Executors.newSingleThreadExecutor、Executors.newFixedThreadPool,使用了这个队列,并且都没有设置容量(无界队列)。

3.SynchronousQueue(一个不缓存任务的阻塞队列)
生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到 maxPoolSize,则执行拒绝策略。
其 吞 吐 量 通 常 高 于 LinkedBlockingQueue。 快捷工厂方法 Executors.newCachedThreadPool 所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

4. PriorityBlockingQueue(具有优先级的无界阻塞队列)
优先级通过参数 Comparator 实现。

5. DelayQueue(这是一个无界阻塞延迟队列)
底层基于 PriorityBlockingQueue 实现的,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。

Java 中的阻塞队列(BlockingQueue)与普通队列相比,有一个重要的特点:在阻塞队列为空时,会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中取元素时,线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

六、threadFactory 线程工厂
七、handler 拒绝策略
AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
DiscardPolicy:丢弃任务,但是不抛出异常
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 。也就是当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务从队尾添加进去,等待执行。
CallerRunsPolicy:谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被 shutdown 则直接丢弃

线程池工作流程

  1. 提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker () 方法创建一个核心线程去执行任务;
  2. 如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;
  3. 如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker () 方法创建一个非核心线程去执行任务;
  4. 如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略

总结来说就是优先核心线程、阻塞队列次之,最后非核心线程。

为什么阿里不让用 Executors 创建线程池?

  1. Executors 创建的六种
    Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
    Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
    Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
    Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
    Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
    Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】

  2. ThreadPoolExecutor 创建的一种(ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置)ThreadPoolExecutor 提供了四个构造方法

  3. FixedThreadPool SingleThreadPool
    允许请求队列长度为Integer.MAX_VALUE可能会堆积大量请求从而导致OOM
    CachedThreadPool ScheduledThreadPool
    允许创建线程数量为Integer.MAX_VALUE可能会创建大量线程从而导致OOM

  4. 创建一个规范线程池,我们参考 DUBBO 线程池定义命名工厂

public class NamedInternalThreadFactory extends NamedThreadFactory {
    public NamedInternalThreadFactory() {
        super();
    }

    public NamedInternalThreadFactory(String prefix) {
        super(prefix, false);
    }

    public NamedInternalThreadFactory(String prefix, boolean daemon) {
        super(prefix, daemon);
    }

    @Override
    public Thread newThread(Runnable runnable) {
        String name = mPrefix + mThreadNum.getAndIncrement();
        InternalThread ret = new InternalThread(mGroup, runnable, name, 0);
        ret.setDaemon(mDaemon);
        return ret;
    }
}

public class NamedThreadFactory implements ThreadFactory {
    protected static final AtomicInteger POOL_SEQ = new AtomicInteger(1);
    protected final AtomicInteger mThreadNum = new AtomicInteger(1);
    protected final String mPrefix;
    protected final boolean mDaemon;
    protected final ThreadGroup mGroup;

    public NamedThreadFactory() {
        this("pool-" + POOL_SEQ.getAndIncrement(), false);
    }

    public NamedThreadFactory(String prefix) {
        this(prefix, false);
    }

    public NamedThreadFactory(String prefix, boolean daemon) {
        mPrefix = prefix + "-thread-";
        mDaemon = daemon;
        SecurityManager s = System.getSecurityManager();
        mGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
    }

    @Override
    public Thread newThread(Runnable runnable) {
        String name = mPrefix + mThreadNum.getAndIncrement();
        Thread ret = new Thread(mGroup, runnable, name, 0);
        ret.setDaemon(mDaemon);
        return ret;
    }

    public ThreadGroup getThreadGroup() {
        return mGroup;
    }
}

再定义一个线程池,在线程池执行方法开放一个业务名称参数供调用方设置

public class ThreadPoolStarter {
      public static ThreadPoolExecutor getExecutor(String threadName) {
        if (executor == null) {
          synchronized (ThreadPoolStarter.class) {
            if (executor == null) {
              int coreSize = Runtime.getRuntime().availableProcessors();
              BlockingQueue<Runnable> queueToUse = new LinkedBlockingQueue<Runnable>(QUEUE_SIZE);
              executor = new ThreadPoolExecutor(coreSize, POOL_CORE_SIZE, MAX_SIZE, TimeUnit.SECONDS, queueToUse, new NamedInternalThreadFactory(threadName, true), new AbortPolicyDoReport(threadName));
            }
      }
    }
    return executor;
  }
}

public class ThreadExecutor {
  public static void execute(String bizName, Runnable job) {
    ThreadPoolStarter.getExecutor(bizName).execute(job);
  }

  public static Future<?> sumbit(String bizName, Runnable job) {
    return ThreadPoolStarter.getExecutor(bizName).submit(job);
  }
}

编写一个实例进行测试

public void testThread() throws Exception {
    for (int i = 0; i < 10000; i++) {
      ThreadExecutor.execute("BizName", new Runnable() {
        @Override
        public void run() {
          System.out.println("公众号互联网公园");
        }
      });
      Thread.sleep(1000L);
    }
  }
}

如何提交一个线程到线程池?

使用 execute 方法,无返回值,入参为常见的 Runnable 类型

ExecutorService executor = Executors.newCachedThreadPool();
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("do something");
    }
};
executor.execute(runnable);

使用 submit 方法,有返回值,submit 有 3 个重载函数,最常用的是下面这个,参为常见的 Callable 类型

Future<T> submit(Callable<T> task);

ExecutorService executor = Executors.newCachedThreadPool();
Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("do something");
        return "finished";
    }
};
Future<String> submit = executor.submit(callable);
//阻塞主线程,等待任务执行完毕,返回值为call()的返回值
System.out.println(submit.get());

线程池 submit 和 execute 有什么区别?

1、接收的参数不一样

2、submit 有返回值,而 execute 没有
用到返回值的例子,比如说我有很多个做 validation 的 task,我希望所有的 task 执行完,然后每个 task 告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。
然后我就可以把所有失败的原因综合起来发给调用者。
个人觉得 cancel execution 这个用处不大,很少有需要去取消执行的。
而最大的用处应该是第二点。

3、submit 方便 Exception 处理
意思就是如果你在你的 task 里会抛出 checked 或者 unchecked exception,
而你又希望外面的调用者能够感知这些 exception 并做出及时的处理,那么就需要用到 submit,通过捕获 Future.get () 抛出的异常。
Future.get () 在执行成功后返回的值是 null

怎么理解 Java 中的线程中断?

// 继承线程一定要重写run方法
    @Override
    public void run() {
    	// 在这里添加判断线程中断信号的方法 接收到线程中断 则不在打印 但是它不会清楚线程中断信号
        for (int i = 0; !Thread.currentThread().isInterrupted() && i < 200000; i++) {
            System.out.println("i:" + i);
        }
    }


// 主程序执行线程中断方法
thread.interrupt();

什么是幂等性?

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue ()” 函数就是一个幂等函数,无论多次执行,其结果都是一样的。

解决幂等性的方案有:

1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select 是天然的幂等操作;

2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回 0,删除的数据多条,返回结果多个) ;

3、唯一索引:防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户 ID 加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);

4, token 机制(防止重复提交)
原理上通过 session token 来实现的 (也可以通过 redis 来实现)。当客户端请求页面时,服务器会生成一个随机数 Token,并且将 Token 放置到 session 当中,然后将 Token 发给客户端(一般通过构造 hidden 表单)。
下次客户端提交请求时,Token 会随着表单一起提交到服务器端。

服务器端第一次验证相同过后,会将 session 中的 Token 值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的 Token 没变,但服务器端 session 中 Token 已经改变了。

5、悲观锁
获取数据的时候加锁获取。select * from table_xxx where id=‘xxx’ for update; 注意:id 字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;

6、乐观锁 —— 乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过 version 或者其他状态条件:

通过版本号实现 update table_xxx set name=#name#,version=version+1 where version=#version#;
通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0 要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
7、分布式锁

如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统 (redis 或 zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志 (用户 ID + 后缀等) 获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁 (分布式锁要第三方系统提供);

8、select + insert
并发不高的后台系统,或者一些任务 JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;

9、状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机 (状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助

10、对外提供接口的 api 如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source 来源,seq 序列号;source+seq 在数据库里面做唯一索引,防止多次付款 (并发时,只能处理一个请求) 。
重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源 source,一个是来源方序列号 seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。

标签:Java,收集器,对象,重点,面试,2023JAVA,线程,内存,方法
From: https://www.cnblogs.com/big-mouse/p/17093342.html

相关文章

  • 面试题: es6新增内容
    **1.letconst****2.symbol****3.解构赋值**答:解构赋值语法是一种Javascript表达式。通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量**4.模板字......
  • 2023年SQL大厂高频实战面试题(详细解析)
    大家好,我是宁一。已经连续四个周没有休息了,最近主业、副业都是忙碌的巅峰期,晚上11点下班回家,再写课写到凌晨两点。连续一个多月连轴转,每天最大的愿望,就是睡足觉。这一阶段终......
  • #yyds干货盘点# LeetCode程序员面试金典:汉诺塔问题
    题目:在经典汉诺塔问题中,有3根柱子及N个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的......
  • #yyds干货盘点# LeetCode面试题:寻找两个正序数组的中位数
    1.简述:给定两个大小分别为m和n的正序(从小到大)数组 nums1和 nums2。请你找出并返回这两个正序数组的中位数。算法的时间复杂度应该为O(log(m+n))。 示例1:输入:n......
  • 代码随想录算法训练营第四天 | 24. 两两交换链表中的节点、19.删除链表的倒数第N个节
    24.两两交换链表中的节点力扣题目链接: 19.删除链表的倒数第N个节点力扣题目链接: 面试题02.07.链表相交力扣题目链接: 142.环形链表II力扣题目链接: ......
  • # 代码随想录算法训练营Day4|24.两两交换链表中的节点 19.删除链表的倒数第N个节点 面
    24.两两交换链表中的节点题目链接:24.两两交换链表中的节点总体思路:两两交换链表中的节点使用虚拟头节点可以更方便地进行交换,这样头节点和普通节点可以以同一种方式进行......
  • 面试
    java基础如何实现跨平台--jvm翻译奇迹马编译生成字节码文件,通过java虚拟机变成机器码不同系统可以设置自己的袭击JDK=JRE(环境)+Java工具+编译器+调试器JRE=JVM......
  • RabbitMQ 面试题
    1基本的知识queue绑定exchange有三种模式fanout--exchange将消息发送到所有的queue。direct--exchange根据消息的routingkey,选择routingkey相同......
  • Redis 面试题
    1持久化1.1RDBfork一个子线程来将数据进行持久化,使用写时复制的技术,如果主线程要对数据进行修改,那么就复制一份,交给主线程修改,原来的那一份交给子线程来复制到RDB文......
  • MySQL 面试题
    1数据库基础知识1.1数据库三大范式第一范式:每一个列都不可拆分第二范式:在第一范式的基础上,非主键完全依赖于主键,而不能是依赖于主键的一部分。第三范式:在第二......