Home

JVM 类加载机制

当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制。JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

在开始聊之前,先给大家看一道面试题。

class Grandpa
{
    static
    {
        System.out.println("爷爷在静态代码块");
    }
}    
class Father extends Grandpa
{
    static
    {
        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father()
    {
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
    static
    {
        System.out.println("儿子在静态代码块");
    }

    public Son()
    {
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
    public static void main(String[] args)
    {
        System.out.println("爸爸的岁数:" + Son.factor);	//入口
    }
}

请写出最后的输出字符串。

正确答案是:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

我相信很多同学看到这个题目之后,表情是崩溃的,完全不知道从何入手。有的甚至遇到了几次,仍然无法找到正确的解答思路。

其实这种面试题考察的就是你对Java类加载机制的理解。 如果你对Java加载机制不理解,那么你是无法解答这道题目的。这篇文章,我将通过对Java类加载机制的讲解,让你掌握解答此类题目的方法。

加载

下面是对于加载过程最为官方的描述。

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:

  • JVM规范校验。 JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • 代码逻辑校验。 JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。

准备(重点)

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象。 Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的类型。 在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

public static int sector = 3;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

public static final int number = 3;

之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

看完了Java的类加载机智之后,是不是有点懵呢。不怕,我们先通过一个小例子来醒醒神。

public class Book {
    public static void main(String[] args)
    {
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }

    int price = 110;

    static
    {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;
}

思考一下上面这段代码输出什么?

给你5分钟思考,5分钟后交卷,哈哈。

怎么样,想好了吗,公布答案了。

书的静态代码块
Hello ShuYi.

怎么样,你答对了吗?是不是和你想得有点不一样呢。

下面我们来简单分析一下,首先根据上面说到的触发初始化的5种情况的第4种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类),我们会进行类的初始化。

那么类的初始化顺序到底是怎么样的呢?

重点来了!

重点来了!

重点来了!

在我们代码中,我们只知道有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法和 对象初始化方法 。

那么这两个方法是怎么来的呢?

  • 类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

上面的这个例子,其类初始化方法就是下面这段代码了:

static
{
  System.out.println("书的静态代码块");
}
static int amount = 112;
  • 对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

上面这个例子,其对象初始化方法就是下面这段代码了:

{
  System.out.println("书的普通代码块");
}
int price = 110;
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);

类初始化方法 和 对象初始化方法 之后,我们再来看这个例子,我们就不难得出上面的答案了。

但细心的朋友一定会发现,其实上面的这个例子其实没有执行对象初始化方法。

因为我们确实没有进行 Book 类对象的实例化。如果你在 main 方法中增加 new Book() 语句,你会发现对象的初始化方法执行了!

感兴趣的朋友可以自己动手试一下,我这里就不执行了。

通过了上面的理论和简单例子,我们下面进入更加复杂的实战分析吧!

实战分析

class Grandpa
{
    static
    {
        System.out.println("爷爷在静态代码块");
    }
}    
class Father extends Grandpa
{
    static
    {
        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father()
    {
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
    static
    {
        System.out.println("儿子在静态代码块");
    }

    public Son()
    {
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
    public static void main(String[] args)
    {
        System.out.println("爸爸的岁数:" + Son.factor);	//入口
    }
}

思考一下,上面的代码最后的输出结果是什么?

最终的输出结果是:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?

这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。 因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

对面上面的这个例子,我们可以从入口开始分析一路分析下去:

  • 首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor 类成员变量,但是 Son 类中并没有定义这个类成员变量。于是往父类去找,我们在 Father 类中找到了对应的类成员变量,于是触发了 Father 的初始化。
  • 但根据我们上面说到的初始化的 5 种情况中的第 3 种(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化 Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father 类。于是我们先初始化 Grandpa 类输出:「爷爷在静态代码块」,再初始化 Father 类输出:「爸爸在静态代码块」。
  • 最后,所有父类都初始化完成之后,Son 类才能调用父类的静态变量,从而输出:「爸爸的岁数:25」。

怎么样,是不是觉得豁然开朗呢。

我们再来看一下一个更复杂点的例子,看看输出结果是啥。

class Grandpa
{
    static
    {
        System.out.println("爷爷在静态代码块");
    }

    public Grandpa() {
        System.out.println("我是爷爷~");
    }
}
class Father extends Grandpa
{
    static
    {
        System.out.println("爸爸在静态代码块");
    }

    public Father()
    {
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
    static
    {
        System.out.println("儿子在静态代码块");
    }

    public Son()
    {
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
    public static void main(String[] args)
    {
        new Son(); 	//入口
    }
}

输出结果是:

爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~

怎么样,是不是觉得这道题和上面的有所不同呢。

让我们仔细来分析一下上面代码的执行流程:

  • 首先在入口这里我们实例化一个 Son 对象,因此会触发 Son 类的初始化,而 Son 类的初始化又会带动 Father 、Grandpa 类的初始化,从而执行对应类中的静态代码块。因此会输出:「爷爷在静态代码块」、「爸爸在静态代码块」、「儿子在静态代码块」。
  • 当 Son 类完成初始化之后,便会调用 Son 类的构造方法,而 Son 类构造方法的调用同样会带动 Father、Grandpa 类构造方法的调用,最后会输出:「我是爷爷」、「我是爸爸」、「我是儿子~」。

看完了两个例子之后,相信大家都胸有成足了吧。

下面给大家看一个特殊点的例子,有点难哦!

public class Book {
    public static void main(String[] args)
    {
        staticFunction();
    }

    static Book book = new Book();

    static
    {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    public static void staticFunction(){
        System.out.println("书的静态方法");
    }

    int price = 110;
    static int amount = 112;
}

上面这个例子的输出结果是:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法

下面我们一步步来分析一下代码的整个执行流程。

在上面两个例子中,因为 main 方法所在类并没有多余的代码,我们都直接忽略了 main 方法所在类的初始化。

但在这个例子中,main 方法所在类有许多代码,我们就并不能直接忽略了。

  • 当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
  • 当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。
  • JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。

对于 Book 类,其类构造方法()可以简单表示如下:

static Book book = new Book();
static
{
    System.out.println("书的静态代码块");
}
static int amount = 112;

于是首先执行static Book book = new Book();这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:

{
    System.out.println("书的普通代码块");
}
int price = 110;
Book()
{
    System.out.println("书的构造方法");
    System.out.println("price=" + price +", amount=" + amount);
}

于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。

当类实例化完成之后,JVM 继续进行类构造器的初始化:

static Book book = new Book();  //完成类实例化
static
{
    System.out.println("书的静态代码块");
}
static int amount = 112;

即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。

  • 到这里,类的初始化已经完成,JVM 执行 main 方法的内容。
public static void main(String[] args)
{
    staticFunction();
}

即输出:「书的静态方法」。

方法论

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。 在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。 当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。 JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。 JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

Read more

JVM 内存模型与垃圾回收机制

java内存模型

在C++中我们需要手动申请内存然后释放内存,否则就会出现对象已经不再使用内存却仍被占用的情况。在Java中JVM内置了垃圾回收的机制,帮助开发者承担对象的创建和释放的工作,极大的减轻了开发的负担。那是不是我们就不需要了解JVM了,显然在做一些优化或者深入研究应用性能的时候,JVM还是起了很关键的作用的。

Java内存模型结构分为线程共享区和线程私有区

  • 线程共享区: 堆、方法区
  • 线程私有区: 虚拟机栈、本地方法栈、程序 计数器

堆: 用于存放对象实例和数组 ,由于堆是用来存放对象实例,因此堆也是垃圾收集器管理的主要区域,故也称为GC堆。由于现在的垃圾收集器基本都采用分代收集算法,所以堆的内部结构只包含新生代和老年代。

方法区:

  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 方法区通常和永久区(Perm)关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作
  • 根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常

虚拟机栈:

  • 每个方法在执行的时候都会创建一个 栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
  • 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程
  • 局部变量表主要存放一些基本类型的变量和对象句柄,它们可以是方法参数,也可以是方法的局部变量

程序计数器:
为什么需要程序计数器?

  • 在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址
  • 程序计数器是线程私有的一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器
  • 如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址
  • 如果正在执行的是 Native 方法,则计数器的值为空
  • 程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域*

    ———————————JVM 内存模型———————————

img

JAVA中的垃圾回收机制

程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了 。而Java堆区和方法区则与之不同,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

垃圾定位分析:

有两种方式,一种是引用计数(但是无法解决循环引用的问题);另一种就是可达性分析。

判断对象可以回收的情况:
  • 显示的把某个引用置位NULL或者指向别的对象
  • 局部引用指向的对象
  • 弱引用关联的对象

如何确定某个对象是“垃圾”?

引用计数算法

首先来谈谈什么是引用:JAVA中当一个对象被创建的时候会给该对象分配一个变量,这个变量便称为对象的引用。当任何其它变量被赋值为这个对象的引用时,计数加1。但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。 此种处理方式是最快速的。但是有bug,相互引用的变量永远无法为0

public class ReferenceFindTest {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

可达性分析算法

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。例如如中EFG 对象在图中不可达 ,但是相互引用,他们便是GC处理的对象。

img

在Java语言中,可作为GC Roots的对象包括下面几种:   a) 虚拟机栈中引用的对象(栈帧中的本地变量表);   b) 方法区中类静态属性引用的对象;   c) 方法区中常量引用的对象;   d) 本地方法栈中JNI(Native方法)引用的对象。

分代收集算法

img

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的 核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域 。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

年轻代(Young Generation)的回收算法

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

年老代(Old Generation)的回收算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)的回收算法

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区,具体的回收可参见上文2.5节。

常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:

img

Serial收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(停止-复制算法)

新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(停止-复制算法)

并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

Parallel Old收集器(停止-复制算法)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

GC是什么时候触发的(面试最常见的问题之一)

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC ,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以 Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此 应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节 。有如下原因可能导致Full GC:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC之后Heap的各域分配策略动态变化;

Read more

阿里p6面试题

一面 (电话面试)

1、介绍自己比较熟悉的项目和项目中遇到的难点

2、Springbean生命周期

bean创建: new一个对象到容器–>属性注入–>是否实现了Aware类–>后置处理器,执行初始化前的方法–>初始化–>后置处理器,执行初始化完成后的方法–>完成bean创建

销毁: 执行@PostDestroy 注解的方法–>bean实现了DisposableBean,执行destroy方法–>执行配置文件中的destroy-method

3、谈谈依赖注入和面向切面

依赖注入: 通过发射的方式,把创建bean的权限交由spring来统一管理,可以避免硬编码造成的代码耦合

面向切面: aop是面向切面编程的思想,spring通过代理的方式,将面向切面编程定义成一个规范,通过代理模式,将两个或多个有关联的业务,在代码层面实现节藕

4、HashMap原理和扩容机制

hashmap的内部实现是数组和链表的结合,新建hashmap的时候会默认初始化数组长度为16,精准度为0.75;执行插入操作时,通过通过给key做hash处理,将得到的值和16求膜,将value插入与之对应的小标;当出现相同的下标,value将通过链表的形式链接起来,并且是将value插入到最顶端;

5、常用并发包下的类

接口: Callback,Future ,FutureService ,Executor,BlockingQueue

类: ConcurrentHashMap,ConcurrentListMap,ConcurrentListSet,CopyOnWriteArrayList, CopyOnWriteArraySet,ArrayBlockingQueue,FutureTask,ListedBlockingQueue,Executors,ThreadPoolExecutor

6、Redis持久化方式,为什么这么快?

自己总结:

  1. 内存数据库,一减少对磁盘读取的IO
  2. 非阻塞IO,IO多路复用
  3. 单线程模型,减少线程上下文切换和竞争

参考其他的总结:

一、 Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。

二、 再说一下IO,Redis使用的是__非阻塞IO,IO多路复用__,使用了__单线程来轮询描述符__,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切 换和竞争。

三、 Redis采用了__单线程的模型__,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。

四、另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储 ,再如,跳表,使用有序的数据结构加快读取的速度。

五、还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。

7、自己平时如何提升的,看书或者网站?

二面

1、Jvm类加载机制,分别每一步做了什么工作?

加载–>校验–>解析–>准备–>解析–>初始化–>使用–>卸载

加载: 将Class类加载到内存,接着在JVM的方法区创建一个对应的Class对象

校验: JVM代码规范娇艳,代码逻辑校验

准备: 分配内存并初始化,这里需要注意两个关键点,内存分配的对象以及初始化的类型。初始化的是static 修饰的类变量

解析: JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析

初始化: JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化

使用: JVM 开始从入口方法开始执行用户的程序代码

卸载: JVM 开始销毁创建的 Class 对象

2、Jvm内存模型,垃圾回收机制,如何确定被清除的对象?

共享内存区: 堆,方法区

私有内存区: 栈,计数器,本地方法区

堆: 存放对象和数组,也是GC处理的区域

方法区: 用于存放常量,静态变量,Class

栈: 保存栈帧,栈帧中包括:局部变量表(方法参数,也可以是方法的局部变量)、操作数栈、动态链接、方法出口

计数器: 当线程数大于CPU核数,线程之间就要根据时间片轮询抢夺CPU时间资源,计数器会记录线程的状态及上下文

本地方法区: 调用扩展方法,通常是通过 JNI 调用 C或C++

垃圾回收机制: 引用计数算法 可达性分析算法 分代收集算法

触发轻GC的条件:

当新对象生成,并且在Eden申请空间失败时,就会触发,通常就是 Eden 空间满的时候触发

触发重GC的条件:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC之后Heap的各域分配策略动态变化;

3、了解哪些垃圾回收器和区别?

垃圾回收器 说明 特性 JVM参数
单行垃圾回收器 为单线程环境设计,只使用一个单线程进行垃圾回收 通过持有应用程序所有的线程进行工作 -XX:+UseSerialGC
并行垃圾回收器 多线程垃圾回收 也会冻结所有的应用程序线程当执行垃圾回收的时候 默认使用,不需要单独配置
并发标记垃圾回收器 使用多线程扫描堆内存 标记需要清理的实例并且清理被标记过的实例 XX:+USeParNewGC
G1垃圾回收器 可以在回收内存之后对剩余的堆内存空间进行压缩 适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收 –XX:+UseG1GC

4、多线程相关,线程池的参数列表和拒绝策略

public ThreadPoolExecutor(int corePoolSize, // 核心线程数
                          int maximumPoolSize, // 最大线程数
                          long keepAliveTime, // 超时时间数
                          TimeUnit unit, // 超时时间单位
                          BlockingQueue<Runnable> workQueue, // 设置排队线程
                          ThreadFactory threadFactory, // 线程工厂
                          RejectedExecutionHandler handler // 拒绝策略 ) {
。。。。
}
  1. 直接丢弃(DiscardPolicy)

  2. 丢弃队列中最老的任务(DiscardOldestPolicy)。

  3. 抛异常(AbortPolicy)

  4. 将任务分给调用线程来执行(CallerRunsPolicy)。

5、Jvm如何分析出哪个对象上锁?

6、Mysql索引类型和区别,事务的隔离级别和事务原理

索引的概念:

  • 索引是特殊的文件,饱含着对所有数据表里所有记录的引用指针
  • 索引分为:聚簇索引、非聚簇索引,聚簇索引是按照数据存放的物理位置为顺序的,而非聚簇索引就不一样了;聚簇索引:能提高多行检索的速度,非聚簇索引:单行的检索很快。
  • 要注意的是,建立太多的索引将会影响更新和插入的速度,因为它需要同样更新每个索引文件。对于一个经常需要更新和插入的表格,就没有必要为一个很少使用的where字句单独建立索引了,对于比较小的表,排序的开销不会很大,也没有必要建立另外的索引。

索引的类型:

  1. 普通索引: 唯一任务是加快对数据的访问速度
  2. 唯一性索引: 与普通索引类似,不同的就是:索引列的值必须唯一
  3. 全文索引: 全文索引只能作用在 CHAR、VARCHAR、TEXT、类型的字段上。创建全文索引需要使用 FULLTEXT 参数进行约束
  4. 单列索引: 创建单列索引,即在数据表的单个字段上创建索引。创建该类型索引不需要引入约束参数,用户在建立时只需要指定单列字段名,即可创建单列索引
  5. 多列索引: 创建多列索引,即在数据表的多个字段上创建索引。与上述单列索引类似,创建该类型索引不需要引入约束参数。
  6. 空间索引: 只有 MyISAM 类型的表支持该类型 ‘ 空间索引 ’。而且,索引字段必须有非空约束

MySQL 事务隔离级别分为四个不同层次:

  1. 读未提交: 事务能够看到其他事务尚未提交的修改
  2. 读已提交: 事务能够看到的数据都是其他事务已经提交的修改,并不保证再次读取时能够获取同样的数据
  3. 可重复读(MySQL InnoDB 引擎的默认隔离级别)
  4. 串行化: 并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁

7、Spring scope 和设计模式

饿汉模式 懒汉模式 内部类 枚举

8、Sql优化

  1. sql 命令优化,比如 count(1) 比count(*) 效率高,可通过在sql命令前加 explain
  2. 添加索引
  3. 分库分表,读写分离
  4. 数据库分区

三面

1、fullgc的时候会导致接口的响应速度特别慢,该如何排查和解决?

  • Full GC次数过多
  • CPU过高
  • 不定期出现的接口耗时现象
  • 某个线程进入WAITING状态
  • 死锁

2、项目内存或者CPU占用率过高如何排查?

3、ConcurrentHashmap原理

  • 一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组

  • 对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑

  • 我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法

Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;//负载因子
            this.threshold = threshold;//阈值
            this.table = tab;//主干数组即HashEntry数组
        }

Hashmap原理详情

ConcurrentHashmap原理详情

4、数据库分库分表

  • 纵向:
    • 垂直分库:根据业务耦合性,将关联度低的不同表存储在不同的数据库
    • 垂直分表:基于数据库中的”列”进行,某个表字段较多
  • 横向:
    • 根据数值范围
    • 根据数值取模

5、MQ相关,为什么kafka这么快,什么是零拷贝?

  • kafka这么快

    • kaffa保存数据是按照顺序保存到磁盘,磁盘顺序读写速度 > 内存随机读写速度
    • Memory Mapped Files(内存映射文件)

    • 基于sendfile实现Zero Copy,减少拷贝次数
    • 批量压缩

6、小算法题

7、http和https协议区别,具体原理

HTTP与HTTPS有什么区别?

  HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

  HTTPS和HTTP的区别主要如下:

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

  2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

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

  4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

HTTPS的工作原理

  我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。

HTTP与HTTPS的区别-马海祥博客

 客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。

  (1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。

  (2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。

  (3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。

  (4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。

  (5)Web服务器利用自己的私钥解密出会话密钥。

  (6)Web服务器利用会话密钥加密与客户端之间的通信。

img

四面(Leader)

1、手画自己项目的架构图,并且针对架构和中间件提问

2、印象最深的一本技术书籍是什么?

Read more

腾讯(50道面试题)

线程池有什么作用?

说说几种常见的线程池及使用场景。

什么是反射机制?

说说反射机制的作用。

反射机制会不会有性能问题?

你怎么理解http协议?

说说http协议的工作流程。

http有哪些请求提交方式?

http中的200,302,403,404,500,503都代表什么状态?

http get和post有什么区别?

你怎么理解cookie和session,有哪些不同点?

什么是web缓存?有什么优点?

什么是https,说说https的工作原理?

什么是http代理服务器,有什么用?

什么是虚拟主机及实现原理?

什么是Java虚拟机,为什么要使用?

什么是分布式系统?

分布式系统你会考虑哪些方面?

讲一讲TCP协议的三次握手和四次挥手流程。

为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?为什么不能用两次握手进行连接?

为什么TCP TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

什么是DoS、DDoS、DRDoS攻击?如何防御?

描述一下Java异常层次结构。

什么是检查异常,不受检查异常,运行时异常?并分别举例说明。

finally块一定会执行吗?

正常情况下,当在try块或catch块中遇到return语句时,finally语句块在方法返回之前还是之后被执行?

try、catch、finally语句块的执行顺序。

Java虚拟机中,数据类型可以分为哪几类?

Java中,栈的大小通过什么参数来设置?

一个空Object对象的占多大空间?

讲一讲垃圾回收算法。

如何解决内存碎片的问题?

如何解决同时存在的对象创建和对象回收问题?

讲一讲内存分代及生命周期。

什么情况下触发垃圾回收?

如何选择合适的垃圾收集算法?

堆大小通过什么参数设置?

JVM有哪三种垃圾回收器?

吞吐量优先选择什么垃圾回收器?响应时间优先呢?

如何进行JVM调优?有哪些方法?

如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决?

讲讲CAP理念。

怎么理解强一致性、单调一致性和最终一致性?

分布式系统设计你会考虑哪些策略?

最常见的数据分布方式是什么?

谈一谈一致性哈希算法。

如何理解选主算法?

OSI有哪七层模型?TCP/IP是哪四层模型。

Read more

美团(30道面试题)

#

java虚拟机内存模型

内存溢出一般发生在哪个区?永久代会不会导致内存溢出?

动态加载类的框架了解哪些?

动态代理一般有哪几种实现方式?动态代理的应用场景有哪些?

栈会不会溢出?栈溢出一般抛什么异常?jvm在哪里设置栈的大小?设置的参数是什么?

jvm的垃圾回收机制?

java类加载机制?如何实现自定义类加载器?findClass与loadClass的区别?

String、StringBuffer、StringBuilder的区别?对应的使用场景?

如何实现不可变的类?

浅复制和深复制?怎样实现深复制?

HashMap、HashTable、ConcurrentHashMap的区别?

CAS是一种什么样的同步机制?

spring加载bean的流程?

java线程池?线程池构造函数的几个参数含义?keepAliveTime解释一下?

一个接口,要去调用另外5个接口,每一个接口都会返回数据给这个调用接口,调用接口要对数据进行合并并返回给上层。这样一种场景可能用到并发包下的哪些类?你会怎么去实现这样的业务场景?

CountDownLatch和CyclicBarrier的区别?

线程加锁有哪些方式?synchronized和lock的区别?

volatile关键字的作用?为什么使用AtomicLong而不使用Long?AtomicLong的底层是怎么实现的?

mysql的存储引擎有哪几种?

sql优化有哪些着手点?组合索引的最左前缀原则的含义?

springmvc处理请求的流程?

spring的事务怎么使用?事务回滚?自定义异常?

脏读?幻读?

tcp四次挥手的过程?TIME_WAIT为什么至少设置两倍的MSL时间?

了解哪些开源的中间件?缓存?消息?分布式框架?

用到过哪些设计模式?单例模式的实现?

数据库的脏读,幻读,不可重复读出现的原因原理,解决办法

数据库的隔离级别、MVCC

乐观锁、悲观锁、互斥锁、读写锁的原理实现与区别

一致性hash算法原理与应用

CAP原则

  • 一致性
  • 可用性
  • 有效性

CAS操作

  • 在多线程的时候 当使用外置锁的时候 会用到CAS操作

map想过面试题

  • 新建 Map map = new HashMap() 不占内存
  • 使用put的时候 才开始开辟内存
  • jdk7:数组+链表
  • jdk8:数组+链表+红黑树 链表的长度大于8 –>红黑树 <–链表长度小于6

Read more

京东(20道面试题)

Tomcat的基本架构是什么?

jvm调优,工作的是怎么用的,垃圾回收的策略有哪些?

Java 中的线程池是如何实现的?创建线程池的几个核心构造参数?

对比下 volatile 对比 Synchronized 的异同。

GC机制是什么?

垃圾回收器的基本原理是什么?垃圾回收器可以立马回收内存吗?怎样主动通知虚拟机进行垃圾回收?

类加载器双亲委派模型机制?

集合的种类以及区别,HashMap底层如何实现?HashMap中Hash冲突是怎么解决的?

Spring IOC 怎么管理 Bean 之间的依赖关系,怎么避免循环依赖?

Spring Bean 创建过程中的设计模式?

InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区别?

简述事务的特性?

什么是MySQL隔离级别?

如何理解BTree机制?

谈谈对慢查询分析?MySQL常用优化方式有哪些??

谈谈悲观锁和乐观锁及用SQL实现?

简述三次握手及四次挥手的过程?

对比B+树索引和 Hash索引?

MySQL的锁并发?

高并发场景(领红包)如何防止死锁,保证数据一致性?

集群与负载均衡的算法与实现?

简述分库与分表设计?

分库与分表带来的分布式困境与应对之策?

加锁机制是什么?

Read more

百度(24道面试题)

手写ArrayList

手写进制转换算法,求出一个数的二进制数1的个数

AVA基础 equals和==

多线程方式、threadlocal,各种锁,synchronized和lock

设计模式、spring类加载方式、实例保存在哪、aop ioc、反射机制

类加载器,双亲委派模型,热部署

jvm内存模型,内存结构、堆的分代算法、堆的分区、gc算法、gc过程。

tcp ip 七层模型 rest接口规范 get和post区别,长度,安全。

tcp ip的arp协议,两个同一网络的主机如何获得对方的mac地址。

负载均衡、高并发、高可用的架构

mysql的引擎区别

redis缓存,redis的集群部署,热备份,主从备份,主从数据库,hash映射找到知道指定节点。

了解云计算么,了解云容器docker么,容器和虚拟机的区别。

项目中的数据库备份,主从数据库、集群

数据库的索引原理,b+树原理,trie树引申,二叉查找树的原理

海量数据中查找一个单词,分布式计算map reduce ,或者用hsah映射筛选部分结果

java的抽象类和接口区别、java的hashmap,java的内存模型,分区,分代垃圾回收算法。实例、常量放在哪里。

int 4个字节,double 8个字节。

多线程中的wait和sleep区别,notify的作用

设计模式了解哪些,写一个观察者模式。实现两个接口,一个是主题一个是观察者,并写出对应方法。

写一个生产者消费者队列的方法,分别写两个类代表生产者和消费者,并且用队列模拟其生产消费。用while循环和wait notify可以实现,但我忘记在队列上加synchronize关键字,于是让我再写一题。写的是:输入一个字符串,输入第一个只出现一次的字符,写出来了。

tcp ip的四次挥手 子网掩码的作用, 子网掩码(subnet mask)又叫 网络掩码 、 地址掩码 、子网络遮罩,它是一种用来指明一个 IP地址 的哪些位标识的是 主机 所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成 网络地址 和 主机地址 两部分。

了解linux么,说一下linux的内核锁?没接触过。

有没有用过sed 使用shell脚本写一个将文本中的字符替换掉的脚本,大概说了一下用grep   替换。没再问linux的

Read more

阿里(60道面试题)

1.Java事件机制包括哪三个部分?分别介绍下。

2.使用线程池的原因?

3.线程池的作用有哪些?

4.几种常见的线程池及其各自的使用场景是怎样的。

5.线程池都有哪几种工作队列?

6.怎么理解无界队列和有界队列?

7.线程池中的几种重要的参数及流程说明。

8.什么是反射机制?

9.说说反射机制的作用。

10.反射机制会不会有性能问题?

11.你怎么理解http协议?

12.说说http协议的工作流程。

13.http有哪些请求提交方式?

14.http中的200,302,403,404,500,503都代表什么状态?

15.http get和post有什么区别?

16.你怎么理解cookie和session,有哪些不同点?

17.什么是web缓存?有什么优点?

18.什么是https,说说https的工作原理?

20.什么是虚拟主机及实现原理?

21.什么是Java虚拟机,为什么要使用?

22.说说Java虚拟机的生命周期及体系结构。

23.说一说Java内存区域。

24.什么是分布式系统?

25.分布式系统你会考虑哪些方面?

26.为什么说TCP/IP协议是不可靠的?

27.OSI有哪七层模型?TCP/IP是哪四层模型。

28.TCP协议的三次握手,四次挥手流程。

29.为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?为什么不能用两次握手进行连接?

32.描述一下Java异常层次结构。

33.什么是检查异常,不受检查异常,运行时异常?并分别举例说明。

34.finally块一定会执行吗?

35.正常情况下,当在try块或catch块中遇到return语句时,finally语句块在方法返回之前还是之后被执行?

36.try、catch、finally语句块的执行顺序。

37.Java虚拟机中,数据类型可以分为哪几类?

38.怎么理解栈、堆?堆中存什么?栈中存什么?

39.为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

40.在Java中,什么是是栈的起始点,同是也是程序的起始点?

43.Java中有没有指针的概念?

44.Java中,栈的大小通过什么参数来设置?

45.一个空Object对象的占多大空间?

46.对象引用类型分为哪几类?

47.讲一讲垃圾回收算法。

48.如何解决内存碎片的问题?

49.如何解决同时存在的对象创建和对象回收问题?

50.讲一讲内存分代及生命周期。

51.什么情况下触发垃圾回收?

52.如何选择合适的垃圾收集算法?

53.JVM有哪三种垃圾回收器?

54.JVM中最大堆大小有没有限制?

55.如何进行JVM调优?有哪些方法?

56.堆大小通过什么参数设置?

60.如何解决业务层的数据访问问题?

61.为了解决数据库服务器的负担,如何做数据库的分布?

62.什么是著名的拜占庭将军问题?

63.讲讲CAP理念。

64.怎么理解强一致性、单调一致性和最终一致性?

65.分布式系统设计你会考虑哪些策略?

Read more