原创

Java后台开发面试要点准备

Java

接口和抽象类的区别是什么?

  • 接口可以 extends 多个接口
  • 类可以实现很多个接口,但是只能继承一个抽象类
  • 抽象类中可以有一个或多个抽象方法,而接口中的方法必须都是抽象方法
  • 接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final
  • 抽象类和方法必须使用abstract关键声明为抽象,而接口中的方法默认被修饰为public abstract类型(默认为公开抽象的方法)

什么是值传递和引用传递

  • 值传递,意味着传递了对象的一个副本。因此,就算是改变了对象副本,也不会影响源对象的值
  • 引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象所做的改变会反映到所有的对象上

HashMap和Hashtable有什么区别?

  • HashMap允许键和值是null,而Hashtable不允许键或者值是null
  • Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境
  • HashMap提供了可供应用迭代的键的集合,因此,HashMap是快速失败的。另一方面,Hashtable提供了对键的列举

说出ArrayList,Vector, LinkedList的存储性能和特性
ArrayList:
底层数据结构是数组,查询快,增删慢
线程不安全,效率高
Vector:
底层数据结构是数组,查询快,增删慢
线程安全,效率低
LinkedList:
底层数据结构是链表,查询慢,增删快
线程不安全,效率高

Collection 和 Collections的区别

  • Collection是集合类的上级接口,继承与他的接口主要有Set 和List.
  • Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

&和&&的区别
&是位运算符,表示按位与运算,&&是逻辑运算符,表示逻辑与(and)

final和finally、finalize的区别
final:关键字,修饰类,不能派生子类,修饰变量或方法,不能被改变
finally:异常处理语句块,如果抛出一个异常,那么相匹配的catch语句就会执行,然后控制就会进入finally块
finalize:方法名,是在 Object 类中定义的,因此所有的类都继承了它,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等

try和finally中同时return会返回什么
返回finally语句块内容

sleep() 和 wait() 有什么区别?
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁,不释放所占有的资源。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,释放所占有的所有资源,进入等待此对象的等待锁定池,是不能自动唤醒的,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态

error和exception有什么区别?
Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止
Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常

Java中的异常处理机制的简单原理和应用
当JAVA 程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。违反语义规则包括2种情况。一种是JAVA类库内置的语义检查。例如数组下标越界,会引发IndexOutOfBoundsException;访问null的对象时会引发NullPointerException。另一种情况就是JAVA允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用throw关键字引发异常。所有的异常都是java.lang.Thowable的子类

多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是同步代码块和同步方法

synchronized和ReentrantLock的区别
synchronized:关键字,Java语言内置,不需要手动释放锁,在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
ReentrantLock:类,通过这个类可以实现同步访问,需要用户手动释放锁,并且必须在finally从句中释放,否则可能导致死锁,Lock可以让等待锁的线程响应中断,可以提高多个线程进行读操作的效率

throw和throws
throw语句用来明确地抛出一个”异常”,写在代码块中。
throws用来标明一个成员函数可能抛出的各种”异常”,写在方法后

垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行

什么是java序列化,如何实现java序列化?
序列化就是一种用来处理对象流的机制,把Java对象转换为二进制的数据流。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流

是否可以从一个static方法内部发出对非static方法的调用?
不可以,如果其中包含对象的method();不能保证对象初始化

使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的

final StringBuffer a=new StringBuffer(“immutable”); 
执行如下语句将报告编译期错误: 
a=new StringBuffer(“”); 
但是,执行如下语句则可以通过编译: 
a.append(” broken!”);

请说出作用域public,private,protected,以及不写时的区别

作用域 当前类 同一package 子类 其它package
public
protected ×
friendly × ×
private × × ×

StringBuffer与StringBuilder的区别
StringBuffer和StringBuilder类都表示内容可以被修改的字符串,StringBuilder是线程不安全的,运行效率高,如果一个字符串变量是在方法里面定义,这种情况只可能有一个线程访问它,不存在不安全的因素了,则用StringBuilder。如果要在类里面定义成员变量,并且这个类的实例对象会在多线程环境下使用,那么最好用StringBuffer

heap和stack有什么区别
java的内存分为两类,一类是栈内存,一类是堆内存。栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。
堆是与栈作用不同的内存,一般用于存放不放在当前方法栈中的那些数据,例如,使用new创建的对象都放在堆里,所以,它不会随方法的结束而消失。方法中的局部变量使用final修饰后,放在堆中,而不是栈中

深拷贝和浅拷贝
如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
总结:浅拷贝和深拷贝只是相对的,如果一个对象内部只有基本数据类型,那用 clone() 方法获取到的就是这个对象的深拷贝,而如果其内部还有引用数据类型,那用 clone() 方法就是一次浅拷贝的操作

CAS
Compare and Swap,比较并操作,指的是将预期值与当前变量的值比较(compare),如果相等则使用新值替换(swap)当前变量,否则不作操作
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

构造器能否被重写?
构造器是不能被继承的,因为每个类的类名都不相同,而构造器名称与类名相同,所以根本谈不上继承。
又由于构造器不能继承,所以就不能被重写。但是,在同一个类中,构造器是可以被重载的

查看死锁
Jconsole
Jstack:Jstack -l/-F 进程号

引用
强引用:只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象
软引用:只有在内存不足的时候JVM才会回收该对象。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中

