Java知识整理

2019-04-14  本文已影响0人  四月Chen

[TOC]

第1章 Java基础

1.1 序列化

  Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或者转移这些二进制数组达到持久化的目的。要实现序列化,需要实现java.io.Serializable接口。
  反序列化是和序列化相反的过程,就是把二进制数组转化为对象的过程。在反序列化的时候,必须有原始类的模板才能将对象还原。

1.2 反射机制

1.2.1 定义

  JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

1.2.2 反射机制相关类

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

1.3 泛型

泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

1.3.1 泛型方法

public static <E> printArray(E[] array){
    for(E element:array){
        System.out.printf("%s",element);
    }
}

1.3.2 泛型类

public class Box<T>{
    privat T t;
    public void set(T t){
        this.t = t;
    }
    
    public T get(){
        return t;
    }
}

1.3.3 类型通配符

  1. 类型通配符一般使用?代替具体的类型参数。
  2. <? extends T>表示该通配符所代表的类型是 T 类型的子类。
  3. <? super T>表示该通配符所代表的类型是 T 类型的父类。

1.3.4 类型擦除

  Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List<Object>和 List<String>等类型,在编译之后都会变成 List。 JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。

1.4 自动装箱和拆箱

装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。

image

1.5 static关键字

  1. 静态变量
    对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
  2. 静态方法
  3. 静态代码块
  4. 静态内部类
    静态的内部类可以直接作为一个普通类来使用,而不需实例化外部类。
  5. 静态引入包
    然后在这个类中,就可以直接用方法名调用静态方法,而不必用ClassName.方法名 的方式来调用。

第2章 Java集合

2.1 面试题

2.1.1 HashMap和TreeMap的区别

2.1.2 HashSet和TreeSet的区别

2.1.3 队列和链表的区别

2.1.4 HashMap线程不安全表现

  1. put的时候导致的多线程数据不一致。
    这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
  2. resize死循环
    使用头插法,链表的顺序会翻转。

第3章 JVM

3.1 类加载机制

3.1.1 类加载过程

加载-->连接--初始化
加载-->验证-->准备-->解析-->初始化

  1. 加载
    Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。
  2. 验证
    确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  3. 准备
    为类的静态成员分配内存,并设置默认初始值。
  4. 解析
    将常量池中的符号引用替换为直接引用
  5. 初始化
    初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

3.1.2 类加载器

3.1.3 双亲委派

image
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父
类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候, 子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

3.2 JVM内存结构

image

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区
域【JAVA 堆、方法区】、直接内存。

3.2.1 程序计数器

是当前线程所执行的字节码的行号指示器

3.2.2 虚拟机栈

是描述 java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

3.2.3 本地方法栈

同虚拟机栈,Native方法

3.2.4 堆

对象和数组

3.2.5 方法区/永久代

存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。运行时常量池是方法区的一部分。

3.2.6 MetaSpace Java8元数据区

3.2.7 直接内存

NIO编程

3.3 运行时堆内存划分

3.4 垃圾回收

3.4.1 如何确定垃圾

  1. 引用计数法
  2. 可达性分析
    通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

3.4.2 GC算法

3.4.2.1 标记清除算法(Mark-Sweep)

分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重。

3.4.2.2 复制算法(copying)

按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。

3.4.2.3 标记整理算法(Mark-Compact)

标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

3.4.2.4 分代收集

3.4.3 垃圾收集器

3.4.3.1 Serial 垃圾收集器(单线程、 复制算法)

Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工
作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

3.4.3.2 ParNew 垃圾收集器(Serial+多线程)

Serial 收集器的多线程版本

3.4.3.3 Parallel Scavenge 收集器(多线程复制算法、高效)

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器, 它重点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

3.4.4 Serial Old 收集器(单线程标记整理算法 )

Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,
这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:

  1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
  2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。


    image

3.4.5 Parallel Old 收集器(多线程标记整理算法)

image

3.4.6 CMS 收集器(多线程标记清除算法)

