1、线程状态转移
(1)线程生命周期中的5种状态
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Bolocked)和死亡(Dead)
新建(New):程序使用new关键字创建一个线程之后,该线程就处于新建状态,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。不会执行线程的线程执行体。
就绪(Runnable):线程对象调用start()方法后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器(线程私有),处于就绪状态的线程并没有开始运行,只是表示该线程可以运行,线程何时运行取决于JVM中线程调度器的调度。
运行(Running):处于就绪状态的线程获得CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞(Boloked):当调用sleep()、一个阻塞式IO方法、同步锁、等待通知、suspend()方法挂起都会使线程进入阻塞状态。(Blocked、Waiting、Timed_Waiting)
- 线程调用sleep()方法主动放弃所占用的处理器资源;
- 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
- 线程在等待某个通知(notify);
- 程序调用了线程的suspend()方法将该线程挂起,但这个方法易造成死锁,应该避免使用。
死亡(Dead):以如下3种方式结束线程(终止线程的方法)
- run()或call()方法执行完成,线程正常结束;可以设置一个flag标志位来控制循环是否执行,让线程离开run()方法;
- 线程抛出一个未捕获的Exception或Error;使用interrupt()方法打破阻塞状态,抛出InterruptedException异常,在run()方法中捕获该异常来让线程安全退出;
- 直接调用该线程的stop()方法来结束该线程(该方法易造成死锁,不推荐使用)
补充:线程从阻塞状态解除——进入就绪状态的过程:
- 调用sleep()方法的线程经过了指定时间;
- 线程调用的阻塞式IO方法已经返回;
- 线程成功地获得试图取得的同步监视器(锁);
- 线程正在等待某个通知时,其他线程发出了一个通知;
- 处于挂起状态的线程被调用了resume()恢复方法。
(2)start()方法和run()方法的区别
- 1)start()是启动线程,让线程从新建状态变为就绪状态;线程得到CPU时间片后,执行run()中的线程执行体;
- 2)start()只能调用一次;run()可以重复调用。
- 3)启动线程只能用start(),系统会把run()方法当做线程执行体处理;如果直接调用run(),系统会把线程对象当作普通对象,此时run()也是一个普通方法,而不是线程执行体。
- 4)源码中:start()源码中实际上通过本地方法start0()启动线程,会新运行一个线程,新线程会调用run()方法;而run()源码中target是Runnable对象,run()直接调用Thread线程的Runnable成员的run()方法,并不会新建一个线程。
(3)wait()和sleep()的区别
1)原理不同:wait()属于Object类;sleep()属于Thread类的静态方法;
2)锁的处理:wait()会导致线程释放对象的锁;sleep()导致当前线程让出CPU执行时间但不会释放对象的锁;
3)唤醒方式:wait()通过等待其他线程notify()或notifyAll()唤醒或者指定一个时间;sleep(long millis)是Thread的静态方法,经过指定时间后从阻塞状态进入就绪状态;
4)使用区域:wait()必须放在同步控制方法或者同步语句块中使用;而sleep()方法可以放在任何地方使用。
(4)创建线程的三种方式
1)继承Thread类创建线程类;
2)实现Runnable接口创建线程类;
3)使用Callable和Future创建线程。
1)继承Thread类创建线程类;
- 1.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表线程需要完成的任务。run()即为线程执行体;
- 2.创建Thread子类的实例,即创建线程对象;
- 3.调用线程对象的start()方法来启动该线程。
2)实现Runnable接口创建线程类;
- 1.定义Runnable接口的实现类,并实现该接口的run()方法,该run()是线程执行体;
- 2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
- 3.调用线程对象的start()方法来启动该线程。
3)使用Callable和Future创建线程。
- 1.创建Callable接口的实现类,并实现call()方法,该call()方法即为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例;
- 2.使用FutureTask类包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
- 3.使用FutureTask对象作为Thread对象的target创建并通过线程对象的start()方法启动线程;
- 4.调用FutureTask对象的get()方法获得子线程执行结束后的返回值。
4)三种方式的区别
- 1)返回值:继承Thread类,run()方法没有返回值;实现Runnable接口和Callable接口方式基本相同,但Callable接口定义的call()方法具有返回值,可以声明抛出异常;
- 2)继承:继承Thread类,不能再继承其他类;线程类实现Runnable和Callable接口可以继承其他类;
- 3)访问当前线程:继承Thread类,需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程;线程类实现Runnable和Callable接口,若访问当前线程,必须使用Thread.currentThread()方法。多个线程共享同一个target对象,适合多个相同线程来处理同一份资源的情况,从而将CPU、代码和数据分开,形成清晰的模型,较好地体现面向对象思想;
综合:推荐使用线程类实现Runnable和Callable接口方式创建多线程。
(5)为什么notify()、wait()等函数是定义在Object中而不是在Thread中?
因为notify(),wait()依赖于”同步锁“,而”同步锁“是对象所持有,并且每个对象有且只有一个。
(6)yield()方法
- 1)yield()是Thread类的一个静态方法,作用是让步而不会阻塞线程,让当前线程从“运行状态”进入“就绪状态”,让具有相同优先级或更高优先级的等待线程获取CPU执行权(当前线程有可能再次进入到“运行状态”)
- 2)yield()和sleep(long millis)的区别:yield()从Running进入Runnable状态,极有可能被调度选中继续运行;sleep(long millis)使线程进入Blocked状态且会等待超时时间后再进入Runnable状态,之后再等线程调度器的调度。
- 3)yield()和wait()的区别:wait()让线程由Running进入Blocked or Waiting状态,会释放同步锁。
(7)sleep()方法
- 1)sleep(long millis)方法是Thread类的一个静态方法,作用是让当前线程暂停一段时间,并进入阻塞状态。
- 2)两种重载形式:static void sleep(long millis)和static void sleep(long millis, int nanos),其中millis单位是毫秒,nanos单位是毫微秒。
- 3)sleep(long millis)方法常用来暂停程序的执行,进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行机会(即使系统没有其他可执行的线程,处于sleep()中的线程也不会执行)。
- 4)sleep(long millis)和wait()方法的区别:wait()会释放同步锁。
- 5)sleep()方法不会释放“锁标志”,容易引起死锁问题。
(8)sleep()与yield()的比较
- 1)优先级:sleep()方法给其他线程运行机会时,不考虑线程的优先级,低优先级的线程有可能回运行;yield()方法只会给相同优先级或更高优先级的线程运行的机会;
- 2)状态:sleep()方法使得线程转入阻塞撞他,在指定时间内不会执行;而yield()方法只是使当前线程重新回到就绪状态,所以当前线程有可能在进入到就绪状态后马上又被执行;
- 3)异常:sleep()方法声音抛出InterruptedException异常;而yield()方法没有声明任何异常;
- 4)可移植性:sleep()比yield()方法(跟操作系统有关)具有更好的可移植性。
2、ReentrantLock的理解
(1)ReentrantLock的介绍
1)Lock提供一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁都是显式的。
2)ReentrantLock实现Lock接口,并提供与synchronized相同的互斥性和内存可见性。
3)ReentrantLock提供和synchronized一样的可重入的加锁语义。
4)ReentrantLock是显式锁,需要显式进行lock以及unlock操作。形式比内置锁复杂,必须在finally块中释放锁,否则如果在被保护的代码中抛出异常,这个锁就永远都无法释放。加锁时,需要考虑在try块中抛出异常的情况,如果可能使对象处于某种不一致的状态,则需要更多的try-catch或try-finally代码块。
Lock lock = new ReentrantLock();
...
lock.lock();
try{
//更新对象状态
//捕获异常,并在必要时恢复不变性条件
}finally{
lock.unlock();
}
(2)使用ReentrantLock的灵活性(也是与synchronized的区别)
1)等待可中断
使用lock.lockInterruptibly()可以使得线程在等待锁支持响应中断;
使用lock.tryLock()可以使线程在等待一段时间过后如果还未获得锁就停止等待而非一直等待,更好的避免饥饿和死锁问题;
2)公平锁
默认情况下是非公平锁,但是可以成为公平锁,公平锁就是锁的等待队列的FIFO,不建议使用,会浪费许多时钟周期,达不到最大利用率。
3)锁可绑定多个条件
与ReentrantLock搭配的通信方式是Condition,且可以为多个线程建立不同的Codition
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await(); //等价于synchronized中的wait()方法
condition.signal(); //等价于notify()
condition.signalAll(); //等价于notifyAll()
(3)为什么要创建一种与内置锁相似的新加锁机制?
内置锁能很好的工作,但是在功能上存在一些局限性,如synchronized无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁的时候无限地等待下去,内置锁必须在获取该锁的代码块中释放,简化了编码工作,且与异常处理操作实现很好的交互,但无法实现非阻塞结构的加锁机制。所以需要提供一种更灵活的加锁机制来提供更好的活跃性或性能。
(4)synchronized和ReentrantLock的抉择
1)当需要可定时的、可轮询的、可中断锁、公平锁以及非块结构的锁(锁分段技术)时,才使用ReentrantLock,否则优先使用synchronized,毕竟现在JVM内置的是synchronized。
2)ReentrantLock的危险性,如果在try-finally中,finally未进行unlock,就会导致锁没有释放,无法追踪最初发生错误的位置。
(5)读-写锁ReadWriteLock
互斥锁,虽然避免了“写/写”冲突和“写/读”冲突,但也避免了“读/读”冲突,而大部分读操作比较多,且读操作不会改变数据,所以ReentrantLock提供的非互斥的读写锁,可以放宽加锁需求,允许多个读操作访问一个资源,实现更高的并发性,提升程序的性能。
3、synchronized的理解
(1)synchronized的介绍
- 1)synchronized机制提供了对与每个对象相关的隐式的监视器锁,并强制所有锁的获取和释放都必须在同一个块结构中。当获取了多个锁时,他们必须以相反的顺序释放。即synchronized对于锁的释放是隐式的。
- 2)synchronized同步块对同一条线程是可重入的,不会出现自己把自己锁死的问题。
- 3)synchronized可以修饰类、方法(包括静态方法)、代码块。修饰类和静态方法时,锁的对象是Class对象;修饰普通方法时,锁的是调用该方法的对象;修饰代码块时,锁的是方法块括号里的对象。
- 4)synchronized性能中会避免“读/读”操作,但读操作频繁,通过ReentrantLock提供的ReadWriteLock读写锁来解决该问题;阻塞线程时,需要OS不断的从用户态转到核心态,消耗处理器时间,通过自适应自旋来解决该问题。
- 5)synchronized的锁是存放在Java对象头里。
- 6)当一个线程进入一个对象的一个synchronized方法后,其他线程可以进入此对象的其他非synchronized方法或者调用静态的synchronized方法;如果要调用其他synchronized方法,则必须在该方法内部调用wait()方法。
(2)synchronized方法和synchronized块的区别
1)synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的,锁的是方法块后面括号里的对象;synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法,锁的是调用该方法的对象。
2)同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorexit和monitorenter相对应,任何对象都有一个monitor与之相对应,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他处于锁定状态,线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。在常量池中添加了ACC_ SYNCHRONIZED标识符,JVM就是根据该标识符来实现方法的同步。
(3)synchronized字节码层面实现的锁(锁计数器)。
- synchronized关键字经编译后,在同步块前后分别形成monitorenter和monitorexit两个字节码指令;
- 在执行monitorenter指令时,首先要尝试获取对象的锁,若这个对象没被锁定,或当前线程已经拥有了那个对象的锁,就把锁的计数器加1;
- 在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就会被释放;
- 若获取对象锁失败,当前线程进入阻塞等待,直到对象锁被另外一个线程释放为止。
(4)synchronized和ReentrantLock的异同
同:
- 都是可重入的,都属于同步互斥的手段;
异:
- 用法:synchronized是原生语法层面的互斥锁,加载同步方法或者同步代码块;ReentrantLock是API层面的互斥锁,显式地指定起始位置和终止位置;
- 锁机制:synchronized是内置锁,获取多个锁后,以相反的顺序隐式释放锁;ReentrantLock必须显式加锁释放锁,且可以自由的顺序释放锁。
- 功能:ReentrantLock增加高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件。
(5)重入锁
重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
(6)对象头
1)对象头中包括两部分数据:标记字段和类型指针;
2)类型指针Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
3)标记字段Mark Word是一个非固定的数据结构,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
(7)锁优化
1)jdk1.6对锁进行优化:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁。
4、volatile的理解
(1)volatile的介绍
1)volatile是比synchronized关键字更轻量级的同步机制:访问volatile变量时不会执行加锁操作,因此不会使执行线程阻塞。
2)volatile保证可见性和禁止指令重排序,底层是通过“内存屏障”来实现,但不保证原子性。
3)写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。
(2)可见性以及禁止指令重排序实现原理
1)可见性:可见性指当一条线程修改该变量值,新值对于其他线程来说是立即得知的。volatile底层通过lock前缀,作用使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于对Cache中的变量进行了JMM的“store和write”操作,本线程使其他线程的该变量的缓存行无效,当其他线程需要读该变量时发现该变量缓存行无效,就从主存中重新加载数据,所以保证了数据的可见性。
2)禁止指令重排序优化:volatile修饰的变量会在汇编代码中多执行一个lock操作,相当于一个内存屏障(Memory Barrier,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时并不需要内存屏障;但若有多个CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。lock指令把修改同步到内存时,意味着所有之前的操作都已经执行完毕,形成“指令重排序无法越过内存屏障”的效果。
(3)volatile的使用场景
- 对变量的写入操作不依赖变量的当前值,或能确保只有单个线程更新变量的值;
- 该变量不会与其他状态变量一起纳入不变性条件中;(该变量没有包含在其他变量的不变式中)
- 在访问变量时不需要加锁。
(4)JMM对volatile变量定义的特殊规则:
- 1.当前线程每次使用变量前都必须先从主内存刷新最新的值,用于保证能看到其他线程对变量的修改。(read、load、use连续执行。)
- 2.当前每次修改变量后都必须立刻同步到主内存中,用于保证其他线程可以看到自己对线程的修改。(assign、store、write连续执行。)
- 3.volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
(5)as-if-serial语义允许对存在控制依赖的操作做重排序的原因
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
(6)happen-before先行发生原则
1)顺序执行代码规则:同一个线程中的,前面的操作 happen-before 后续的操作;
2)synchronized规则:监视器上的解锁操作 happen-before 其后续的加锁操作;
3)volatile规则:对volatile变量的写操作 happen-before 后续的读操作;
4)线程启动规则:线程的start() 方法 happen-before 该线程所有的后续操作;
5)线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作;
6)传递性:如果a happen-before b,b happen-before c,则a happen-before c。
5、抽象类和接口的区别
- 1.普通成员变量:抽象类中可以有普通成员变量,接口中没有普通成员变量(static final变量);
- 2.方法:抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法;
- 3.静态成员变量:抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
- 4.静态方法:抽象类中可以包含静态方法,但不能是抽象静态方法;接口中不能包含静态方法(静态的方法不能被覆写)
- 5.构造方法(静态的):抽象类可以有构造方法,接口中不能有构造方法;
- 6.方法访问权限:抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
- 7.子类实现和继承: 一个类可以实现多个接口,但只能继承一个抽象类。
特点 |
抽象类 |
接口 |
构造方法 |
有 |
无 |
普通成员变量 |
有 |
无 |
普通方法 |
可以有非抽象的 |
必须是抽象的 |
抽象方法的访问类型 |
public、protected和默认 |
只能是public的,默认public abstract |
静态方法 |
可以有 |
无 |
静态成员变量 |
有 |
有 public static final的 |
其他类 |
只能继承一个抽象类 |
可以实现多个接口 |
应用场景 |
模块之间通信契约 |
代码重用 |
6、高速缓存的出现以及缓存一致性问题。
(1)高速缓存出现的原因?
答:由于计算机的存储设备(内存读写慢)与处理器的运算速度(快)有几个数量级的差距,加入一层读写速度尽可能接近处理器运算速度的高速缓存作为内存与处理器之间的缓冲。高速缓存的原理:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,再从缓存中同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
(2)缓存一致性问题?
答:在多处理器系统中,每个处理器都有自己的高速缓存,它们之间又共享同一主内存,当多个处理器的运算任务都涉及同一块主内存时,将可能导致各自的缓存数据不一致,则同步到主内存时数据不统一。解决方案:各个处理器访问缓存时遵守一些缓存一致性协议。
7、JMM内存模型
(1)工作内存与主内存
主内存:JMM规定所有变量都存储在主内存中(相当于Java堆中对象实例部分),相当于所有工作内存之间变量的中转站。
工作内存:每条线程都有自己的工作内存,保存了呗线程使用到的变量的主内存副本拷贝。线程对变量的读写都必须在工作内存中进行,对应Java虚拟机栈中的部分区域。
(2)工作内存与主内存之间的交互操作:
lock、unlock、read、load、use、assign、store、write
操作 |
作用区域 |
作用 |
lock(锁定) |
主内存变量 |
把一个变量标识为一条线程独占的状态。 |
unlock(解锁) |
主内存变量 |
把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 |
read(读取) |
主内存变量 |
把一个变量的值从主内存传输到线程的工作内存中,便于load的使用。 |
load(载入) |
工作内存变量 |
把read操作从主内存得到的变量值放入工作内存的变量副本中。 |
use(使用) |
工作内存变量 |
把工作内存中的一个变量值传递给执行引擎,每当VM遇到一个需要使用变量值的字节码指令时执行。 |
assign(赋值) |
工作内存变量 |
把一个从执行引擎接收到的值赋给工作内存的变量,每当VM遇到一个给变量赋值的字节码指令时执行。 |
store(存储) |
工作内存变量 |
把工作内存中的一个变量值传送到主内存中,便于write的使用。 |
write(写入) |
主内存变量 |
把store操作从工作内存得到的变量值放入主内存的变量中。 |
8、原子性、可见性和有序性
(1)原子性
原子性就是指:一个操作或多个操作,要么全部执行且执行的过程不被任何因素打断,要么就都不执行。基本数据类型的访问读写是具有原子性的(除了long和double64位)
(2)可见性
可见性是指:当一个线程修改共享变量的值,其他线程能够立即得知这个修改。(JMM通过在变量修改后将新值同步回主内存,在其他线程读取该变量时,从主内存刷新变量值的方式保证可见性。)
(3)有序性
volatile禁止指令重排序(底层是通过内存屏障实现,内存屏障后面的操作不会先于前面的操作发生,所以保证前面的操作都完成后,再完成后面的操作);synchronized是一个变量在同一个时刻只允许一条线程对其lock操作,即持有同一个锁的两个同步块只能串行地进入。
9、先行发生原则(happens-before)
(1)先行发生原则的介绍
1)先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。
2)JMM定义的两个操作之间的偏序关系。如操作A先行发生于操作B,则在发生操作B之前,操作A的影响(修改内存中共享变量的值、发生消息、调用方法)会被操作B观察到。
(2)先行发生原则的8个
规则 |
介绍 |
程序次序规则 |
在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(控制流顺序,如分支、循环等结构) |
管程锁定规则 |
一个unlock先行发生于后面对同一个锁的lock操作。 |
volatile变量规则 |
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。 |
线程启动规则 |
Thread对象的start()方法先行发生于此线程的每一个动作。 |
线程终止规则 |
线程中的所有操作都先行发生于对此线程的终止检测。通过Thread.join()方法结束、Thread.isAlive()的返回值检测到线程已经终止。 |
线程中断规则 |
对线程interrupt()方法的调用先行发生于被中断的代码检测到中断事件的发生,可通过Thread.interrupted()方法检测到是否有中断发生。 |
对象终结规则 |
一个对象的初始化完成(构造函数执行结束)先行发生于finalize()方法的开始。 |
传递性 |
操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。 |
10、HashMap和TreeMap
- 1.查询插入等用途(顺序性):在Map中插入、删除和定位元素,HashMap适合;若要按顺序遍历键,则TreeMap适合。
- 2.底层优化:HashMap可以调优初始容量和负载因子;TreeMap没有调优选项,因为树总处于平衡状态。
- 3.实现接口:都实现了Cloneable接口。HashMap继承AbstractMap;TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键的升序)。
- 4.底层实现:HashMap底层是数组+链表法;TreeMap底层是数组+红黑树。(从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值(默认为8)便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。)
11、HashMap和Hashtable
同:
两者都是用key-value方式获取数据。
异:
- 1.null值:HashMap允许null值作为key和value;Hashtable不允许null的键值;
- 2.顺序性:HashMap不保证映射的顺序不变,但是作为HashMap的子类LinkedHashMap默认按照插入顺序来进行映射的顺序;Hashtable无法保证;
- 3.线程安全:HashMap是非同步的(非线程安全),效率高;Hashtable是同步的(线程安全的),效率低。(HashMap同步通过Collections.synchronizedMap()实现)
- 4.快速失败机制:迭代HashMap采用fail-fast快速失败机制(快速失败机制:是一个线程或软件对于其故障做出的响应,用来即时报告可能会导致失败的任何故障情况,如果一个Iterator在集合对象上创建了,其他线程想结构化的修改该集合对象,抛出并发修改异常ConcurrentModificationException);而HashTable的enumerator迭代器不是fail-fast的(Hashtable的上下文同步:一个时间点只能有一个线程可以修改哈希表,任何线程在执行Hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放;
- 5.父类:HashMap继承AbstractMap,Hashtable继承Dictionary;
- 6.数组默认大小:HashMap底层数组的默认大小是16,扩容是2*old;Hashtable底层数组默认是11,扩容方式是2*old+1;
- 7.效率:HashMap是非线程安全的,单线程下效率高;Hashtable是线程安全的,方法都加了synchronized关键字进行同步,效率较低;
- 8.计算hash方式不同:HashMap是二次hash,对key的hashCode进行二次hash,获得更好的散列值;而Hashtable是直接使用key的hashCode对table数组进行取模。
如何让HashMap同步
通过Collections集合工具类中的synchronizedMap()方法实现同步:Map map = Collections.synchronizedMap(hashMap);
Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedTMBap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理
12、Collection和Collections的区别
- Collection是集合类的顶层接口,主要子接口由List、Set和Queue组成。
- Collections是针对集合类的工具类,提供操作集合的一系列静态方法,如线程安全化、搜索、排序等操作。
- Collections的用法:当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它:在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
13、ArrayList和LinkedList的区别?
- 底层实现:ArrayList实现是基于动态数组的数据结构,LinkedList是基于链表的数据结构(双向链表);
- 查询:对于随机访问get和set,ArrayList支持,LinkedList不支持,因为LinkedList要移动指针;
- 增删:对于新增和删除操作add和remove,在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
- 应用场景:ArrayList适合一列数据的后面添加数据而不是在前面或中间,且需要随机访问元素;LinkedList适合在一列数据的前面或中间添加或删除数据,且按照顺序访问其中的元素。
- 消耗内存:LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点都存储前后节点的引用。(双向链表)
14、ArrayList和Vector的区别?
1.线程安全性:ArrayList是非线程安全的,Vector是线程安全的,如果需要在迭代的时候对列表进行改变,使用CopyOnWriteArrayList。
2.效率:ArrayList是非同步的,效率高;Vector是同步的,效率低;
15、Hashmap和Hashset区别
HashSet底层是通过HashMap实现的
- public HashSet() {
- map = new HashMap<>();
- }
- 4. private static final Object PRESENT = new Object();
- 5. public boolean add(E e) {
- return map.put(e, PRESENT)==null;
- }
add的时候,调用map的put方法,value始终是PRESENT,所以HashSet是所有value值都相同的HashMap。
- 1.实现接口:HashSet实现了set集合的接口,不允许有重复的值(准确说应该是元素作为key,value是定义为final的对象),将对象存储在HashSet之前,需要确保对象已经重写了equals和hashCode方法,这样才能比较两个对象的值是否相等,确保set中没有存储相等的对象。HashMap实现了map集合接口,对键值对进行映射。Map中不允许重复的键。
- 2.存储元素:HashMap存储的是键值对,不允许有重复的键;Hashset存储的是对象,不允许有重复的对象。
- 3.添加元素方法:HashMap使用put方法将元素放入map中,HashSet使用add方法将元素放入set中。
- 4.hashCode值的计算:HashMap中使用键对象计算hashcode的值,HashSet中使用成员对象来计算hashcode值。
- 5.效率:HashMap比较快,因为使用唯一的键来获取对象,HashSet比HashMap慢。
- 6.底层实现:HashSet是所有value值都相同的HashMap。HashSet内部使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个object而已。private transient HashMap<E,Object> map;只是包含了hashmap的key。
16、Iterater和ListIterator区别
- 遍历目标:可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
- 遍历方向:Iterator只可以向前遍历,而LIstIterator可以双向遍历。
- 功能区别:ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
17、HashMap
(1)底层实现
1)底层构成:HashMap底层是数组+链表的形式,实现Map.Entry接口。数组是Entry[]数组,是一个静态内部类,数组元素Entry是key-value键值对,持有一个指向下一个元素的next引用,从而构成单向链表。
2)底层数组:HashMap底层数组长度为2^n,默认是16,负载因子是0.75,所以最大容量阈值为16*0.75=12,当数组元素超过该值就进行扩容。(增加一倍)
3)底层链表:底层哈希表解决哈希冲突的方法是:链地址法,将相同的hash值的对象组织成一个链表放在hash值对应的槽位。
4)底层数组长度为2^n的原因:这么保证h&(length-1)总是计算得到索引值是位于table数组的索引之内。
(2)负载因子为0.75?
答:这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
(3)put的过程(hashCode()和equals()的使用)
1)向一个HashMap中添加一对key-value时,首先对key的hashCode值进行二次hash,根据hash值确认在table中存储的位置、
2)若Entry的存储位置上没有元素,直接插入;否则,遍历该Entry处的链表,通过equals比较key值是否相等,如果返回true,则新添加的Entry的value覆盖原来的value;如果返回false,则将新对象放入到数组中,将原有的Entry对象链接到此对象后面(新对象next指向原对象)。
(4)遍历方式
1)通过Map.Entry接口实现键值对的遍历
Map<String, Integer> map = new HashMap<>();
Iterator<Map.Entry<String, Integer>> itr = map.entrySet().iterator();
//等价于Set<Map.Entry<String, Integer>> set = map.entrySet();
//Iterator<Map.Entry<String, Integer>> itr = set.iterator();
while(itr.hasNext()){
Map.Entry<String, Integer> entry = itr.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key+" : "+value);
}
2)先得到键的Set集合,再通过map的get()方法得到value
Iterator<String> it = map.keySet().iterator();
while(it.hasNext()){
String key = it.next();
Integer value = map.get(key);
System.out.println(key+"="+value);
}
3)foreach循环
for(Map.Entry<String, Integer> entry : map.entrySet()){
System.out.println(entry.getKey() +"=" +entry.getValue());
}
(5)解决hash冲突的方式
为什么会有冲突?因为不同对象的hashCode()方法返回了一样的值。
- 1.开放定址法:线性探测再散列、二次探测再散列、随机探测再散列;(二次探测法弥补了线性探测法不能探测前面的缺陷)
- 2.再哈希法:换一种哈希函数;
- 3.链地址法:在数组中冲突元素后面拉一条链路,存储重复的元素;
- 4.建立一个公共溢出区:其实就是建立一个表,存放那些冲突的元素。
(6)Java8中解决hash冲突的改进
从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值(默认为8)便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。
18、访问权限
(1)访问权限由高到低:
public(接口访问权限)、protected(继承访问权限)、包访问权限(没有使用任何访问权限修饰词)、private(私有无法访问)
(2)4个访问权限详解
1)public:公共的,修饰类、方法和属性,可以被所有类访问。
2)protected:受保护的,修饰方法和属性,可以在类内部、相同包以及该类的子类所访问。
3)private:私有的,修饰类、方法和属性,只能该类内部使用。
4)包访问权限:没有任何访问权限修饰符,在类内部以及相同包下面的类使用。
19、值传递与引用传递
值传递:Java中原始数据类型都是值传递,传递的是值的副本,形参的改变不会影响实际参数的值;
引用传递: 传递的是引用类型数据,包括String,数组,列表,map,类对象等类型,形参与实参指向的是同一内存地址,因此形参改变会影响实参的值。
20、final关键字
1)使用范围:数据、方法和类
2)final关键字:final可以修饰属性、方法、类。
3)final修饰类:当一个类被final所修饰时,表示该类是一个终态类,即不能被继承。
4)final修饰方法:当一个方法被final所修饰时,表示该方法是一个终态方法,即不能被重写(Override)。
5)final修饰属性:当一个属性被final所修饰时,表示该属性不能被改写。
21、初始化及类加载
1)加载的触发:静态域或者对象的创建都会加载。
2)加载的过程:父类静态域——父类静态块——子类静态域——子类静态块——父类成员变量及代码块——父类构造器——子类成员变量及代码块——子类构造器。
22、基本数据类型与包装类
1)基本数据类型:byte:8位;short:16位;int:32位;long:64位;float:32位;double:64位;char:16位;boolean:8位。
2)所有的包装类(8个)都位于java.lang包下,分别是Byte,Short,Integer,Long,Float,Double,Character,Boolean
23、==与equals方法的区别
1)基本数据类型的比较:只能用==
2)引用数据类型的比较:==是比较栈内存中存放的对象在堆内存地址,equals是比较对象的内容是否相同。
3)特殊:String对象
1.构造函数创建对象时:
String s1 = new String("java");
String s2 = new String("java");
System.out.println(s1==s2); //false
System.out.println(s1.equals(s2));
//true
2.字面量形式创建对象时:
String s1 = "java";
String s2 = "java"; //此时String常量池中有java对象,直接返回引用给s2;
System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
String的字面量形式和构造函数创建对象
1)String s = "aaa";采用字面值方式赋值
1.查找String Pool中是否存5728“aaa”这个对象,如果不存在,则在String Pool中创建一个“aaa”对象,然后将String Pool中的这个“aaa”对象的地址返回来,赋给引用变量s,这样s会指向String Pool中的这个“aaa”字符串对象;
2.如果存在,则不创建任何对象,直接将String Pool中的这个“aaa”对象地址返回来,赋给s引用。
2)String s = new String("aaa");
1.首先在String Pool中查找有没有"aaa"这个字符串对象,如果有,则不在String Pool中再去创建"aaa"这个对象,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回来,赋给s引用,导致s指向了堆中创建的这个"aaa"字符串对象;
2.如果没有,则首先在String Pool中创建一个"aaa"对象,然后再去堆中创建一个"aaa"对象,然后将堆中的这个"aaa"对象的地址返回来,赋给s引用,导致s指向了堆中所创建的这个"aaa"对象。
24、Object类的公有方法8种,11个
clone()(protected的)、toString()、equals(Object obj)、hashCode()、getClass()、finialize()(protected的)、notify()/notifyAll()、wait()/wait(long timeout)、wait(long timeout, intnaos)
25、面向对象的三大基本特征
封装、继承和多态
(1)封装
隐藏一切可以隐藏的消息,只向外界提供最简单的编程接口;类就是对数据和方法的封装;方法就是对具体实现细节的封装;
(2)继承
从已有的类继承得到继承信息,创建新类的过程,并无需重新编写与原来的类相同的方法或成员变量情况下就可以对这些功能进行扩展。
(3)多态
允许父类型的引用指向子类型的对象。
实现方式:方法重载(编译器绑定,前绑定)和方法重写(运行期绑定,后绑定)
26、final、finally、finalize的区别
final是Java的一个关键字,用于定义不能被继承的类,不能被覆写的方法(但是可以重载),不能修改的常量。
finally是Java的一个关键字,是异常处理操作的统一出口。
finalize是Object类中所提供的一个方法,用于在对象回收之前进行收尾操作。
27、try、throw和throws区别
(1)throw和throws区别
1.throw是语句抛出一个异常。
语法:throw (异常对象);
throw e;
throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常)
语法:[(修饰符)](返回值类型)(方法名)([参数列表])[throws(异常类)]{......}
public void doA(int a) throws Exception1,Exception3{......}
2.throw语句用在方法体内,表示抛出异常,由方法体内的语句处理;throws语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。
3.throw是具体向外抛异常的动作,已经发生异常,被捕获到,要抛出该异常,所以它是抛出一个异常实例;throws主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常,倾向于发生,但不一定发生。
(2)try和throws区别
如果该功能内部可以将问题处理,用try,如果处理不了,则交由调用者处理,用throws进行抛出异常。
区别:后序程序需要运行,则用try;后序程序不需要继续运行,则用throws;
28、switch语句
1.7之前:byte、short、int、char
1.7及之后:byte、short、int、char、String
29、反射
(1)反射机制的介绍
1)反射主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
2).在Java运行时环境中,对任意一个类,可以通过反射知道类的属性以及方法;对于任意一个对象,通过反射可以调用它的任意方法。通过反射动态获取类的信息以及动态调用对象的方法的功能。要想解剖一个类,必须先要获取到该类的字节码文件对象,而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。
(2)反射的作用
1.反编译:.class----->.java
2.通过反射机制访问Java对象的属性、方法、构造方法等;
3.在运行时判断任意一个对象所属的类;
4.在运行时构造任意一个类的对象;
5.在运行时判断任意一个类所具有的成员变量和方法;
6.在运行时调用任意一个对象的方法。
(3)获取Class对象的三种方式
第一种方式:使用继承自Object的getClass()方法
Person p = new Person();
Class c = p.getClass();
第二种方式:任意类都具备一个class静态属性
Class c2 = Person.class;
第三种方式:将类名作为字符串传递给Class类的静态方法forName
Class c3 = Class.forName("Person");
30、异常
1)编译时异常(Checked Exception)
除了RuntimeException及其子类,Exception中所有的子类都是,这种异常必须要处理,要不编译通不过。
2)运行时异常(Unchecked Exception)
RuntimeException及其子类都是,这种异常不用处理,编译会通过,不过这样的程序会有安全隐患,遇到这种异常是需要改代码的。属于编程错误,会自动被JVM抛出异常。
3)严重错误问题
用Error进行描述,这个问题发生后,一般不编写针对代码进行处理,而是要对程序进行修正.通常都是由虚拟机抛出的问题。
31、浅拷贝和深拷贝
(1)浅拷贝和深拷贝的介绍
1)浅拷贝:基本数据类型直接拷贝,但是对象,直接将源对象中的引用值拷贝给新对象的字段,即所有的对其他对象的引用仍然指向原来的对象。任何一个对该引用对象的修改,都会反映到其他引用上。
2)深拷贝:基本数据类型直接拷贝,但是对象,复制一个相同的对象,并将这个新的对象的引用赋给新拷贝的字段,即那些引用其他对象的变量将指向被复制过的新对象。
浅拷贝:拷贝一个对象时,拷贝这个对象里的所有字段,而引用对象不拷贝,直接拷贝指向这个引用对象的变量字段,所以如果副本中引用对象发生改变,源对象也会改变。
深拷贝:拷贝一个对象时,对象中的字段和引用对象都会拷贝,所以副本和源对象是相互独立的。相当于是一个克隆体。
(2)Java中的克隆
Object中定义的clone()方法,是protected,只有实现了Cloneable接口的类才可以在其实例上调用clone()方法,否则会抛出CloneNotSupportException
(3)深拷贝一个对象
1)该对象实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象引用的其他对象也clone一份,该被引用的的对象也必须实现Cloneable接口并实现clone方法。
2)利用串行化做深复制,将对象序列化,然后再进行反序列化。使对象实现Serializable接口。写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来便可以重建对象。
(4)clone方法的工作流程
1)需要克隆的类实现Cloneable接口并重写clone()方法,创建副本;
2)在该类中调用clone方法被委派给super.clone(),可以是自定义的super class或者是默认的java.lang.Object;
3)最后调用到达java.lang.Object的clone()时,验证相关的类是否实现了Cloneable接口,如果没有实现那么抛出CloneNotSupportedException,否则创建filed-by-field。
32、String
(1)StringBuilder和StringBuffer
特性 |
StringBuilder |
StringBuffer |
final和不可继承性 |
都是 final 类, 都不允许被继承; |
都是 final 类, 都不允许被继承; |
长度 |
可变 |
可变 |
线程安全 |
非线程安全 |
线程安全(synchronized修饰) |
性能 |
好 |
不好(synchronized修饰方法,效率降低) |
(2)String不变性的理解
1)final和不能继承性:String类是被final修饰的,不能被继承;
2)“+”的使用:使用+号链接字符串的时候会创建新的字符串,底层,JDK1.5之前,转化为StringBuffer对象的连续append()操作,在JDK1.5及之后,通过new一个StringBuilder对象,append()方法实现连接。
3)String s = new String("Hello world");可能创建两个对象也可能创建一个对象。如果String Pool池中有该字符串,则仅仅在堆中创建一个对象,然后将返回堆中的对象引用赋给s;如果String Pool池中没有该字符串对象,则在堆和String Pool中都要创建对象。字符串常量池实现原理就是因为String对象是不可变的,可以安全保证多个变量共享同一个对象,否则String对象若可变,一个引用操作改变了对象的值,则其他变量会受到影响。
33、面向对象的原则
单一职责、开放封闭、里氏替换、依赖倒置、合成聚和复用、接口隔离、迪米特法则
单一职责:一个类只做它该做的事(高内聚);
开放封闭:对扩展开放,对修改关闭;
里氏替换:任何时候都可用子类型替换父类型;
依赖倒置:面向接口编程(抽象类型可被任何一个子类所替代);
合成聚和复用:优先使用聚合或合成关系复用代码;
接口隔离:一个接口只应描述一种能力,接口应该是高内聚的(小而专一);
迪米特法则:最少知识原则,一个对象应对其他对象尽可能少的了解。
34、Java1.8新特性
1)接口可以有默认的方法(default关键字修饰),也可有静态方法(声明并可以实现);
2)lambada表达式并且在类型推测方面有很大提高;(lambda表达式允许将一个函数当做方法的参数,将代码当做数据。
3)扩展了集合类Collection.steram();
4)HashMap底层实现可以采用数组+链表+红黑树实现,当链表长度超过阈值8时,就将链表转换为红黑树,由O(n)提升到O(logn);
5)扩展注解的支持(局部变量、泛型类、父类与接口的实现、方法的异常都可以添加异常);
35、重写(@Override)与重载
(1)区别
1)形式:重写Override,方法名、参数列表、返回值类型小于等于父类的;重载Overload,方法名相同,参数列表不同(个数、类型、顺序),返回值类型可同可不同1
2)分派:重写是运行时动态分派,发生在继承的父子类中;重载是编译时的静态分派,同一个类中;
3)权限:重写,子类的覆写的方法权限不能比父类的小;重载,无权限限制;
4)定义:重写,当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法, 即在子类中重写该方法——相同参数,不同实现(动态多态性);重载,同样的一个方法能够根据输入数据的不同,做出不同的处理,即方法的重载——有不同的参数列表(静态多态性);
(2)重写中的两同两小一大原则:
两同:方法名相同,参数类型相同
两小:子类返回类型小于等于父类方法返回类型,
子类抛出异常小于等于父类方法抛出异常,
一大:子类访问权限大于等于父类方法访问权限。
(3)多态的实现机制
多态是表示当同一个操作作用在不同对象时,会有不同的语义,从而会产生不同的结果。两种表现形式:方法的重载和方法的重写;
1)方法的重载:同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,是一种编译时多态;
2)方法的重写:子类可以覆盖父类的方法,同样的方法在父类与子类中有着不同的表现形式,父类的引用变量不仅可以指向父类的实例对象,也可以指向子类的实例对象;同样,接口的引用变量也可以指向其实现类的实例对象。而程序调用的方法是在运行期才动态绑定,是一种运行时多态。
注意:只有类中的方法才有多态的概念,类中成员变量没有多态的概念,成员变量的取值是在编译期间就确定的。
36、泛型
1)泛型是参数化类型,所操作的数据类型被指定为一个参数;
2)泛型擦除:Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。运行时,会将泛型去掉,生成的class文件中是不带泛型的。这个称之为泛型的擦除。
3)是jdk1.5出现的安全机制,将错误检测移入到编译期,可以指定容器类要持有什么类型的对象,而且由编译器保证类型的正确性;
4)泛型的补偿:避免了强制装换的麻烦,所有的强制类型都是自动和隐式的,以提高代码的重用率;
5)因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,所以泛型类中静态变量或静态方法中不能使用泛型类所声明的泛型类型参数。
37、内部类
(1)内部类介绍
1)内部类可以很好的实现隐藏(一般的非内部类不允许有private与protected权限);
2)内部类拥有外围类的所有元素的访问权限;
3)内部类可以间接实现多重继承,最重要的是,内部类允许继承多个非接口类型(类或抽象类);
4)内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立;
(2)内部类的分类
1)在类中定义一个类(成员内部类、静态内部类);
2)在方法中定义一个类(局部内部类、匿名内部类);没有访问修饰符,、
(3)内部类分类的4种(或者说是3种:成员内部类、局部内部类、匿名内部类)
1)静态内部类static inner class,即嵌套类
- 最简单的内部类形式,只要类定义时加上static关键字;
- 不能和外部类有相同的名字;
- 被编译成独立的.class文件,名称为OuterClass$InnerClass.class的形式;
- 只可以访问外部类的静态成员和静态方法,包括私有静态成员和方法;
- 生成静态内部类对象的方式:OuterClass.InnerClass inner = new OuterClass.InnerClass();
2)成员内部类member inner class
- 成员内部类也是定义在另一个类中,但是定义时不能用static修饰;
- 成员内部类和静态内部类可以类比为非静态成员变量和静态成员变量;
- 成员内部类就像是一个实例变量,可以访问其外部类的所有成员变量和方法,不管是静态还是非静态的;
- 在外部类内创建成员内部类实例:this.new InnerClass();
- 在外部类之外创建成员内部类实例OuterClass.InnerClass inner =new OuterClass().new InnerClass();
- 在内部类里访问外部类的成员:OuterClass.this.member;
3)局部内部类local inner class
- 局部内部类不能有访问说明符,因为不是外围类的一部分,但是可以访问当前代码块内的常量以及此外围类的所有成员。
- 局部内部类定义在方法中,比方法的范围小;
- 像局部变量一样,不能被public、protected、private和static修饰;
- 只能访问方法中定义的final类型的局部变量;
- 局部内部类只能在方法中使用,即只能在方法中生成局部内部类的实例并且调用其方法。
4)匿名内部类anonymous inner class
- 匿名内部类就是没有名字的局部内部类,不使用关键字class,extends,implements,没有构造方法;
- 匿名内部类隐式地继承一个父类或实现一个接口;
- 通常作为一个方法参数;
- 如果定义一个匿名内部类,并希望使用一个在其外部定义的对象,则编译器会要求参数引用是final的,否则编译错误。
(4)匿名内部类和局部内部类区别
1.使用场景:匿名内部类是用于实例初始化,如果需要重构构造器则需要局部内部类,局部内部类的名字在方法外是不可见的。
2.使用局部内部类:需要不止一个该内部类的对象。
38、网络编程
(1)网络编程介绍
网络编程是java.net包以及配合java.io包,编写运行在多个设备(计算机)的程序,这些设备通过网络连接起来。
(2)TCP/IP协议
IP协议:准备定位到网络的一台或几台主机,所以网络层负责网络主机的定位,数据传输的路由,由IP地址唯一确定网络中一台主机。
TCP协议:进行可靠高效的数据传输,所以传输层负责面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象。
(3)TCP和UDP
1)TCP是传输控制协议,是一种面向连接的保证可靠传输的传输层协议,可以得到一个顺序的无差错的数据流,必须建立连接。
2)UDP是用户数据报协议,是一个面向无连接的传输层协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,在网络上以任何可能的路径传往目的地,叨叨目的的时间以及内容的正确性无法保证。
区别:
1)有无连接:TCP是面向连接的,三次握手,四次挥手;UDP是面向无连接的,每个数据报中都有完整的地址信息(源地址和目的地址);
2)通讯方式:TCP由于需要建立连接,所以是点对点通讯;UDP由于不需要建立连接,所以可以实现广播发送;
3)数据大小:TCP传输无大小限制;UDP传输数据有大小限制,每个数据报限定在64KB之内;
4)可靠性:TCP是可靠的协议,能保证接收方完全正确顺序获取发送方的数据;UDP是不可靠的协议,发送方的数据报不一定以相同的次序达到接收方;
5)传输效率:TCP由于可靠的传输,其效率低于UDP;
5)应用场景:TCP适用于远程连接和文件传输(可靠性及数据不定长性);UDP适用于视频会议系统(保持连贯性);
(4)Socket编程
1)Socket为“套接字”,即IP地址+端口;
2)Socket通讯过程:服务器程序将一个套接字绑定到一个特定的端口,并通过此套接字等待和监听某个端口是否有连接请求(ServerSocket类的accept()方法);客户端向服务端发送连接请求;服务端收到连接请求向客户端发出接收消息,这样一个连接就建立起来了;客户端和服务端都可以相互发送消息与对方进行通讯;关闭Socket。
3)Socket的基本工作:创建Socekt;打开连接到Socket的输入输出流;按照一定的协议对Socket进行读写操作;关闭Socket。
4)客户机/服务器C/S结构:通信双方一方作为服务器等待客户端提出请求并予以响应。客户端则在需要服务时向服务器提出申请,服务器始终运行,监听网络端口,一旦有客户端请求,就会启动一个服务线程来响应该客户端,同时自己继续监听服务端口,使后来的客户端也能及时的得到服务。
(5)七层参考模型
协议:
应用层:远程登录协议telnet、文件传输协议ftp、超文本传输协议http、域名服务DNS、简单邮件传输协议SMTP、邮局协议POP3等;
传输层:传输控制协议TCP、用户数据报协议UDP、
网络层:网际协议IP、Internet互联网控制报文协议ICMP、Internet组管理协议IGMP
39、序列化与反序列化
(1)序列化介绍
1)实现Serializable接口和Externalizable接口的对象转换成字节序列,并能通过该字节序列完全恢复原来的对象,序列化可以弥补不同操作系统间的差异。
2)只有实现了Serializable和Externalizable接口的了的对象才能被序列化,Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。
(2)序列化步骤
- 1.创建一个对象输出流,可以包装一个其他类型的目标输出流,如文件输出流;(JDK中,java.io.Object.OutputStream)
- 2.通过对象输出流的writeObject(Object obj)方法写对象。
(3)反序列化步骤
- 1.创建一个对象输入流,可以包装一个其他类型的源输入流,如文件输入流(JDK中,java.io.Object.InputStream);
- 2.通过对象输入流的readObject(Object obj)方法读取对象;
(4)序列化注意
1)static修饰的属性、方法不会被序列化;
2)通过网络、文件进行序列化时,必须按照写入的顺序读取对象;
3)反序列化时必须有序列化对象的class文件;
4)serializabeID要声明从而避免由于不同的JVM而导致反序列化的失败;
5)序列化协议:protobuf占用空间小、序列化反序列化快,但可读性差,所以应用于跨网络之间数据通信比较合适;json可读性比xml稍差但比protobuf好,空间和速度比protobuf差但比xml好,所以适用于局域网和同机器不同系统之间通信;xml效率不高,但可读性很好,所以适用于数据存储。
6)序列化时,A中有B的引用,A序列化时,会将B一并地序列化;若B不能被序列化,则需要transient关键字来修饰,表示该变量不会被序列化。
40、常见设计模式及应用
(1)单例模式:
1)定义:保证整个应用程序中某个实例有且只有一个产生。
2)应用:hibernate中Session的创建就是SessionFactory通过饿汉式单例创建的;
Spring中自动创建bean,默认是singleton,后面bean是通过注入获得对象而不需要再new;
struts1 Action就是单例模式的;
(2)工厂模式:
1)定义:定义一个接口来创建对象,让子类来决定实例化哪些类,用工厂方法代替new操作。(creator声明工厂方法、ConcreteCreator重定义工厂方法、Product产品接口、ConcreteProduct具体产品)
2)应用:JDBC是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,由一组用Java语言编写的类和接口组成。
Spring中BeanFactory作为Spring基础的IOC容器,是Spring的一个Bean工厂。如果从工厂模式看,就是用来生产Bean,然后提交给客户端。
(3)观察者模式:
1)定义:定义对象间的一种一对多的依赖关系,发布——订阅模式,让多个观察者同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能自动更新自己。(Subject主题、ConcreteSubject具体主题、Observer抽象观察者、ConcreteObserver具体观察者)
2)应用:触发联动,Tomcat中控制组件生命周期的Lifecycle就是观察者模式,以及对Servlet实例的创建,Session的管理,如抽象主题Lifecycle接口,具体主题StandardServer,抽象观察者LifecycleListener,具体观察者ServerLifecycleListener。
(4)代理模式:
1)定义:给某个对象创建一个代理对象,由这个代理对象控制对原对象的引用,而创建这个代理对象可以在调用原对象时可以增加一些额外的操作。
2)分类:远程代理、虚拟代理、保护代理、智能引用代理
3)应用:Spring AOP中JDK动态代理;
(5)适配器模式:
1)定义:适配器将源角色适配成目标接口,将一个类的接口,转换成客户期望的另一个接口,使得原本由于接口不兼容而不能在一起工作的那些类可以在一起工作。主要是用来解决不兼容、不匹配而引入的。(目标接口Target、Adaptee源角色、Adapter适配器)
2)应用:Java I/O中字符和字节读写是不兼容的,则通过适配器来转换,如将字节数据转变成字符流,InputStream作为源角色,Reader类作为目标接口,通过InputStreamReader适配器将InputStream适配成Reader类。
(6)策略模式:
1)定义:将可变的部分从程序中抽象分离成算法接口,该接口下分别封装一系列算法实现,并使它们可以相互替换,从而导致客户端程序独立于算法的改变。(策略环境Context、Strategy抽象策略、ConcreteStrategy具体抽象策略)
2)Spring中策略模式的实现:Bean定义对象的创建及代理对象的创建。代理对象的创建由JDK动态代理(实现接口)和CGLIB代理。
(7)装饰器模式:
1)定义:装饰器将某个类重新装扮,但作为原来的这个类使用者不应该感受到装饰前和装饰后有什么不同,否则就破坏了原有类的结构,装饰器模式对被装饰类的使用者透明。保持原有的接口,但是增强原有对象的功能。(Component抽象组件,ConcreteComponent具体组件实现,Decorator抽象装饰器,ConcreteDecorator具体装饰器实现类)
2)应用:I/O中,InputStream就是抽象组件,FileInputStream就是具体组件,而FilterInputStream是抽象装饰器,BufferedInputStream就是具体装饰器,作用就是使得InputStream读取的数据保存在内存中,而提高读取的性能。
41、面向推向的设计原则
1)单一职责原则、开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则、合成复用原则、迪米特法则
2)里氏代换原则更好的实现了开闭原则;开闭原则是目的,里氏代换原则是基础,依赖倒转是手段
设计原则名称 |
定 义 |
单一职责原则 (Single Responsibility Principle, SRP) |
一个类只负责一个功能领域中的相应职责 |
开闭原则 (Open-Closed Principle, OCP) |
软件实体应对扩展开放,而对修改关闭 |
里氏代换原则 (Liskov Substitution Principle, LSP) |
所有引用基类对象的地方能够透明地使用其子类的对象
|
依赖倒转原则 (Dependence Inversion Principle, DIP) |
抽象不应该依赖于细节,细节应该依赖于抽象 |
接口隔离原则 (Interface Segregation Principle, ISP) |
使用多个专门的接口,而不使用单一的总接口 |
合成复用原则 (Composite Reuse Principle, CRP) |
尽量使用对象组合,而不是继承来达到复用的目的
|
迪米特法则 (Law of Demeter, LoD) |
一个软件实体应当尽可能少地与其他实体发生相互作用 |
42、内存溢出和内存泄漏
(1)内存溢出
1)定义:内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现内存溢出;比如申请了一个Integer,但给它存了Long才能存下的数,那就是内存溢出。内存溢出就是要求分配的内存超出了系统能分配的,系统分配不满足需求,就会产生out of memory。
2)举例:栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。
3)解决方案:
1.增加JVM内存大小,设置Xmx最大堆参数和Xms最小堆参数;
2.优化程序,释放垃圾,避免死循环,及时释放各种资源(内存、数据库的连接、防止一次载入太多数据)
(2)内存泄漏
1)定义:内存泄漏 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光。如果一个进程在运行过程中占用的内存无限制上升,该进程会有内存泄漏。
2)内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。
3)内存泄露:指对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被使用。
4)解决方案:
1.注意集合类,例如HashMap,ArrayList,等等。因为它们是内存泄漏经常发生的地方。当它们被声明为静态时,它们的生命周期就同应用程序的生命周期一般长。
2.注意事件监听器和回调,如果一个监听器已经注册,但是当这个类不再被使用时却未被注销,就会发生内存泄漏。
3.“如果一个类管理它自己的内存,程序员应该对内存泄漏保持警惕。”很多时候当一个对象的成员变量指向其他对象时,不再使用时需要被置为null。
43、运行时数据区
运行时数据区 |
线程是否私有 |
作用 |
异常 |
程序计数器 |
线程私有 |
每个线程都有自己的程序计数器,是当前线程所执行的字节码的行号指示器。 |
无 |
虚拟机栈 |
线程私有 |
是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用户存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就是对应着一个栈帧在虚拟机栈中入栈和出栈的过程。 |
若线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;若虚拟机可以动态扩展,而扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常 |
本地方法栈 |
线程私有 |
为虚拟机使用本地(Native)方法服务 |
同虚拟机栈 |
Java堆 |
线程共享 |
在虚拟机启动时创建,该区域唯一目的是存放对象实例,几乎所有实例都是在这里分配内存;gc的主要区域 |
若堆中没有内存完成实例分配且堆无法再扩展时,将会抛出OutOfMemoryError异常。 |
方法区 |
线程共享 |
存已被VM加载的类信息、常量、静态变量、即时编译器后的代码等数据 |
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常 |
运行时常量池 |
线程共享 |
属于方法区;Class文件中的常量池用于存放编译期生成的各种字面量和符号引用,该内容将在类加载后进入方法区的运行时常量池中存放。 |
受方法区的限制,当常量池无法再申请到内存时,会抛出OutOFMemoryError异常。 |
44、对象创建过程
1)new类名;
2)根据new的参数在常量池中定位一个类的符号引用;
3)如果没有找到这个符号引用,说明类还没有被加载,则进行类的加载、验证、准备、解析和初始化;
4)虚拟机为对象分配内存;(规整的堆内存用指针碰撞(serial、parNew收集器);不规整的堆内存用空闲列表(CMS基于Mark-Sweep算法收集))
5)将分配的内存初始化为零值,对象头的设置;
6)调用对象的<init>方法。
45、对象头
(1)对象组成
1)Java对象三部分组成:对象头、实例字段和对齐填字段;
(2)对象头
1)Mark Word:用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向锁ID、偏向时间戳等)
2)类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针确定对象是哪个类的是咧;
(3)实例数据
是对象存储的真正有效信息,存储着自身定义的和从父类继承下来的实例字段,字段的存储顺序会受到JVM的分配策略和字段在Java源码中定义顺序的影响;
(4)访问方式
1)对象的访问方式分为:句柄访问和直接指针访问。
2)使用句柄访问,则Java堆中会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各种的具体地址信息;
3)使用直接指针访问,则Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息科,而reference中存储的直接就是对象地址。
46、类加载过程
(1)类加载过程
类加载过程分为:加载——验证——准备——解析——初始化
加载 |
加载又分为三个阶段:(通果类全限定名获二进制字节流——字节流转化成方法区运行时数据结构——在堆中创建对象作为方法区中类的数据访问的入口) (1)通过一个类的全限定名来获取定义此类的二进制字节流; (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 |
验证 |
确保Class文件的字节流中包含的信息符合当前虚拟机的要求 分为:文件格式验证、元数据验证、字节码验证、符号引用验证 (1)文件格式验证:保证字节流是符合Class文件的规范,能被当前VM处理,从而保证字节流能够正确解析进入方法区; (2)元数据验证:对元数据信息进行语义分析;如验证final类型的类有没有子类,方法有没有被覆盖; (3)字节码验证:对类的方法体进行校验,对数据流和控制流分析;如检查每个操作码是否合法; (4)符号引用验证:对类自身以外的信息进行匹配性校验,保证解析阶段能正常进行;如调用其他类的方法时,查看是否有这个方法。 |
准备 |
正式为类变量分配内存并设定初始值(数据的零值),内存是在方法区中分配。(不是实例变量) |
解析 |
将常量池内的符号引用替换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析。 |
初始化 |
真正开始执行类的定义的Java程序代码(字节码),实际上是执行类的构造器<clinit>()方法的过程。(类变量的赋值以及静态语句块) |
(2)类加载器及双亲委派模型
启动类加载器——扩展类加载器——应用程序加载器(系统加载器)
工作过程:
1)若一个类加载器收到类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成;
2)每层次的类加载器都是按照1)方式去完成,所以所有的家在请求都传送给顶层的启动类加载器中;
3)当父加载器反馈自己无法完成这个加载请求(搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载,调用findClass()方法。
好处:
Java类随着它的类加载器一起具备一种带有优先级的层次关系,提高软件系统的安全性,用户自定义的类加载器不可能加载应该由父加载器的可靠类,防止恶意的代码代替父类加载器加载的可靠代码。如类java.lang.Object,存放在rt.jar中,无论哪个类加载器去加载这个类,最终都是委派给双亲委派模型顶层的启动类加载器去加载,所以Object类在程序汇的各种类加载环境中都是同一个类;若没有双亲委派模型,由各个类加载器自行去加载,若用户自己编写了一个称为java.lang.Object的类,并存放在了程序的ClassPath中,则系统中就会出现多个不同的Object类。
47、GC垃圾收集
(1)概述
先判断对象是否存活: |
引用计数法(对象之间相互循环引用问题)以及可达性分析 |
gc算法: |
“标记-清除”,复制,”标记-整理“,分代算法。 |
gc器: |
Serial收集器,Serial Old收集器,ParNew收集器,Parallel Scavenge收集器,Parallel Old收集器,CMS收集器,G1收集器 |
内存分配与回收策略: |
对象优先在Eden分配;大对象直接进入老年代;长期存活的对象将进入老年代;动态对象年龄判定;空间分配担保 |
(2)可达性分析算法
1)思想:
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象是不可用的。
2)GC Roots的对象(类似于方法区中存储:类信息、常量、静态变量、即时编译器编译后的代码)
- 虚拟机栈(栈帧中本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象;
(3)引用
强引用、软引用、弱引用、虚引用
强引用 |
程序代码之中普遍存在的,如Object obj = new Object()这类引用,垃圾收集器永远不会回收掉强引用关联的对象。 |
软引用 |
一些有用但并非必需的对象。(看当前内存是否足够,如果足够,第二次回收时,不回收软引用关联的对象)在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。应用:经常使用的对象。 |
弱引用 |
描述非必需对象。弱引用关联的对象只能生存到下一次垃圾收集发生之前。(GC工作时,无论当前内存是否足够,都会回收弱引用关联的对象)应用:不经常使用的对象。 |
虚引用 |
称为幽灵引用或者幻影引用。最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的实例。唯一目的是能在这个对象被收集器回收时收到一个系统通知。 |
(4)GC算法
算法 |
特点 |
标记-清除 |
过程:分为“标记”和“清除”两个阶段: (1)首先标记出所需要回收的对象(引用计数法和可达性分析,两次标记过程); (2)在标记完成后统一回收所有被标记的对象。 缺点两个: (1)效率问题:标记和清除两个过程的效率不高; (2)空间问题:标记清除后会产生大量不连续的内存碎片,导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 |
复制 |
过程:可以解决效率问题,将可用的内存按容量划分为大小相等的两块。 (1)每次只使用其中的一块; (2)当这一块用完了,就将还存活的对象复制到另一块上; (3)然后再把已使用的内存空间清理掉。 优点:每次对整个半区进行内存回收,避免内存碎片问题,只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 缺点:将内存缩小为原来的一半,代价高;当对象存活率较高时需要进行较多的复制操作,效率降低。 应用:回收新生代,新生代中分为Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。默认Eden:Survivor=8:1,Survivor不够时,老年代内存分配担保。 |
标记-整理 |
过程: (1)首先标记处所需要回收的对象; (2)不直接对可回收对象进行清理,让所有存活的对象都向一端移动; (3)直接清理掉端边界以外的内存。 优点:改进了复制算法在对象存活率较高时带来的效率问题; 应用:老年代收集(对象存活率较高) |
分代收集 |
过程:根据对象存活周期的不同将内存划分为新生代和老年代,根据各自的特点采用合适的收集算法。 (1)新生代中,每次垃圾收集时都发现有大批对象死去,少量存活,选用复制算法; (2)老年代中,对象存活率高、没有额外空间进行分配担保,使用“标记-清理”或者“标记-整理”。 |
(5)GC垃圾收集器
GC器 |
介绍 |
优缺点 |
应用场景 |
示意图 |
Serial收集器 |
Serial是单线程收集器,在GC时,必须暂停其他所有的工作线程,直至收集结束; |
优点:简单而高效;对于限定单个CPU环境,Serial收集器没有线程交互开销,效率很高。 缺点:stop the world给用户带来不良体验,如计算机每运行一段时间就暂停下来响应几分钟处理垃圾收收集。 |
client模式下的默认新生代收集器。 |
Serial/Serial Old收集器
|
ParNew收集器 |
ParNew是Serial的多线程版本(GC线程的多线程) |
优点:除了Serial收集器外,只有ParNew能够与CMS收集器配合工作。 |
运行在Server模式下的VM首选新生代收集器。 |
ParNew/Serial Old收集器运行示意图
|
Parallel Scavenge收集器 |
Parallel Scavenge是使用复制算法的新生代收集器,并行的多线程收集器,关注吞吐量。 |
优点:更关注吞吐量,即吞吐量 缺点:牺牲停顿时间,交互性差; |
适用于后台运算而不需要太多的交互的任务。 |
Parallel Scavenge/ Parallel Old收集器运行示意图
|
Serial Old收集器 |
Serial Old是Serial收集器的老年代版本的单线程收集器,使用“标记-整理”算法。 |
单线程高效而简单 |
1)主要给Client模式下的VM使用。 2)若在Server模式下用,两大用途:1.在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;2.作为CMS收集器备选,并在Concurrent Mode Failure时使用。 |
Serial/Serial Old收集器运行示意图
|
Parallel Old收集器 |
Parallel Old是Parallel Scavenge的老年代版本的多线程收集器,使用“标记-整理”算法,jdk1.6开始提供。 |
优点:关注吞吐量以及CPU资源敏感 |
注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge + Parallel Old收集器。适合吞吐量优先。 |
Parallel Scavenge/ Parallel Old收集器运行示意图
|
CMS收集器 |
1)CMS是一种获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法。 2)4个步骤:初始标记、并发标记、重新标记、并发清除。 |
优点:并发收集;低停顿。 缺点:1.对CPU资源敏感;2.无法处理浮动垃圾;3.收集后产生大量空间碎片 |
在互联网站或者B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短,给用户带来较好的体验。 |
CMS收集器运行示意图
|
G1收集器 |
1)G1是面向服务端应用的垃圾收集器,为了代替JDK1.5中的CMS收集器,将整个Java堆划分为多个大小相等的独立区域,保留新/老年代概念,不是物理隔离,而是一部分Region的集合,所以不需要连续。化整为零的思路。 2)4个步骤:初始标记、并发标记、最终标记、筛选回收。 |
优点:1.并发与并行;2.分代收集;3.空间整合;4.可预测的停顿。
|
面向服务端应用。 |
G1收集器运行示意图
|
(6)内存分配策略:
对象优先在Eden分配;大对象直接进入老年代;长期存活的对象将进入老年代;动态对象年龄判定;空间分配担保。
1)对象优先在Eden分配
- 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,VM将发起一次Minor GC。
- 新生代总可用空间:Eden区 + 1个Survivor区的总容量。
新生代GC(Minor GC)和老年代GC(Major GC/ Full GC)
新生代GC:Minor GC指发生在新生代的垃圾收集动作,Java对象大多是朝生夕灭,Minor GC非常频繁,回收速度快。
老年代GC:Major GC指发生在老年代的GC,出现此GC,经常会伴随至少一次Minor GC,一般Major GC比Minor GC慢10倍以上。
2)大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象。如长字符串以及数组。
大对象问题:经常出现大对象会导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来存储大对象。
参数:-XX:PretenureSizeThreshold参数,(只对Serial和ParNew有效)可以使得大于该值的对象直接在老年代分配,目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法进行GC)。
3)长期存活的对象将进入老年代
思想:VM给每个对象定义一个对象年龄计数器:
- 若对象在Eden出生并经过第一次Minor GC后仍然存活,且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1;
- 对象在Survivor中每“熬过”一次Minor GC,年龄就增加1岁;
- 当年龄增加到一定程度(默认为15岁),将会晋升到老年代中;(老年代进入的阈值可以通过-XX:MaxTenuringThreshold设置)
4)动态对象年龄判定
若在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold的阈值。
5)空间分配担保(针对老年代是否有能力担保的)
- 在发生Minor GC前,VM会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若成立,则Minor GC可以确保安全;
- 若不成立,则VM查看HandlePromotionFailure设置值是否允许担保失败,若允许,则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试进行一次Minor GC,若小于,或参数设置为不允许担保失败则改为Full GC。
- JDK6 Update24之后的规则为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就进行Minor GC,否则进行Full GC。