public class Main {
    public static void main(String[] args) {

        SoftReference<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}

弱引用:当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象

public class Main {
    public static void main(String[] args) {

        WeakReference<String> sr = new WeakReference<String>(new String("hello"));

        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

虚引用:如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收

public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

产生死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用
  • 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不可抢占:进程已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系

避免死锁
加锁顺序,确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
加锁时限,尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
死锁检测,主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景

公平锁、非公平锁

  • 公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
  • 非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
    非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列
    Java中的ReentrantLock 默认的lock()方法采用的是非公平锁

Synchronized实现原理
显示同步,monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令
修饰同步方法:隐式同步,不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因.

锁的状态
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
无锁
偏向锁,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能
轻量级锁,倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
自旋锁,轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了
重量级锁

谈谈volatile的作用?实现原理以及使用场景
①volatile只能保证多线程三大特性中的可见性有序性。1)可见性:每个线程都有一个自己的本地内存,对于共享变量,线程每次读取和写入的都是共享变量在本地内存中的副本,然后在某个时间点将本地内存和主内存的值进行同步。而当修改volatile修饰的变量后,强制把对变量的修改同步到主内存。而其它线程在读取自己的本地内存中的值的时候,发现是valotile修饰的且已经被修改了,会把自己本地内存中的值置为无效,然后从主内存中读取。2)有序性:在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,这种重排序一般只能保证单线程下执行结果不被改变。当被volatile修饰的变量后,将会禁止重排序。②代码层面实现:通过内存屏障来实现的。所谓的内存屏障,是在某些指令中插入屏障指令。虚拟机读取到这些屏障指令时主动将本地内存的变量值刷新到内存,或直接从主内存中读取变量的值。通过屏障指令会禁止屏障前的操作命令和屏障后的命令进行重排序。系统层面实现:在多处理器下,保证各个处理器的缓存是一致的,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。③1:多线程间状态标识;2:单例模式中双重检查锁的写法;3:定期观察成员变量状态的方法

线程与进程
进程是系统进行资源调度和分配的一个基本单位,线程是进程的实体,是CPU调度和分派的基本单位
一个进程可以有多个线程,多个线程也可以并发执行

如何停止线程?

  • 主线程提供volatile boolean flag, 线程内while判断flag
  • 线程内while(!this.isInterrupted), 主线程里调用interrupt
  • f(this.isInterrupted) throw new InterruptedException()
    或return,主线程里调用interrupt
  • 将一个线程设置为守护线程后,当进程中没有非守护线程后,守护线程自动结束

多线程实现方式

  • extends Thread
  • implements Runnable
  • implements Callable, 重写call, 返回future (主线程可以用线程池submit)

Java自动装箱与拆箱

//boolean原生类型自动装箱成Boolean
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

//byte原生类型自动装箱成Byte
public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}

//byte原生类型自动装箱成Byte
public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    return new Short(s);
}

//char原生类型自动装箱成Character
public static Character valueOf(char c) {
    if (c <= 127) { // must cache
        return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

//int原生类型自动装箱成Integer
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

//int原生类型自动装箱成Long
public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

//double原生类型自动装箱成Double
public static Double valueOf(double d) {
    return new Double(d);
}

//float原生类型自动装箱成Float
public static Float valueOf(float f) {
    return new Float(f);
}

谈谈如何通过反射创建对象

  1. 通过Class字节码对象newInstance();(默认通过无参构造创建)
  2. 通过获取构造器getConstructor(Class<?>..parameterTypes);(通过有参的构造器,参数可以指定具体类型和多个数量)

ArrayList是否会越界?
会,在多线程下

为什么集合类没有实现Cloneable和Serializable接口?
克隆(cloning)或者序列化(serialization)的语义和含义是跟具体的实现相关的。因此应该由集合类的具体实现类来决定如何被克隆或者序列化

Iterator和ListIterator的区别是什么?

  • Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List
  • Iterator对集合只能是顺序向后遍历,ListIterator既可以向前也可以向后
  • ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等

快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
快速失败(fail-fast)
在使用迭代器对集合对象进行遍历的时候,如果A线程对集合进行遍历,正好B线程对集合进行修改(增加、删除、修改)则A线程会抛出ConcurrentModificationException异常
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历
安全失败(fail-safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception,例如CopyOnWriteArrayList

线程创建有很大开销,怎么优化?
使用线程池

Java中有几种线程池?
java里面的线程池的顶级接口是Executor,Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池是ExecutorService
newCachedThreadPool 缓存型线程池,在核心线程达到最大值(Interger. MAX_VALUE)之前,有任务进来就会创建新的核心线程,并加入核心线程池,即时有空闲的线程,也不会复用
newFixedThreadPool 建立一个线程数量固定的线程池,规定的最大线程数量,超过这个数量之后进来的任务,会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则
newScheduledThreadPool 计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池
newSingleThreadExecutor 建立一个只有一个线程的线程池,如果有超过一个任务进来,只有一个可以执行,其余的都会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则

概括的解释下线程的几种可用状态
新建,新创建了一个线程
就绪,调用start()方法,位于线程池,等待cpu调用
运行,获取到cpu时间片,执行代码
阻塞,线程让出cpu资源,暂时停止运行
死亡,run方法执行结束、或异常退出,结束生命周期

讲一下非公平锁和公平锁在reetrantlock里的实现
非公平锁: 当线程争夺锁的过程中,会先进行一次CAS尝试获取锁,若失败,则进入acquire(1)函数,进行一次tryAcquire再次尝试获取锁,若再次失败,那么就通过addWaiter将当前线程封装成node结点加入到Sync队列,这时候该线程只能乖乖等前面的线程执行完再轮到自己了。
公平锁: 当线程在获取锁的时候,会先判断Sync队列中是否有在等待获取资源的线程。若没有,则尝试获取锁,若有,那么就那么就通过addWaiter将当前线程封装成node结点加入到Sync队列中

反射的实现与作用
通过获取类的字节码,将字节码加载到内存中,在程序运行时生成一个该类的实例
作用:得到一个对象所属的类,运行时创建一个类,获取一个类的所有成员变量与方法,在运行时调用对象的方法

给我一个你最常见到的runtime exception
NullPointerExceptio 空指针异常
SecurityException 安全异常
IndexOutOfBoundsException 下标越界异常
ClassNotFoundException 类没找到时,抛出该异常
ClassCastException 类型强制转换异常
IOException 输入输出异常

HashMap

原理

  • HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null
  • HashSet 本身就是在 HashMap 的基础上实现的
  • 若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子0.75
  • 调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为bucketIndex.如果两个对象的hashCode不同,那么equals一定为false;否则,如果其hashCode相同,equals也不一定为 true。所以,理论上,hashCode可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals()来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到
  • jdk1.8中,首先,对key进行hash运算,为空,hash值为0,如果table[0]没有数据,直接插入,如果有数据,则进行数据的更新;若不为空,则先计算key的hash值,然后和table.length -1 & 运算找到在数组中的索引位置,如果table数组在该位置处有元素,则查找是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链尾。此外,若table在该处没有元素,则直接保存

hash()和indexFor()

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);  // 作用等价于取模运算,但这种方式效率更高
}
  • hash() 方法用于对Key的hashCode进行重新计算,而 indexFor()方法用于生成这个Entry对象的插入位置。当计算出来的hash值与hashMap的(length-1)做了&运算后,会得到位于区间[0,length-1]的一个值。特别地,这个值分布的越均匀,HashMap 的空间利用率也就越高,存取效率也就越好,保证元素均匀分布到table的每个桶中以便充分利用空间
  • hash():使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀
  • indexFor():保证元素均匀分布到table的每个桶中; 当length为2的n次方时,h&(length -1)就相当于对length取模,而且速度比直接取模要快得多,这是HashMap在速度上的一个优化

扩容resize()和重哈希transfer()

  • 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置相同
  • 如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
  • 为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)
  • 重哈希的主要是一个重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程