一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
过程

1.初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
2.并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
3.重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对4.象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。

image

3.4.7 G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器, G1 收集器两个最突出的改进是:

  1. 基于标记-整理算法,不产生内存碎片。
  2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
    G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

3.5 JAVA 四中引用类型

3.5.1. 强引用

在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

3.5.2. 软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

3.5.3. 弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

3.5.4. 虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。

3.6 面试题

3.6.1 什么情况出发MinorGC

虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间。
1、如果大于的话,直接执行minorGC
2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
4、如果大于的话,执行minorGC

3.6.2 触发FullGC

  1. 老年代空间不足
    如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。
  2. 持久代空间不足
    如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
  3. YGC出现promotion failure
    promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.
  4. 统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
    在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行Full GC。
  5. 显示调用System.gc

3.6.3 内存泄漏

无用对象内存无法回收。

3.6.4 内存溢出

通俗的讲就是内存不够。

第4章 JAVA多线程并发

4.1 线程

4.1.1 什么是线程

操作系统调度的最小单元,拥有计数器、堆栈和局部变量等属性。

4.1.2 线程的状态

4.1.3 线程的基本方法

4.1.3.1. 线程等待(wait)

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后, 会释放对象的锁。因此, wait 方法一般用在同步方法或同步代码块中。

4.1.3.2. 线程睡眠(sleep)

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致
线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

4.1.13.3. 线程让步(yield)

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,
优先级高的线程有更大的可能性成功竞争得到 CPU 时间片, 但这又不是绝对的,有的操作系统对
线程优先级并不敏感。

4.1.10.4. 线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。 这
个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
    程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
    InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异
    常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止
    一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以
    根据 thread.isInterrupted()的值来优雅的终止线程。

4.1.3.5. Join 等待其他线程终止

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

4.1.10.6. 线程唤醒(notify)

Object 类中的 notify() 方法, 唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象
上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调
用其中一个 wait() 方法,在对象的监视器上等待, 直到当前的线程放弃此对象上的锁定,才能继
续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

4.2 wait和sleep的区别

  1. wait()是Object类方法,sleep()属于Thread类
  2. wait()线程会释放对象锁,wait()不会。
  3. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。调用wait()的线程需要其他线程调用notify()或notifyAll()唤醒。
  4. wait()方法必须放在同步控制方法和同步代码块中使用,sleep()方法则可以放在任何地方使用。

4.3 Java内存模型(JMM)

多线程通信方式有:共享内存和消息传递。Java并发采用的是共享内存模型。
围绕着在并发过程中如何处理原子性、可见性和有序性建立。


Java内存模型

4.3.1 指令重排序

4.3.2 顺序一致性

4.3.3 happens-before

4.3.4 内存屏障

4.4 volatile

Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。

4.5 Java锁

4.5.1 乐观锁和悲观锁

4.5.2 自旋锁

自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

4.5.3 公平锁和非公平锁

  1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
  2. Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁。

4.5.4 共享锁和独占锁

java 并发包提供的加锁模式分为独占锁和共享锁。

  1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
  2. java 的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

4.5.5 可重入锁

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。

4.5.6 死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
死锁产生的四个必要条件。

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

死锁的预防

1、有序资源访问
2、超时机制

4.6 synchronized

4.6.1 synchronized 作用范围

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是 Class实例
  3. 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

4.6.2 对象头

synchronized用的锁存在Java对象头中。mark word被分成两部分,lock word和标志位。

4.6.3 锁的升级

四种状态:无锁、偏向锁、轻量级锁、重量级锁。

  1. 偏向锁
    Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只判断对象头的Mark Word里是否存储着指向当前线程的偏向锁。
  2. 轻量级锁
    轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
  3. 重量级锁
    Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么
    Synchronized 效率低的原因。因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。

4.7 ReentantLock

第5章 MySQL

1.悲观锁和乐观锁
2.表级锁和行级锁

上一篇 下一篇

猜你喜欢

热点阅读