HashMap的底层数组长度为何总是2的n次方

  • 当底层数组的length为2的n次方时, h&(length - 1) 就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化
  • 不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快

Hashtable

Hashtable的默认容量(数组大小)为11,默认的负载因子为0.75

put

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
    }
  • hashtable之所以是线程安全的,原因为在put和get方法上使用synchronized关键字进行修饰
  • 限制了value不能为null
  • 由于直接使用key.hashcode(),而没有向hashmap一样先判断key是否为null,所以key为null时,调用key.hashcode()会出错,所以hashtable中key不能为null
  • 遍历table[index]所连接的链表,查找是否已经存在key与需要插入的key值相同的节点,如果存在则直接更新value,并返回旧的value
  • 如果table[index]所连接的链表上不存在相同的key,则通过addEntry()方法将新节点加载链表的开头

Hashtable和hashmap的区别

  • hashmap中key和value均可以为null,但是hashtable中key和value均不能为null
  • hashmap采用的是数组(桶位)+链表+红黑树结构实现,而hashtable中采用的是数组(桶位)+链表实现
  • hashmap中出现hash冲突时,如果链表节点数小于8时是将新元素加入到链表的末尾,而hashtable中出现hash冲突时采用的是将新元素加入到链表的开头
  • hashmap中数组容量的大小要求是2的n次方,如果初始化时不符合要求会进行调整,而hashtable中数组容量的大小可以为任意正整数
  • hashmap中的寻址方法采用的是位运算按位与,而hashtable中寻址方式采用的是求余数
  • hashmap不是线程安全的,而hashtable是线程安全的,hashtable中的get和put方法均采用了synchronized关键字进行了方法同步
  • hashmap中默认容量的大小是16,而hashtable中默认数组容量是11

ConcurrentHashMap

put(), get()

  • 不允许key值为null,也不允许value值为null
  • HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作

this, super

  • this()函数主要应用于同一类中从某个构造函数调用另一个重载版的构造函数。this()只能用在构造函数中,并且也只能在第一行。所以在同一个构造函数中this()和super()不能同时出现
  • super()函数在子类构造函数中调用父类的构造函数时使用,而且必须要在构造函数的第一行

内存泄露问题

  • 静态集合类: 如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放
  • 各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭
  • 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露

ThreadLocal内存泄露?
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露

ThreadLocal内存泄漏根源
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用

ThreadLocal内存泄露解决办法
每次使用完ThreadLocal,都调用它的remove()方法,清除数据

线程池调优?

  • 设置最大线程数,防止线程资源耗尽
  • 使用有界队列,从而增加系统的稳定性和预警能力(饱和策略)
  • 根据任务的性质设置线程池大小:CPU密集型任务(CPU个数个线程),IO密集型任务(CPU个数两倍的线程),混合型任务(拆分)

数据库

MySQL

主键,唯一索引区别

  • 主键一定会创建一个唯一索引,但是有唯一索引的列不一定是主键
  • 主键不允许为空值,唯一索引列允许空值;
  • 一个表只能有一个主键,但是可以有多个唯一索引;
  • 主键可以被其他表引用为外键,唯一索引列不可以;
  • 主键是一种约束,而唯一索引是一种索引,是表的冗余数据结构,两者有本质的差别

索引失效

  • 字符串不加单引号
  • 在 where 子句中对字段进行 null 值判断
  • like查询以%开头
  • 对于多列索引,不是使用的第一部分,则不会使用索引
  • where 子句中使用 or 来连接条件
  • 使用mysql内部函数导致索引失效
  • where 子句中的“=”左边进行函数、算术运算或其他表达式运算

索引种类
聚集索引:主键索引
非聚集索引:普通索引,唯一索引,组合索引,全文索引

Innodb和MyISAM
Innodb
支持事务
支持行级锁
默认主键索引为聚集索引
MyISAM
读多写少
表级锁
不支持外键
myisam在磁盘存储上有三个文件,每个文件名以表名开头,扩展名指出文件类型,.frm 用于存储表的定义,.MYD 用于存放数据,.MYI 用于存放表索引

共享锁,排他锁

  • InnoDB普通 select 语句默认不加锁(快照读,MYISAM会加锁),而CUD操作默认加排他锁
  • MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC
  • 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读.
  • 在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本(有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录
  • SELECT … LOCK IN SHARE MODE :共享锁(S锁, share locks)。其他事务可以读取数据,但不能对该数据进行修改,直到所有的共享锁被释放
  • SELECT … FOR UPDATE:排他锁(X锁, exclusive locks)。如果事务对数据加上排他锁之后,则其他事务不能对该数据加任何的锁。获取排他锁的事务既能读取数据,也能修改数据。
  • InnoDB默认隔离级别 可重复读(Repeated Read)
  • 查询字段未加索引(主键索引、普通索引等)时,使用表锁
  • InnoDB行级锁基于索引实现
  • 索引数据重复率太高会导致全表扫描:当表中索引字段数据重复率太高,则MySQL可能会忽略索引,进行全表扫描,此时使用表锁。可使用 force index 强制使用索引

悲观锁、乐观锁
悲观锁:
每次在拿数据的时候都会上锁
大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性
乐观锁:
假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做
使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号
当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据

隔离级别

  • Read Uncommitted(读取未提交内容): 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取事务未提交的数据,也被称之为脏读(Dirty Read)
  • Read Committed(读取提交内容): 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果
  • Repeatable Read(可重复读): 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题
  • Serializable(可串行化): 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争

innoDB和MyISAM的区别?

  • InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务
  • InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败
  • InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
  • InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快
  • Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高,但是innodb可以使用sphinx插件支持全文索引,并且效果更好

三范式
第一范式,一个关系模式中所有属性都是不可分的
第二范式,满足第一范式,且非主属性完全依赖主键
第三范式,满足第二范式,属性不依赖于其它非主属性

ACID
原子性,一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
一致性,在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态
隔离性,一个事务所做的修改在最终提交前,对其他事务是不可见的
持久性,一旦事物提交,则其所做的修改就会永远保存在数据库中

主从复制

主从不同步解决办法

  1. 若数据不完全统一,可跳过错误继续同步
  2. 重新做主从,完全同步
    1.先进入主库,进行锁表,防止数据写入
    2.进行数据备份
    3.查看master 状态
    4.把mysql备份文件传到从库机器,进行数据恢复
    5.停止从库的状态
    6.然后到从库执行mysql命令,导入数据备份
    7.设置从库同步,注意该处的同步点,就是主库show master status信息里的| File| Position两项
    8.重新开启从同步
    9.查看同步状态

Redis

Redis的数据结构
String, Hash, List, Set, ZSet

Hash底层结构
redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable

Redis缓存怎么运行的?

  • 使用ANSIC编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库
  • 主从复制
  • 哨兵模式

持久化

  • RDB持久化
    是指把当前进程数据生成快照保存到硬盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。触发RDB持久化过程分为手动触发自动触发
    优点:代表Redis在某一个时间点上的数据快照,适合全量同步;恢复数据快于AOF
    缺点:无法做到实时持久化/秒级持久化
    牺牲性能,缓存一直
  • AOF持久化
    以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录。工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)
    优点:实时同步
    缺点:效率慢,文件大

讲一下redis的主从复制怎么做的?
全量同步:一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份
增量同步: Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令

Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步

redis为什么读写速率快性能好?
1. 存内存操作。数据存在内存中
2. 单线程操作。避免了频繁的上下文切换
3. 采用非阻塞IO多路复用机制。内部实现采用epoll,采用了epoll+自己实现的简单的事件框架(事件分离器),内部采用非阻塞的执行方式,吞吐能力比较大。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间
4. 数据结构简单。使用特殊数据结构,对数据存储进行了优化

redis为什么是单线程?
为了不让cpu成为redis的瓶颈,而单线程方便管理、容易实现,且不用锁,所以使用单线程,利用队列技术将并发访问变为串行访问

缓存的优点?
减少数据库读操作,降低数据库压力;数据从内存中读取,加快响应速度

计网

HTTP

说一说四种会话跟踪技术
URL重写
隐藏表单域
cookie
session

浏览器输入网址后发生了什么?
第一步:在浏览器中输入url后,应用层会使用DNS解析域名,如果本地存有对应的IP,则使用;如果没有,则会向上级DNS服务器请求帮助,直至获得IP。域名解析详细过程会在下文讲到。
第二步:应用层将请求的信息装载入HTTP请求报文,信息包含了请求的方法(GET / POST)、目标url、遵循的协议(http / https / ftp…)等,然后应用层将发起HTTP请求。
第三步:传输层接收到应用层传递下来的数据,并分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。通过三次握手和目标端口建立安全通信。
第四步:网络层接收传输层传递的数据,根据IP通过ARP协议获得目标计算机物理地址—MAC。当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标。
第五步:找到目标MAC地址以后,就将数据发送到数据链路层,这时开始真正的传输请求信息,传输完成以后请求结束。
第六步:服务器接收数据后,从下到上层层将数据解包,直到应用层
第七步: 服务器接收到客户端发送的HTTP请求后,查找客户端请求的资源,将数据装载入响应报文并返回,响应报文中包括一个重要的信息——状态码,如200,404,500

常见状态码

状态码 英文描述 中文描述
100 继续
101 协议升级
200 OK 请求成功
301 永久性转移
302 临时移动
304 请求资源未修改
307 临时重定向
400 Bad Request 客户端请求语法错误
401 要求认证
403 拒绝请求
404 没有找到客户端请求资源
500 服务器内部错误
502 错误网关,代理服务器连接不到服务器
503 服务不可用

sessionStorage 、localStorage 和 cookie 之间的区别
共同点:都是保存在浏览器端,且同源的
区别:

  • HTML5 提供
  • cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递;cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识
  • 而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  • 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
  • 作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。Web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者。Web Storage 的 api 接口使用更方便

Cookie,Session区别

  • cookie数据存放在客户的浏览器上,session数据放在服务器上
  • cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session
  • session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用COOKIE

TCP三次握手

第一次:客户端发送数据包,并将同步信号ACK置为1,表明建立连接请求,序列号为x
第二次:服务端接受到请求,发送数据包,并将同步信号SYN和确认信ACK号置为1,序列号为y,确认号为x+1
第三次:客户端收到请求,发送确认,将确认信号ACK置为1,确认号为y+1

为什么是三次握手不是两次握手
防止出现请求超时导致脏连接。如果是两次,一个超时的连接请求到达服务器,服务器会以为是客户端创建的连接请求,然后同意创建连接,而客户端不是等待确认状态,丢弃服务器确认数据,导致服务器单方面创建连接。如果是三次,服务器没有收到客户端的确认信息,最终超时导致连接创建失败,因此不会出现脏连接

为什么是四次挥手
TCP是全双工通信,服务器和客户端都可以发送和接受数据,客户端告诉服务器断开连接,等待服务器确认,两次握手,服务器处理完数据后,向客户端发送断开连接信号,等待客户端的确认,四次握手

GET、POST区别

  • get参数通过url传递,post放在request body中
  • get请求在url中传递的参数是有长度限制的,最多只能有1024字节,而post没有
  • GET产生一个TCP数据包;POST产生两个TCP数据包
  • GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同

TCP、UDP区别

  • TCP是面向连接的,UDP是无连接的
  • TCP是可靠的,UDP是不可靠的
  • TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多的通信模式
  • TCP是面向字节流的,UDP是面向报文的
  • TCP有拥塞控制机制;UDP没有拥塞控制,适合媒体通信
  • TCP首部开销(20个字节)比UDP的首部开销(8个字节)要大

TCP为什么可靠

  • 确认和重传机制:确认丢失、确认迟到、超时重传(停止等待协议)
  • 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段
  • 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
  • 拥塞控制: 当网络拥塞时,减少数据的发送
  • 有序:未按序到达的数据确认会重新发送

forward和redirect区别
请求转发(Forward),客户端器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,转发页面和转发到的页面可以共享request里面的数据
请求重定向(Redirect)实际是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的,状态码302

OSI五层
应用层-HTTP
传输层-TCP/UDP
网络层-IP
数据链路层
物理层

HTTP2

二进制传送。使用的是二进制传送,二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示
多路复用。有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求
头部压缩。通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID就可以知道表头的值了
服务器推送。在客户端未经请求许可的情况下,主动向客户端推送内容

设计模式

创建型模式(5种)
1. 单例模式
懒汉式

public class Singleton {

    private static Singleton instance; 

    //构造函数私有
    private Singleton (){
    }  

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

饿汉式

public class HungrySingleton {

    private static final HungrySingleton mHungrySingleton = new HungrySingleton();

    private HungrySingleton() {
        System.out.println("Singleton is create");
    }

    public static HungrySingleton getHungrySingleton() {
        return mHungrySingleton;
    }
}

DCL

public class Singleton {  

    private volatile static Singleton singleton;  

    private Singleton (){
    }  

    public static Singleton getSingleton() {  

        if (singleton == null) {  
            synchronized (Singleton.class) {  
            if (singleton == null) {  
                singleton = new Singleton();  
            }  
          }  
        }  
        return singleton;  
    }  
}
  • 第一个if (instance == null),只有instance为null的时候,才进入synchronized 第二个if
    (instance == null),是为了防止可能出现多个实例的情况
  • volatile: 主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
      1. 给 singleton 分配内存
      2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
      3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错

静态内部类

public class Singleton {  

    private Singleton (){
    }  

    public static final Singleton getInstance() {  
          return SingletonHolder.INSTANCE;  
    }  

    private static class SingletonHolder {  
         private static final Singleton INSTANCE = new Singleton();  
    }
}

枚举类

public enum  EnumSingleton {

    //定义一个枚举的元素,它就是Singleton的一个实例
    INSTANCE;

    public void doSomething() {
    }
}

2. 工厂方法模式
3. 抽象工厂模式
4. 建造者模式
5. 原型模式

结构型模式(7种)
1. 适配器模式
2. 桥接模式
3. 装饰器模式
Java IO流的设计
4. 组合模式
5. 外观模式
6. 享元模式
7. 代理模式

行为型模式 ( 11种 )
1. 模板方法模式
2. 命令模式
3. 迭代器模式
4. 观察者模式
5. 中介者模式
6. 备忘录模式
7. 解释器模式
8. 状态模式
9. 策略模式
10. 责任链模式
11. 访问者模式

Spring

Spring 框架中都用到了哪些设计模式?

  • 代理模式—在AOP和remoting中被用的比较多
  • 单例模式—在spring配置文件中定义的bean默认为单例模式
  • 模板方法—用来解决代码重复的问题。eg:RestTemplate, JmsTemplate, JpaTemplate
  • 前端控制器—Spring提供了DispatcherServlet来对请求进行分发
  • 视图帮助(View Helper )—Spring提供了一系列的JSP标签,高效宏来辅助将分散的代码整合在视图里
  • 依赖注入—贯穿于BeanFactory / ApplicationContext接口的核心理念
  • 工厂模式—BeanFactory用来创建对象的实例

ApplicationContext Bean生命周期
1. 首先容器启动后,会对scope为singleton且非懒加载的bean进行实例化,
2. 按照Bean定义信息配置信息,注入所有的属性,
3. 如果Bean实现了BeanNameAware接口,会回调该接口的setBeanName()方法,传入该Bean的id,此时该Bean就获得了自己在配置文件中的id,
4. 如果Bean实现了BeanFactoryAware接口,会回调该接口的setBeanFactory()方法,传入该Bean的BeanFactory,这样该Bean就获得了自己所在的BeanFactory,
5. 如果Bean实现了ApplicationContextAware接口,会回调该接口的setApplicationContext()方法,传入该Bean的ApplicationContext,这样该Bean就获得了自己所在的ApplicationContext,
6. 如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation()方法,
7. 如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法,
8. 如果Bean配置了init-method方法,则会执行init-method配置的方法,
9. 如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessAfterInitialization()方法,
10. 经过流程9之后,就可以正式使用该Bean了,对于scope为singleton的Bean,Spring的ioc容器中会缓存一份该bean的实例,而对于scope为prototype的Bean,每次被调用都会new一个新的对象,期生命周期就交给调用方管理了,不再是Spring容器进行管理了
11. 容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy()方法,
12. 如果Bean配置了destroy-method方法,则会执行destroy-method配置的方法,至此,整个Bean的生命周期结束

Spring的BeanFactory和ApplicationContext的区别?
ApplicationContext是实现类,继承ListableBeanFactory(继承BeanFactory),功能更多
ApplicationContext默认立即加载,BeanFactory延迟加载
beanFactory主要是面对与 spring 框架的基础设施,面对 spring 自己。而 Applicationcontex 主要面对与 spring 使用的开发者

Spring IOC 怎么注入类,怎么实例化对象
实例化

  1. Spring IoC容器需要根据Bean定义里的配置元数据使用反射机制来创建Bean
  2. 使用构造器实例化Bean 有参/无参;使用静态工厂实例化Bean;使用实例工厂实例化Bean
  3. 使用@Autowire注解注入的时机则是容器刚启动的时候就开始注入;注入之前要先初始化bean;ApplicationContext 的初始化和BeanFactory 有一个重大的区别:BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean 时才实例化目标Bean;而ApplicationContext 则在初始化应用上下文时就实例化所有单实例的Bean

谈谈Spring MVC的工作原理是怎样的?

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet接收到客户端请求,找到对应HandlerMapping,根据映射规则,找到对应的处理器(Handler)
  3. 调用相应处理器中的处理方法,处理该请求后,会返回一个ModelAndView
  4. DispatcherServlet根据得到的ModelAndView中的视图对象,找到一个合适的ViewResolver(视图解析器),根据视图解析器的配置,DispatcherServlet将要显示的数据传给对应的视图,最后显示给用户

Spring注入
接口注入、setter注入、构造器注入

Spring bean 范围
singleton 单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例
prototype IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例
request 该属性仅对HTTP请求产生作用,每次HTTP请求都将产生一个新实例。只有在Web应用中使用Spring时,该作用域才有效
session 同一个Session共享一个Bean实例。不同Session使用不同的实例
global session 所有的Session共享一个Bean实例

谈谈Spring中自动装配的方式有哪些?
no:不进行自动装配,手动设置Bean的依赖关系
byName:根据Bean的名字进行自动装配
byType:根据Bean的类型进行自动装配
constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误
autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配【在Spring3.0以后的版本被废弃,已经不再合法了】

aop的应用场景?
日志
数据源切换
事务

AOP的原理是什么?
AspectJ:静态代理,即用一种AspectJ支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好
Spring AOP:动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持

JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。需要获得被代理目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用InvokeHandler方法来处理

你如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?

  1. 连接点(Joinpoint):程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点
  2. 切点(Pointcut):将会被增强的连接点,目标类中被增强的方法
  3. 增强(Advice):增强是织入到目标类连接点上的一段程序代码。增强的内容通常以方法的形式体现的
  4. 引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类
  5. 织入(Weaving):生成切面并创建代理对象的过程,将通知和切点结合,并创建代理对象,AOP有三种织入方式:①编译期织入:需要特殊的Java编译期(例如AspectJ的ajc);②装载期织入:要求使用特殊的类加载器,在装载类的时候对类进行增强;③运行时织入:在运行时为目标类生成代理实现增强。Spring采用了动态代理的方式实现了运行时织入,而AspectJ采用了编译期织入和装载期织入的方式
  6. 切面(Aspect):切面是由切点和增强组成的,一个通知对应一个切入点就形成一条线,多个通知对应多个切入点形成多条线,多条线形成了一个面,我们称为切面

Spring支持的事务管理类型有哪些?你在项目中使用哪种方式?
编程式事务:在代码中显式调用开启事务、提交事务、回滚事务的相关方法,基于 <tx><aop> 命名空间的声明式事务管理是目前推荐的方式,其最大特点是与 Spring AOP 结合紧密,可以充分利用切点表达式的强大支持,使得管理事务更加灵活
声明式事务:底层是建立在 AOP 的基础之上。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务,基于 @Transactional 的方式将声明式事务管理简化到了极致

Servlet的生命周期

  1. 在web服务器启动时被加载并实例化,容器运行其init方法初始化,请求到达时运行其service方法
  2. service运行请求对应的doXXX(doGet、doPost)方法
  3. 服务器销毁实例,运行其destory方法

MyBatis

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

  • #{}是预编译处理,提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译,mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值,最后注入进去是带引号的,${}是字符串替换,mybatis在处理${}时,就是把${}替换成变量的值
  • 使用#{}可以有效的防止SQL注入,提高系统安全性

当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
1.通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致

<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”> 
       select order_id id, order_no orderno ,order_price price form orders where order_id=#{id}; 
</select>

2.通过<resultMap>来映射字段名和实体类属性名的一一对应的关系

<select id="getOrder" parameterType="int" resultMap="orderresultmap">
        select * from orders where order_id=#{id}
    </select>
   <resultMap type=”me.gacl.domain.order” id=”orderresultmap”> 
        <!–用id属性来映射主键字段–> 
        <id property=”id” column=”order_id”> 
        <!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–> 
        <result property = “orderno” column =”order_no”/> 
        <result property=”price” column=”order_price” /> 
    </reslutMap>

如何获取自动生成的(主)键值?
通过LAST_INSERT_ID()获取刚插入记录的自增主键值,在insert语句执行后,执行select LAST_INSERT_ID()就可以获取自增主键

<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
        <selectKey keyProperty="id" order="AFTER" resultType="int">
            select LAST_INSERT_ID()
        </selectKey>
        INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
</insert>

在mapper中如何传递多个参数?
1.使用占位符的思想
#{0}#{1}方式

//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。

<select id="selectUser"resultMap="BaseResultMap">  
    select *  fromuser_user_t   whereuser_name = #{0} anduser_area=#{1}  
</select>

@param注解方式

public interface usermapper { 
         user selectuser(@param(“username”) string username, 
         @param(“hashedpassword”) string hashedpassword); 
}

<select id=”selectuser” resulttype=”user”> 
     select id, username, hashedpassword 
     from some_table 
     where username = #{username} 
     and hashedpassword = #{hashedpassword} 
</select>

2.使用Map集合作为参数来装载

<select id=”selectuser” resulttype=”user”> 
     select id, username, hashedpassword 
     from some_table 
     where username = #{username} 
     and hashedpassword = #{hashedpassword} 
</select>

<!--分页查询-->
<select id="pagination" parameterType="map" resultMap="studentMap">

    /*根据key自动找到对应Map集合的value*/
    select * from students limit #{start},#{end};

</select>

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

  • Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能
  • Mybatis提供了9种动态sql标签:trim、where、set、foreach、if、choose、when、otherwise、bind
  • 其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能

Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?

  • 如果配置了namespace那么当然是可以重复的,因为我们的Statement实际上就是namespace+id
  • 如果没有配置namespace的话,那么相同的id就会导致覆盖了

为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

  • Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的
  • 而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具

通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

  • Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数
  • Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement
 com.mybatis3.mappers.StudentDao.findStudentById
可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。
  • Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略
  • Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回

Mybatis比IBatis比较大的几个改进是什么

  • 有接口绑定,包括注解绑定sql和xml绑定Sql
  • 动态sql由原来的节点配置变成OGNL表达式
  • 在一对一,一对多的时候引进了association,在一对多的时候引入了collection节点,不过都是在resultMap里面配置

接口绑定有几种实现方式,分别是怎么实现的?

  • 一种是通过注解绑定,就是在接口的方法上面加上@Select@Update等注解里面包含Sql语句来绑定
  • 另外一种就是通过xml里面写SQL来绑定,在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名

Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数
select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10

简述Mybatis的插件运行原理,以及如何编写一个插件
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法
实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置编写的插件

Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的

Mybatis都有哪些Executor执行器?它们之间的区别是什么?
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String,Statement>内,供下一次使用。简言之,就是重复使用Statement对象
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内

MyBatis与Hibernate有哪些不同?
Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句,不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句,并将java对象和sql语句映射生成最终执行的sql,最后将sql执行的结果再映射生成java对象。

Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大

Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的缺点是学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。

Security

认证流程
登录时,有一个过滤器UsernamePasswordAuthenticationFilter对登录请求进行拦截,在拦截器中实例化一个UsernamePasswordAuthenticationToken对象,再获取AuthenticationManager调用authenticate方法查找支持该对象认证方式的AuthenticationProvider,调用provider的认证方法,会调用自己实现的UserdetailService中的方法查找用户进行认证,认证失败,抛出异常,认证成功,返回封装有用户信息的authentication对象,将其存入SecurityContext

JVM

JVM内存划分

方法区:

  • 方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 运行时常量池:是方法区的一部分,用于存放编译期生成的各种 字面量和符号引用. 字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值. 符号引用:包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符

堆:

  • Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。而且,Java堆在实现时,既可以是固定大小的,也可以是可拓展的,并且主流虚拟机都是按可扩展来实现的(通过-Xmx(最大堆容量)
    和 -Xms(最小堆容量)控制)。如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError 异常
  • TLAB (线程私有分配缓冲区) : 虚拟机为新生对象分配内存时,需要考虑修改指针 (该指针用于划分内存使用空间和空闲空间)时的线程安全问题,因为存在可能出现正在给对象A分配内存,指针还未修改,对象B又同时使用原来的指针分配内存的情况。TLAB的存在就是为了解决这个问题:每个线程在Java堆中预先分配一小块内存
    TLAB,哪个线程需要分配内存就在自己的TLAB上进行分配,若TLAB用完并分配新的TLAB时,再加同步锁定,这样就大大提升了对象内存分配的效率

程序计数器:在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址
虚拟机栈:每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程
本地方法栈:本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常,JDK1.8撤销方法区,增加元空间

方法区的回收

  • 主要是针对 常量池的回收 (判断引用) 和 对类型的卸载
  • 回收类: 1) 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例加载 2) 该类的ClassLoader已经被回收 3) 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

类加载过程
加载:获取二进制字节流、存储结构转化为运行时数据结构、生成Class对象
验证:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备:为类变量分配内存并设置类变量初始值,仅包括类变量
解析:将虚拟机常量池内的符号引用替换为直接引用
初始化:执行类中定义的Java程序代码

OOM
Java堆溢出
内存泄漏
内存溢出
虚拟机栈和本地方法栈溢出
建立过多线程
方法区和运行时常量池溢出
存放太多Class信息、常量
本机直接内存溢出

GC 算法

引用计数法

  • 循环引用

可达性分析算法

  • 通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

标记清除算法

  • 标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收
  • 效率问题:标记和清除两个过程的效率都不高
  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

  • 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
  • 实践中会将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间 (如下图所示),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ),只有10% 的内存会被“浪费”

标记整理算法

  • 标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)
  • 无内存碎片

垃圾收集器

CMS

  • 一种以获取最短回收停顿时间为目标的收集器
  • 老年代收集器
  • 标记-清除算法
  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除
  • 整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行

    缺点:

  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低

  • CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”

  • CMS是一款“标记-清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

G1

  • 面向服务端的垃圾收集器
  • 当今收集器技术发展的最前沿成果之一
  • 标记-整理+复制算法
  • 横跨整个堆内存: G1回收的范围是整个Java堆(包括新生代,老年代),将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合
  • 可预测的停顿: G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • 避免全堆扫描——Remembered Set:为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
  • 初始标记
  • 并发标记,从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行
  • 最终标记
  • 筛选回收,首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率

JVM调优参数
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

收集器设置
-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数。并行收集线程数

为什么分代收集
不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率

新生代进入老生代的情况

  • 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保
  • 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组
  • 长期存活的对象(-XX:MaxTenuringThreshold)将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中
  • 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

空间分配担保

  • 在发生Minor GC之前,虚拟机会先检查老年代最大连续可用空间是否大于新生代所有对象总空间,如果成立,Minor GC安全,不成立,检查是否允许担保失败,允许,检查老年代的剩余空间和之前晋升到老年代对象的平均大小:
  • 如果老年代的剩余空间 < 之前转入老年代的对象的平均大小,则触发Full GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且允许担保失败,则直接Minor GC,不需要做Full GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且不允许担保失败,则触发Full GC

类加载器

启动类加载器
扩展类加载器
应用程序类加载器

JAVA虚拟机的作用?
将java字节码解释为具体平台的具体指令,做到跨平台,也进行Java对象生命周期的管理,实现内存管理

JAVA虚拟机中,哪些可作为ROOT对象?
虚拟机栈中的引用对象
本地方法栈中的引用对象
方法区中类静态变量引用的对象
方法区中常量引用的对象

minor gc如果运行的很慢,可能是什么原因引起的?
新生代太大,导致gc时有太多对象需要回收

服务器

反向代理是什么?
反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的服务器簇的存在

负载均衡是什么?
负载均衡(Load balancing)是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。 使用带有负载均衡的多个服务器组件,取代单一的组件,可以通过冗余提高可靠性。负载平衡服务通常是由专用软件和硬件来完成。主要作用是将大量作业合理地分摊到多个操作单元上进行执行,用于解决互联网架构中的高并发和高可用的问题

Linux

常用命令
ps:查看进程
top:显示进程状态
free:查看内存
df:查看linux系统磁盘空间
du:显示每个文件和目录的磁盘使用空间
tail:查看文件尾部内容
cat:显示整个文件、创建文件、合并文件
grep:搜索过滤

算法

给你一个未知长度的链表,怎么找到中间的那个节点?
快慢指针,快指针走两步,慢指针走一步,快指针到尾了,慢指针走到一半

冒泡排序

public void sort(int[] array) {
    for(int i = 0; i < array.length; i++) {
        for(int j = 0; j < array.length - i - 1; j++) {
            if(array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
    }
}

快速排序

public void sort(int[] array) {
    sort(array, 0, array.length - 1);
}

public void sort(int[] array, int left, int right) {
    int _left = left;
    int _right = right;
    if (left < right) {
        int temp = array[left];
        while (left != right) {
            while (left < right && array[right] >= temp) {
                right--;
            }
            array[left] = array[right];

            while (left < right && array[left] <= temp) {
                left++;
            }
            array[right] = array[left];
        }

        array[right] = temp;
        sort(array, _left, --left);
        sort(array, ++right, _right);
    }
}

直接插入排序
每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的子序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止

public void sort(int[] array) {
    for(int i = 1; i<array.length; i++) {
        int temp = array[i];
        for(int j = i - 1; j >= 0 && array[j] > temp; j--) {
            array[j + 1] = array[j];
        }
        array[j + 1] = temp;
    }
}

希尔排序
简单选择排序
堆排序
归并排序
基数排序

操作系统

进程通讯方式
管道、消息队列、信号量、共享存储、socket

UML

UML中有哪些常用的图?
用例图、时序图、活动图、状态图

正文到此结束