精选文章 JAVA面试(全)

JAVA面试(全)

作者:Mr_zhaozh 时间: 2020-07-28 01:38:46
Mr_zhaozh 2020-07-28 01:38:46

Java

八大基本数据类型

八大基本类型

Byte,short,long,int,double,float,boolean,char

占用大小及其长度

数据类型空间(字节B)取值范围
byte1-2^7 ~ 2^7-1
short2-2^15~ 2^15-1
char20 ~ 2^16-1 char无需符号位
int4-2^31 ~ 2^31-1
float4-2^31 ~ 2^31-1
long8-2^63 ~ 2^63-1
double8-2^63 ~ 2^63-1

自动装箱/拆箱

Java将原始数据类型转换为包装类成为自动装箱

包装类转换为基本数据类型为自动拆箱

为何需要包装类

Java是面向对象语言,基本数据类型不具有对象的特征,凡是需要传递Object类型的参数时就无法使用了,例如在使用集合类时,向集合类中添加元素都默认为Object,基本数据类型装不进去的。

Integer中的缓存池

  • Integer中有一个静态内部类,它在创建时初始化了-128~127在缓存数组中。只有当自动装箱时会从缓存中获取,创建对象不会!

  • -128~127是比较常用的一些数字,缓存后可以加快自动装箱的速度减少堆内存的占用,也算是一种单例模式。

  • 整数类型都有缓存机制,Byte,Short,Long,Character都有范围也都是-127~128

面向对象

三大特性

封装、继承、多态

继承

  • Java支持单继承,带final修饰的类不可继承

  • 子类继承了父类的所有非private的属性和方法

  • 子类可重写父类中非private和final修饰的方法

  • 如果父类中只有带参构造,子类也需要有带参构造。

向上/下转型

子类向父类的转换成为向上转型(父类指针指向子类对象)

父类向子类的转换成为向下转型(强转,可以先用instanceOf验证)

重写和重载

  • 重写@Override指的是子类对父类允许访问的方法进行重新编写,方法名、参数、返回值保持不变

  • 重载@Overload指的是在同一个类中相同方法名和返回值但是参数不同的方法。

多态

多态的三个必要条件,继承、重写、父类引用指向子类对象

抽象

  • abstract关键字可以修饰方法和类。如果一个方法被声明为抽象,那么该类也必须声明为抽象类

  • 抽象类不能被实例化,必须得继承使用。

  • 抽象类中的抽象方法只能给出方法的声明,不能给出具体的实现。

  • 抽象类的子类必须实现父类中的抽象方法,否则也得声明为抽象类。

多线程

ThreadLocal

概述

ThreadLocal是线程本地存储变量的工具类,它是每个线程私有的,与其他线程互不共享,可以用在存储一些变量的传递,在函数调用时可以解耦减少传参。

原理

在每个Thread中都有一个局部变量ThreadLocalMap,它本质上就是一个哈希表。每个ThreadLocal类就是key,而value是我们自定义的。所以一个线程中可以有多个的ThreadLocal,每个ThreadLocal对应一个value。

set操作,就是向线程的ThreadLocalMap中以自身为Key添加节点。

get操作,就是自身为Key向线程中的ThreadLocalMap获取值。

内存泄漏问题

需要注意的是ThreadLocalMap的key是一个弱引用,它会在失去强引用后的下一次GC被回收,那么就会产生[null,value]的情况,如果线程执行完那么Map也失去强引用被回收,没毛病。如果是线程池呢?Map中的这个节点就可能一直存在,越积累越多导致内存泄漏。

解决:

  • 最好是在使用完ThreadLocal后调用remove移除Map中的节点

  • 如果没有remove的话,在ThreadLocalMap的get和set中都会进行清理key=null的脏节点,但是它不保证能一定清除。

为什么Key使用弱引用?

因为使用强引用的话存入Map中将永远不会被自动回收,每次必须显示remove,这样出现内存泄漏的风险更大

为什么Value不使用弱引用?

因为value通常加入后外部就没有强引用了,在GC后value的值就会消失

可见性问题

概念: 多线程中可见性问题指的是,一个线程对共享变量的修改对其他线程是不可见的,或者有滞后性。

原因:

  1. 在JMM中,所有线程的共享变量定义在主存中,每个线程都有一个私有的工作空间(抽象了CPU高速缓存,写缓冲区和寄存器等等协同操作的空间),那么在每个线程对共享变量进行更改时,在工作内存中有最新的数据,如果还没刷新回主内存,那么对其他线程就是不可见的。

  2. 指令重排序。在执行程序时,为了提高性能,编译器和处理器常常会做指令的重排序,它们是在保证单线程程序语义的前提下进行的重排序,如果是多线程就会出现可见性问题。如:线程A还没对b赋值,线程B就读取了变量b。

概念科普文:https://www.jianshu.com/p/36eedeb3f912

Happens-Before

理解: Happens-Before是多线程当中的一项约定,例如A Happens-Before B,就要求A操作中所有的修改必须对B线程可见,强调的是可见性,一种规则,而不限定实现方式,Java中有很多实现HB的例子,例如volatile关键字,对某个volatile修饰的字段的写操作对所有其他读取的线程可见。

Volatile

概述

  • volatile关键字用于修饰共享变量(没有使用final修饰的变量)

  • volatile又称轻量级锁,它可以保障可见性和有序性,并且不加排它锁,不会引起上下文切换。

  • volatile只能保证单个volatile变量读写的原子性(如:a++ 是复合操作a = a + 1,先读取共享变量a再+1再写回主存,分成了三个步骤,a可能在第一步就被人改了,所以不能保证。如果是a = b + 1,b是局部变量就可以保证原子性)。

关于可见性

volatile只能保证共享变量指向的相对最新值,如果是数组,那么只能保证数组的引用的最新,无法保证内部元素的可见性。

如何保障可见性和有序性?

使用内存屏障保证的可见性和有序性

写操作:

  1. 写操作开始之前,在写指令前插入StoreStore屏障防止之前的指令与其重排序,在指令之后插入StoreStore屏障防止下面的操作与其重排序。

    JAVA面试(全)1

     

    2.写操作完成之后,StoreLoad屏障,刷新处理器缓存,将该屏障之前的所有写操作同步到了主存中

    读操作:

    1. 开始读之前,在读指令后插入内存屏障,防止后序的写操作重排序到读之前。

    2. 读取volatile修饰的变量都需要从主存中重新读取最新值

    使用上和其他锁有什么不同?

    volatile只能修饰变量,只能用于实现单个共享变量的可见性,降低开销,如果是需要对一段代码加锁就无能为力了。

    Synchronized

    概述

    • Synchronized又称内部锁,是Java内部实现的互斥锁

    • Synchronized用于修饰方法或者代码段,当他修饰方法时锁定的是this,如果是静态变量或者静态方法则升级为全局锁

    • 判断Synchronized锁是否冲突,就是判断获取的锁对象是否一样

    原理

    在使用synchronized都需要传入一个对象与之关联,实际上这个对象提供的是对Monitor的入口(对象头的信息中有指向Monitor的地址),这也是为什么任意对象都可以作为锁的原因。

    monitor机制,实际上是对操作系统管程概念的实现,在monitor这个类中封装了实现同步互斥等操作的API,Java的Monitor类是由本地的C++代码实现。

    • Monitor中维护了锁的进入次数,实现可重入

    • Monitor中维护了两个队列,EntryList表示获取锁的线程,WaitSet存放wait状态的线程JAVA面试(全)2

     

    锁升级

    在JDK1.6对synchronized进行了优化,在性能上有了较大的提升。

    先看对象头的结构

    JAVA面试(全)3

     

    锁的升级路线:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

    偏向锁:

    在对象头有标志位表示该锁是否为偏向锁,并且记录持有偏向锁的线程ID。偏向锁不会主动释放锁,要么锁转移,要么升级为轻量级锁。

    轻量级锁:

    适用于,锁竞争不强,且线程持有锁的时间也不长的场景,因为CPU执行线程上下文切换,需要转内核态,开销大,轻量级锁也就是自旋锁,CAS忙等。

    重量级锁

    升级成熟悉的synchronized

    锁升级过程:

    1. 当第一个线程1获取锁对象时,进入偏向锁阶段,会在Java对象头中存入线程ID

    2. 当第二个线程来获取锁时,会判断是否为偏向锁,如果是则判断线程ID和新来的是否一致,一致则获取到锁;不一致则判断线程ID的线程是否存活,不存活则替换成新的线程ID,获取到锁;如果线程ID存活,升级为轻量级锁则进入第三步。

    3. 各个线程会在获取轻量级锁时,把锁对象的对象头复制一份到栈帧中并修改为与该线程对应,然后用CAS算法去替换锁的对象头,替换成功则为获取到锁。

    4. 当轻量级锁中自旋次数到达已经限度时,并且又有新的线程前来获取锁,那么轻量级锁就会升级成重量级锁,防止CPU空转。

    参考:https://www.cnblogs.com/duanxz/p/4967042.html

    wait方法必须放在while循环中

    在jdk源码的注释中提到,线程在等待时可能会出现欺骗性唤醒(线程在不被通知、中断或者自身睡眠超时的情况下自己醒来),醒来时判断条件可能还是不符合,那么使用while可以再次做判断。

    notify/NotifyAll

    Notify唤醒的是等待队列中的第一个,并非随机唤醒

    yield

    获取到内部锁后调用它,它不放弃锁,并主动让出CPU的执行,从运行态 ---> 就绪态,但是不保证CPU调度一定会切给别的线程。

    join

    让调用线程等待指定子线程执行结束后(或等待多少毫秒后)才能继续执行。

    原理:

    • join本身是一个Synchronized修饰的方法,

    • 如果没有指定等待时间会调用wait方法,使父线程进入子线程的等待队列中

    • 直到子线程死亡后会自动调用NotifyAll唤醒父线程执行

    interrupt

    修改指定线程的中断标志位,它可以中断处于sleep,wait阻塞下的线程,并抛出一个InterruptedException。

    Atomic常数类

    有AtomicInteger,AtomicLong,AtomicBoolean类,他们的内部实现原理类似。以AtomicInteger为例讲解。

    // 使用volatile修饰,保证可见性
    private volatile int value;
    public final int get() {
                // get无需加锁好理解
            return value;
    }
    public final void set(int newValue) {
                // 直接赋值,由volatile保证的原子性
            value = newValue;
    }
    // 实现线程安全的++
    public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
            return var5;
    }

    可能导致线程不安全的地方使用了CAS无锁机制实现。

    CAS

    compare and set

    基本思路:

    三个操作数:内存位置V,预期值A,修改为新值B。

    如果V的值为A则修改为新的值B,否则不会更新。

    经常和while循环配合使用,实现忙等的自旋锁。

    ABA问题:

    例如线程A将D的值改成了2再改成3再改成2,而对于线程B来说只看到了最后的2,并认为没有变化然后进行更新,这在某些情况下会导致问题。

    如何解决ABA问题?

    在JUC包的原子类中使用的是版本号来解决ABA问题,每次对数据的成功修改都会更新版本号,在比较时如果值相同那么还会比较版本号,如果版本号和数据版本号不一致则不执行修改。

    Atomic数组类

    有AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray三种,他们的原理类似,以AtomicIntegerArray为例。

    数组和常数的区别

    数组类型是引用类型!它的变量指针是指向内存地址中一块区域,所以数组是定长的,每个元素的读取是根据下标计算偏移量,所以对数组某个下标值的操作可以拆分成好几个指令,是线程不安全的操作!

    // 内部为一个定长的数组,并且它不会扩容,它仅仅是一个线程安全操作的数组!!
    private final int[] array;
    // 设置某个下标的值
    // 因为数组中每个数字都要独立的保证可见性和线程安全,而Java无法将数组中的每个元素加上volatile,因此
    // 需要调用本地的C++代码来安全的设置值
    public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
    }
    // 在get值时要保证可见性,同理需要本地C++实现拿到最新的值
    public final int get(int i) {
            return getRaw(checkedByteOffset(i));
    }
    private int getRaw(long offset) {
            return unsafe.getIntVolatile(array, offset);
    }
    // 给指定下标位置的值做加法操作
    public final int addAndGet(int i, int delta) {
            return getAndAdd(i, delta) + delta;
    }
    // 可以知道对数组的写操作还是使用CAS无锁操作完成的
    public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                // 读取对应下标的值
                var5 = this.getIntVolatile(var1, var2);
              // cas设置新值
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    ​
            return var5;
    }

    AQS

    数据结构

    本质上是一个双向链表实现的队列,它是严格的FIFO队列,在并发插入时以及修改共享变量时,使用自旋锁+CAS操作实现的线程安全,其中的阻塞和CAS操作调用的都是Unsafe类中提供的函数。

    内部类

    AQS中有两个子类,Node类为链表的节点类,ConditionObject实现了Condition类,内部也是维护了一个队列,存放不满足条件时等待的线程。

    实现锁机制

    有一个volatile修饰的局部变量state状态,状态的值就表示了当前AQS中的锁是否被持有,以及被持有几个(可重入)。

    AQS的父类

    它的父类AbstractOwnableSynchronizer简称AOS,比较简单,只维护了一个局部变量表示当前持有锁的线程是谁。

    ReentrantLock

    结构

    ReentrantLock实现了Lock接口,内部类继承了AQS。其中两个内部类分别实现了公平锁和非公平锁。

    加锁原理

    以公平锁为例,分为两种情况

    情况一:

    如果锁当前无占用,且队列中无等待,则直接使用CAS函数修改AQS中的状态值+1,然后设置当前线程为持有锁的线程,执行临界区代码。如果是重入锁的话直接令状态+1。

    情况二:

    如果当前已经有线程在排队,如果是重入锁则直接状态+1,否则创建一个节点,加入到AQS队列的末尾,并调用阻塞,等待轮到自己。

    期间还伴有一些其他代码,比如首次添加节点时对双向链表的初始化

    释放锁原理

    公平锁和非公平锁在释放锁上都一样。

    先是对AQS的state变量执行减操作,如果不为0则表示重入锁还未清零,函数结束。如果等于0表示,当前锁已经释放,将AOS中指向持有锁的线程指针置空;并在AQS中寻找下一个可被唤醒的节点,将其唤醒。

    公平锁和非公平锁实现上的区别?

    他们在获取锁的时候有区别,公平锁严格按照获取的顺序获取锁,而非公平锁它在获取锁时不会去判断队列中是否还有线程在等待,而是直接拿,没拿到再进入AQS中排队。

    ReentrantLock对比synchronized

    优势:

    1. 支持Condition,更加精准的条件控制,支持公平锁和非公平锁的配置。

    2. 支持查看当前锁的状态,等待线程的数量,查看某个线程是否在等待队列中

    劣势:

    1. 开发中代码更复杂

    2. 性能上synchronized可能在1.6优化以后不比ReentrantLock差


    Condition

    数据结构

    condition在AQS的内部类中有实现,ConditionObject中也维护一条队列,此队列与AQS互不干扰,存放的是由于不满足条件而阻塞的线程。

    await原理

    当线程调用await时,首先在Condition队列中加入此节点,然后释放此线程持有的锁(不管重入多少层),释放的过程同release,释放完毕后在Condition队列中阻塞自己。此时该节点已经不在AQS队列中了。

    signal原理

    signal执行时,会从Condition队列头开始寻找一个非取消状态的节点,然后将其移动到AQS队列中。

    CountDownLatch

    比较简单,使用的是共享锁实现的门栓功能

    1. 初始化时指定门栓数量,即AQS的状态值,类似于加入几个共享锁

    2. 每次await就判断state的值,如果等于0就直接返回函数,大于0进入AQS队列阻塞

    3. 每次countdown时,CAS去掉一个共享锁

    4. 当释放完毕后,会唤醒所有因为await在AQS中等待的线程

    Semaphore

    也是使用共享锁的思想

    1. 初始化时指定信号量数量,即为AQS状态值

    2. 每次acquire即为让状态值-1,使用CAS竞争,竞争到则返回,余量为0时则加入AQS等待

    3. 每次release即为让状态值+1,并唤醒AQS中所有等待的线程来争抢

    JUC集合

    CopyOnWriteArrayList

    概述:

    CopyOnWriteArrayList是ArrayList的线程安全类

    内部结构

    // 有一个数组来存储元素,它使用了volatile关键字,保证可见性和替换的原子性
    private transient volatile Object[] array;
    // 声明了一个ReentrantLock,保证操作安全使用的锁
    final transient ReentrantLock lock = new ReentrantLock();

    get

    无差别,读取数组下标

    add

    先获取ReentrantLock,接着调用Arrays.copyOf拷贝原数组创建新数组再加入新值。

    迭代器

    CopyOnWriteArrayList不支持fast-fail机制,在获取迭代器时,得到的是当前时刻数组的快照,如果被被其他线程修改了不会读取到脏数据也不会抛异常,也叫fail-safe;并且在迭代器中不支持修改操作,一旦调用会抛出不支持异常。

    其他修改操作同理,都会创建新的数组,效率非常低下,它的效率不比Collections创建的线程安全的ArrayList高

    CopyOnWriteArraySet

    概述:

    CopyOnWriteArraySet是线程安全的HashSet,说白了就是无重复的CopyOnWriteArrayList

    结构

    // 内部依赖CopyOnWriteArrayList实现,而非HashMap了
    private final CopyOnWriteArrayList al;

    add

    add操作只多了一个判断内部CopyOnWriteArrayList中是否已经有重复值的步骤,有重复不添加,无重复则同CopyOnWriteArrayList的添加步骤

    其余操作和CopyOnWriteArrayList同。它的效率也堪忧,因为使用CopyOnWriteArrayList实现的。

    ConcurrentHashMap

    概述:

    线程安全的HashMap,相比Hashtable它的效率更高

    JDK1.7中

    结构:

    JAVA面试(全)4

     

    Segment是一个内部类,它继承了ReentrantLock,可以实现的功能,在Segment中又维护了一个table[ ]也就是真正存放哈希表元素的地方,它也是基于Segment实现的分段锁功能。

    在并发操作时,将所有数据分成了多个区域,每个区域对应一个Segment锁,这样在高并发下可以支持并发修改多个区域的内容。

    put

    put操作需要经过两次哈希计算,第一次计算出所在的Segment,第二次计算在Segment中的位置,然后尝试获取锁,拿到锁后才能做插入操作。

    get

    同理需要两次哈希,先找到Segment,再找到槽位,接着遍历链表

    resize

    1.7的版本中无法对Segment[]进行扩容,只能对Segment中的槽位扩容,并且每次扩容是原来的两倍

    JDK1.8中

    结构: 结构大体同HashMap,只不过多了一些字段,1.8中不再使用Segment的分段锁,而是使用了Node[ ] + CAS + Synchronized实现。它锁的粒度更小,只需要锁住一个节点,并且将原先的链表改为了红黑树。

    put

    1. 首先进行哈希计算,对应到哈希槽位上

    2. 如果槽位正在扩容则进入协助扩容,完成后重新进循环;

    3. 如果槽位没有数据那么使用CAS无锁插入,如果插入失败则重新进入循环判断;

    4. 如果槽位有节点说明是哈希碰撞,那么将会使用synchronized锁住链表或者红黑树的头结点,在进行插入操作。

    5. 最后统计size,检查是否需要扩容

    get

    1. 计算哈希值,定位到table对应的位置

    2. 如果遇到扩容,则会调用标志正在扩容节点的ForwardingNode类的find方法,去新扩容的数组中找,匹配就返回

    3. 如果没扩容就在原位置找,先看头结点,没有再遍历

    多线程协助扩容

    有点复杂...

    对比1.7和1.8的变化

    数据结构上:1.8中不再使用Segment分段,而是和HashMap一样的数组,并且出现哈希碰撞后新增了红黑树结构

    线程安全上:不使用Segment分段锁机制,而是使用CAS和Synchronized保证线程安全,锁的粒度更小了

    阻塞队列

    ArrayBlockingQueue

    概述: 数组实现的线程安全的有界队列,创建时必须指定长度,它由可重入锁和条件实现,可以指定公平或者非公平锁

    结构:

    // 使用数组存储元素
    final Object[] items;
    // 指向队列头的指针
    int takeIndex;
    // 指向队列尾的指针
    int putIndex;
    // 队列中元素的数量
    int count;
    // 可重入锁保证线程安全
    final ReentrantLock lock;
    // 条件队列
    // 当队列无元素时尝试获取会进入notEmpty
    private final Condition notEmpty;
    // 当队列满时尝试添加元素会进入notFull
    private final Condition notFull;

    普通的队列操作和原先相同,只不过执行修改添加删除操作时加锁了。

    take

    获取锁,然后判断队列中是否有元素,如果没有则进入条件等待;如果有则直接获取,并唤醒notFull队列中的一个线程

    put

    获取锁,然后判断队列是否已满,如果满了则进入条件等待队列,如果未满则添加,添加完毕后唤醒notEmpty队列中的一个线程

    LinkedBlockingQueue

    概述: 是单向链表实现的阻塞队列,可以指定队列长度,如果不指定默认大小为int的最大长度

    结构:

    // 队列的最大长度
    private final int capacity;
    // 当前队列的长度
    private final AtomicInteger count = new AtomicInteger();
    // 头结点指针
    transient Node head;
    // 尾节点指针
    private transient Node last;
    // 出队列锁
    private final ReentrantLock takeLock = new ReentrantLock();
    private final Condition notEmpty = takeLock.newCondition();
    // 入队列锁
    private final ReentrantLock putLock = new ReentrantLock();
    private final Condition notFull = putLock.newCondition();
    // 因为是单向链表,所以为严格的FIFO操作,那么入队列一定在尾部,出队列一定在头部,所以两把锁的并发性能会更好

    LinkedBlockingDueue

    概述: 是双向链表实现的阻塞队列,可以指定队列长度,如果不指定默认大小为int的最大长度

    原理: 类似于ArrayBlockingQueue,一把锁控制出入队列操作,两个条件变量

    总结:

    JUC包中的阻塞队列大致分为两类:数组实现和链表实现,数组实现的必须指定长度,链表实现的如果没指定的话默认为int的最大值,需要防止内存溢出。实现上只有单向链表使用了两把锁,其他的类似

    JUC链表

    ConcurrentLinkedQueue

    概述: 单向链表实现的队列,使用CAS函数实现的线程安全

    结构:

    // 注意头结点和尾部节点都是使用volatile修饰的,保证可见性
    private transient volatile Node head;
    private transient volatile Node tail;

    原理:

    所有对链表的修改操作都是使用CAS函数 + while自旋实现的线程安全

    ConcurrentLinkedDueue

    同ConcurrentLinkedQueue

    线程池

    概述

    Java中常见线程池有三种:

    ThreadPoolExecutor我们最常用的线程池

    ScheduledThreadPoolExecutor实现任务的延期执行和周期执行

    ForkJoinPool JDK1.7新增的线程池

    ThreadPoolExecutor

    创建方式:

    • 调用构造,指定所有参数。

    • 使用Executors工具创建。

    核心参数

    //线程池的基本大小
    int corePoolSize
    //线程池中允许的最大线程数
    int maximumPoolSize
    //最大线程空闲时间
    long keepAliveTime
    // 任务的阻塞队列
    BlockingQueue workQueue
    // 任务拒绝策略
    RejectedExecutionHandler handler

    设当前线程池中线程数为size

    • 当size < coreSize时,新任务到达直接创建新线程处理

    • 当size = coreSize时,新任务会被添加进BlockingQueue

    • 当BlockingQueue数量到达上限时,且 maxPoolSize > size > coreSize,则继续创建线程

    • 当BlockingQueue数量到达上限时,且size = maxPoolSize 时,则阻塞队列满,触发任务拒绝策略

    • keepAliveTime是在size超过coreSize后,线程的最大空闲时间,如果空闲时间超过他则会销毁线程,直到size = coreSize。

    任务拒绝策略

    • AbortPolicy:默认策略,触发时直接丢弃任务,并抛出异常

    • DiscardPolicy:触发时丢弃任务,不抛异常

    • DiscardOldestPolicy:触发时丢弃队列最前面的任务,然后重新提交被拒绝的任务

    • CallerRunsPolicy:触发时由调用线程处理该任务

    • 也可以自定义拒绝策略

    两种任务提交方式

    execute(Runnable run)和submit(Callable call)的区别,execute负责提交不需要返回值的任务,submit提交的是有返回值的任务。submit提交后回返回Future对象来获取返回值,注意如果线程还没执行完获取返回值函数将会阻塞!

    两种关闭方式

    shutdown():线程池不再接收新任务,同时等待线程池中剩余的任务完成后关闭线程池

    shutdownNow():线程池不再接收新任务,同时立马关闭线程池,线程池里正在执行的也会被终止

    为什么要使用线程池?

    • 提高效率,频繁的创建销毁线程花销大

    • 方便管理,可以获得执行的状态,防止线程创建过多导致的系统崩溃

    线程池有哪几种阻塞队列?

    1. ArrayBlockingQueue,数组实现的有界队列

    2. LinkedBlockingQueue,单向链表实现的队列,最好指定长度否则可能导致内存溢出,它的吞吐量要高于ArrayBlockingQueue

    3. SynchronousQueue,一个不存储元素的阻塞队列,它所有的插入和获取操作都会被阻塞,直到有一个相对应的操作发生

    4. PriorityBlockingQueue,具有优先级的无界阻塞队列,它可以自己实现一个优先级的比较器,队列按照比较器规则排序,并且是无边界的,需要小心内存溢出。

    线程池的配置思路

    • CPU密集型任务,一般设置为CPU核心数+1,因为CPU密集型任务下CPU使用率很高,过多的线程只会增加上下文切换的开销。

    • IO密集型任务,一般设置为CPU的2N+1,因为IO密集型任务CPU经常要等待IO,CPU使用率并不高,因此可以做更多的上下文切换。

     

    IO

    编码

    计算机只认识0和1,那么需要转换成字母数字就需要将二进制码按照规定的字典转换成其他字符,因此需要各种编码

    字符集和字符编码

    • 字符集是包含所有字符和其对应的二进制码的集合。如ASCII、Unicode

    • 字符编码是一种算法,它用于将存储的编码利用算法转换成对应的二进制码,通过字符集获取到对应的字符。

    例如Unicode下的UTF-8,UTF-16等,Unicode本身为了装下全世界的字符,它要求每个字符占用字节比较大,UTF-8它就是为了在存储时节省空间的编码方式,它是一种可变长的编码。

    ASCII

    最早计算机使用的是ASCII码,每个字符占用1B,也就是8bit,因此ASCII最多表示2 ^ 8 = 256个字符。

    Unicode

    为了兼容多国家的语言字符Unicode产生了,并且码表和ASCII兼容,Unicode通常使用2B表示字符,所以它可以有2 ^ 16 = 65536个字符,但是生僻字复杂字符需要4B。

    UTF-8

    由此可以看出使用Unicode编码后比ASCII多占用1倍的空间。UTF-8就是为此而生,它是一种可变长编码,它可以把Unicode字符根据所需的空间编码成不同的大小,如字母被编码成1字节,汉字3字节等等,由此节省空间。

    总结

    计算机中统一使用Unicode编码,当需要保存或者传输时可以转换为UTF-8编码以节省空间。UTF-8可以说是Unicode的一种实现。

    流的概念

    Java中有字节流和字符流

    字节流:

    以字节单位进行传输,通常用来处理二进制数据,如图片、音乐等等。

    字符流

    以字符为单位进行传输,Java中使用Unicode编码,适合用于操作文本数据

    BIO

    概念

    同步阻塞IO,Java最初实现的IO方式,进程在发起一个IO操作后,必须等待IO操作全部完成后,才能继续执行。

    使用场景

    适合用于连接数少,但是每个连接都需要发送大量数据的情况

    节点流

    文件流

    FileInputStream,FileOutputStrean,FileReader,FileWriter。它们都会直接操作文件。直接对文本进行追加。RandomAccessFile可以实现对文件随机位置的读写

    数组流

    ByteArrayInputStreamByteArrayOutputStream,CharArrayReader,CharArrayWriter。将流中的内容读入数组中,然后进行处理。

    管道流

    PipedInputStream,PipedOutputStream,PipedReader,PipedWrite类似于Linux中的管道,它可以实现线程之间的数据传输

    处理流

    缓冲流

    BufferedImputStrean,BufferedOutputStream,BufferedReader ,BufferedWriter。为节点流增加缓冲的功能,可以避免频繁的读写硬盘,将经常修改的数据存在缓冲区中,使用flush方法刷新进硬盘。

    转换流

    InputStreamReader,OutputStreamWriter。将字节流向字符流转换,当我们在键盘输入或者网络通信的时候需要用到他们,可以进行编码的转换。

    NIO

    概念

    同步非阻塞IO,JDK1.4发布的,线程向操作系统发起IO操作后立即返回不会阻塞,可以做其他的处理,事后需要时不时的询问IO操作是否就绪,再做处理。

    BIO对比NIO

    • Java中BIO是面向流的,NIO是面向缓冲区操作,BIO的缓存区在堆内,而NIO的缓冲区用的是堆外内存

    • BIO流是阻塞的,NIO是非阻塞的;BIO一个线程只能处理一个请求,而NIO的选择器可以让一个线程监控多个连接。

    • BIO是单向的,NIO是双向的

    使用场景

    适合使用与有大量连接,但是只有少量数据的情况。

    几个核心组件

    Buffers,NIO中所有的数据读写都发生在缓冲区

    Channel,有点像流,它可以从缓存区中读写数据,然后进行传输。JAVA面试(全)5

     

     

    Selector,多路复用器,它实现了IO多路复用,向选择器注册Channel后就可以批量监控处理了

    JAVA面试(全)6

    AIO

    概念

    异步非阻塞IO,线程向操作系统发起IO操作后立即返回不阻塞,内核IO操作完成后会通知应用程序,调用IO完成后的回调函数

    IO多路复用

    概念

    程序向操作系统注册socket文件描述符,表示我要监控他们是否有IO事件发送,上册应用程序可以随时的获取已准备好的socket。这就实现了程序使用少量的线程监控大量的连接,它更适用于大量连接中需要等待的情况,如果全部都是活跃连接,就会退化成并行处理。

    实现

    在Linux中使用的是epoll()系统调用实现的IO多路复用,在他之前有poll()和select()这两种实现方式,他们的缺点很明显:

    1. 每次向操作系统索取结果时,需要遍历所有的文件描述符寻找已经就绪的,次次需要从内核态拷贝到用户态

    2. 无状态的。每次检查完下次检查,仍然需要向操作系统提交这些需要检查的socket描述符。

    在epoll的实现中就方便很多了,它在内核态中创建一张表格,存放所有提交去注册的socket描述符,当网卡准备就绪或者有数据到来时,会产生硬件中断,OS会触发中断回调函数,将它对应的文件描述符移动到就绪链表中,应用程序只需要每次通过指向表格的文件描述符去获取已就绪队列的即可。

    结合NIO使用

    多路复用 + NIO 是目前很多高性能框架的选择,每次使用NIO定时的去轮询复用器获取已就绪的socket处理,避免了阻塞的上下文切换。也无需大量线程去监控每一个socket

     

    类加载机制

    类加载器的作用

    类加载器就是加载.class文件,将它读入内存验证、解析,并为他在中创建Class字节码对象。此对象是全局唯一的

    类加载的过程

    JAVA面试(全)7

     

    加载:

    通过类的全限定名(包名+类名)加载class文件的二进制字节流在堆中创建java.lang.Class对象,它是方法区中该类的数据访问入口。

    验证:

    验证class文件中的信息是否符合当前虚拟机的要求

    1. 文件格式验证,验证Class文件是否符合格式规范,并且是否能被当前版本虚拟机处理。(版本号是否符合当前虚拟机处理版本、常量类型是否支持)

    2. 元数据验证,验证代码语义是否符合Java语言的规范要求(是否有父类、是否继承了不允许被继承的类、是否实现接口方法等)

    3. 字节码验证,验证方法体中代码语义是合法的。(类型转换、操作数类型变更)

    4. 符号引用验证,对类自身以外的数据调用的验证。(判断访问的字段方法是否可以被访问如被private修饰、判断访问的字段方法是否存在)

    准备

    为类中的static变量在方法区分配内存并设置初始值(真正赋值在初始化)

    解析

    将Class二进制流中常量池的符号引用替换为直接引用。(Class文件中的常量池用符号描述所引用的目标,替换成直接引用类似指针或者偏移量这种能够直接定位到目标的变量)

    初始化

    1. 如果有继承父类则先初始化父类

    2. 初始化静态成员变量的值(真正赋值),执行static代码块

    3. 初始化成员变量的值,执行普通代码块

    4. 执行方法的空参构造

    几种类加载器

    JAVA面试(全)8

     

    Bootstrap ClassLoader引导类加载器

    最顶层的加载器,由C++实现,主要加载Java的核心类库,加载 %JAVA_HOME%/lib 路径下的核心类库,或者-X bootclasspath参数指定路径下的jar包。如Integer、String、System等。

    Extension ClassLoader扩展类加载器

    它是Java语言实现的,负责加载%JAVA_HOME%/lib/ext下的一些类。

    Application ClassLoader系统类加载器

    它负责加载classpath下,也就是工程目录下我们自己开发的Java类。

    它们在本质上无区别,只是负责加载的目录不同。

    双亲委派模型

     

    1. 双亲委派模型要求当需要加载一个类时,此时的加载器不会自己加载,而是递归委托给上层的父类加载器,最终到达最顶层的加载器。

    2. 引导类加载器父类加载器先检查缓存中是否已经加载类,如果有则直接返回,如果没有检查是否为自己负责的路径下的类,如果无法处理再返回给子加载器处理。

    3. 扩展类加载器同理,先查缓存,再检查路径,无法处理再向下返回。

    4. 到达应用类加载器,同理。如果都无法处理,则抛出异常。

    优点:

    1. 避免重复加载,加载类的请求到来后会从缓存中检查。

    2. 提高安全性,可以避免Java的核心类库被篡改和替换

    弊端:

    如果是上层的加载器加载类时,它无法委派到下层的加载器处理,例如Java的SPI机制,以JDBC为例,在JVM启动时会扫描实现类并初始化实例,但是第三方驱动实现的路径在classpath下,顶层的加载器无法加载它。

    突破双亲委派模型

    接上文的加载JDBC驱动,在老版本中需要使用反射加载启动,但是新版当中,在静态代码块中实现了驱动的注册,也就是说在类加载时就已经注册了驱动,那么它的实现就是突破了双亲委派模型。

    1. Java在Thread初始化时加入了ContextClassLoader,上下文类加载器,默认为ApplicationClassLoader,因此在上层中获取它就可以调用下层加载器加载类了

    2. 另一种破坏方式就是自定义ClassLoader,然后重写loadClass方法,里面可以指定是否委派给父类执行。在JDK1.2以后不建议去重写loadClass方法了,而是重写findClass方法,findClass方法在loadClass中如果父类无法处理则调用findClass处理,这样既能实现自定义类加载器又符合双亲委派模型。

    为什么要破坏双亲委派模型

    1. 当前的ClassLoader无法加载目标类时就需要了

    2. 可以实现对自定义类的读取,实现对加密后的class文件的解码读取。

    3. 可以实现类的热部署,需要重写类加载器的查重机制。

    4. 可以实现探针技术,在类加载器的加载阶段,利用JVMTI(JVM工具包)对字节码进行修改,可以实现对指定方法或者接口添加额外的操作(打印日志、打印执行时间、获取参数结果等等)

    5. 可以实现类的隔离,例如Tomcat中不同的webapp,它使用独立的类加载器加载,即便出现同名的类也可以共存。

    我们能否自己写一个java.lang.Integer类?

    先说结论,肯定不行。

    1. 如果我们自定义一个java.lang.Integer类,并在其中写一个Main方法,点击运行,按照双亲委派模型,在Bootstrap ClassLoader中返回了已经加载好的Integer类,其中并没有Main方法,因而提示方法不存在。

    2. 如果我们用自定义的ClassLoader去加载它呢?会报出安全异常,java开头的包名是受保护的无法加载,这个保护的实现定义在我们自定义类加载器的父类中,无法改变。即便是打包到BootStrap ClassLoader的加载路径下也无法被加载,这个应该有另外的验证机制。

    判断两个类是否相同

    比较两个类的全限定名和类加载器是否相同

    类的卸载

    首先JVM虚拟机自带的BootStrap ClassLoader、ExtClassLoader、AppClassLoader装载的类在整个虚拟机生命周期中是不会被卸载的。只有用户自定义的类加载器装载的类才可以被卸载。

    JAVA面试(全)9

     

     

    Class对象和类加载器总是双向关联的,Class对象又和方法区中的数据结构相关联。

    当图中左侧三个引用都置空后,GC会回收堆和方法区的所有空间。

    1. 所有Class对应的对象都被回收

    2. ClassLoader也被回收

    3. Class对象已经失去引用

    满足这三个条件才可以卸载方法区中的元数据

    JDK1.8中类卸载

    永久代已经被转移到物理内存上了,并为每一个类加载器绑定各自的内存空间。

    [参考]  https://www.cnblogs.com/blueskyli/p/8589870.html 

     

    反射

    概述

    • 反射可以程序运行时期构造任何一个类对象

    • 获取类当中具有的成员变量和方法

    • 调用对象方法和对变量的赋值

    原理

    使用反射需要先获取到这个类的字节码对象,如果是未加载的类则通过全限定类名使用类加载器加载进内存,然后通过内存中的Class对象进行相应的操作。

    使用步骤

    1. 获取字节码对象

    2. 可以读取字节码对象中的数据了,如方法名、变量名、类型等等。

    3. 可以实例化出对象,然后执行其中方法或者属性的赋值。

    应用场景

    框架中使用非常广泛,早期的注册JDBC驱动,Spring当中的IOC,SpringMVC中的参数封装等等

    反射机制优缺点

    优点: 非常灵活,可以在运行期间做各种操作

    缺点: 性能不太好,代码比较繁杂

    SPI机制

    Java的SPI机制全称Service Provider Interface,是Java提供的一套动态加载机制,比如Java定义的JDBC驱动类的标准接口,MySQL、Oracle根据标准接口实现自己的驱动,然后再预定的路径下存放配置文件,Java启动时就会加载这些具体的代码实现。

    JVM

    JAVA面试(全)10

    程序计数器

    程序执行时会将代码转换为指令,在一个时刻每个处理器核心都只会执行一条指令,一条线程的执行流当中会有很多指令,为了线程上下文切换时能恢复到正确的执行位置,每个线程中都有私有的程序计数器。

    注意:JVM中的程序计数器只能记录执行Java方法的指令,如果是本地方法栈则不使用(用别的)。

    虚拟机栈

    • 虚拟机栈是线程私有的,与线程的生命周期相同。

    • 虚拟机栈中存放栈帧,每一次函数调用都会有对应的栈帧入栈,函数调用结束则栈帧出栈。栈帧的大小在编译期就确定了,不受运行期数据的影响。

    栈帧中包含:局部变量表、操作数栈、动态链接、方法返回地址。

    局部变量表:用于存放方法的参数局部变量,基本数据类型会直接存值,引用数据类型存放的是指向堆中的指针。小对象并且使用范围在该方法内可能会有栈上分配

    操作数栈:保存方法执行中算术计算过程的中间结果;调用其他方法传递参数,概念模型中两个栈帧是相互独立的,但是有的JVM会进行优化,令两个栈帧有一部分重叠,共享一部分的操作数栈内存,从而在方法调用时无需额外的复制传递。

    动态链接: 保存了该栈帧对应的方法方法区指针;在该方法运行时访问其他方法或者另一个类的成员变量时,将元数据的符号引用解析转换成直接引用的过程,叫做动态链接(运行时)。

    方法返回地址

    本地方法栈

    JAVA面试(全)11

    本地方法栈和虚拟机栈的作用是类似的,只不过虚拟机栈执行的是Java方法,本地方法栈是为调用C或者C++实现的代码时使用的栈结构。

     

    • 堆是所有线程共享的一块内存区域,用于存放对象实例,几乎所有的对象都在堆中分配内存。

    • 堆是垃圾收集器的主要管理区域,因此也被称为GC堆。

    • 堆可以分成年轻代和老年代,年轻代又有Eden区和survivor区

    方法区

    • 方法区是一种规范,它也是所有线程共享的一块内存区域,在JDK的发展中实现上有所变化,1.7以前叫永久代,1.7以后改成了元空间JAVA面试(全)12

     

    类型信息:包含类的完整名称、直接父类的名称、类的修饰符、类实现接口的有序列表

    运行时常量池: 每个类都有各自的运行时常量池,它保存字面值:像基本数据类型、final修饰的变量(在编译期能确定的值);符号引用:对引用类型或者方法的符号引用(动态链接,在运行时将符号引用解析转换成直接引用的过程)。

    字段信息: 每个字段的修饰符、字段类型、名称

    方法信息: 方法名、返回值、参数类型、修饰符等等

    静态变量:也叫类变量

    对类加载器的引用:保存加载这个类的类加载器的引用

    对Class类的引用:保存堆上对应Class类的引用

    方法表: 存放类中的方法信息以及对应的方法代码(编译后的字节码)指针。方法表是实现动态调用的核心。(具体见多态原理)

    多态的原理/动态调用原理

    Java中方法调用有两类,静态方法调用和动态方法调用。

    • 静态方法调用是对类中的静态方法(static、私有方法、构造)的调用方式,在编译期就已经确定了的。

    • 动态方法调用需要有方法调用的对象(多态),是动态绑定的。

    方法表中存放了一个类中的方法继承的父类方法的映射,如果没重写父类方法则指向父类的代码,如果重写了则指向子类的代码。

    例子:

    class Person {
        public String toString() {
            return "I'm a person.";
        }
     
        public void eat() {
        }
     
        public void speak() {
        }
     
    }
     
    class Boy extends Person {
        public String toString() {
            return "I'm a boy";
        }
     
        public void speak() {
        }
     
        public void fight() {
        }
    }
     
    class Girl extends Person {
        public String toString() {
            return "I'm a girl";
        }
     
        public void speak() {
          System.out.println("Girl speak");
        }
     
        public void sing() {
        }
    }

    JAVA面试(全)13

     

    可以看见方法表中的排列顺序严格按照父类层级排列,也就说Person中的eat()方法在方法表中的偏移量与Girl和Boy的eat()方法的偏移量是相同的

    此时演示一下多态调用:

    class Party {
        void happyHour() {
            Person girl = new Girl();
            girl.speak();
        }
    }

     

    1. 根据Person方法表获得speak()偏移量为15

    2. 根据调用对象的具体对象为Girl,进入到Girl的方法表中

    3. 寻找偏移量为15的方法,查看是否有重写,如果重写了则调用重写方法,没重写则调用父类方法。

    [参考博客]  https://www.cnblogs.com/kaleidoscope/p/9790766.html 

    永久代和元数据区

    在JDK1.7前,方法区的实现为永久代:

    永久代存放在中,和新生代、老年代的内存地址是连续的。存放了永久区规范中的所有信息。

    JDK1.7开始,永久代开始逐渐被替换成了元数据区

    1.7中原本永久代的静态变量和常量池并入了Java堆中,剩余信息放入了本地内存。

    1.8以后永久代就全部转移到本地内存当中了。

    为何移除永久代?

    永久代的调优非常困难,难以确定程序中的类的数量、常量的数量,尤其是使用了动态代理以后系统中的类会非常多,如果没有配置好永久代的大小就会很容易爆出OOM。

    并且永久代和老年代的GC是捆绑在一起的,通常这两个区域的垃圾都比较少,那么永久代满了后会频繁触发GC影响性能。

    将永久代转移到本地内存后,它的大小只会受到机器本身内存大小的限制。

    [参考]  https://blog.csdn.net/zhushuai1221/article/details/52122880 

    内存泄漏/内存溢出

    概念

    内存泄漏:

    指的是程序创建对象后,当不再使用时,不能及时释放它或者完全无法释放的内存空间。

    内存溢出:

    指的是程序需要申请新内存时,没有足够大小的空间提供他使用

    关系?

    • 内存泄漏会导致无用的对象占据内存,数量可能会随着时间增加而增多,最终可能导致内存溢出。

    • 内存溢出可以看做是内存泄漏的症状。

    • 内存泄漏常发生在堆中,内存溢出可能发送在堆、方法区、堆外内存都有可能。

    内存泄漏经常发生在哪?

    内存泄漏一般发送在堆内存中,举几个例子

    • 静态集合类,向集合内添加的元素永远会被集合类持有,如果不清空它会一直存在无法GC

    • 单例模式,单例模式下,单例对象的生命周期和程序一样长,如果他还持有其他对象的引用会导致被持有的也无法释放。

    • IO流、数据库连接这些资源没有显式的关闭

    内存溢出的几种情况

    • java.lang.OutOfMemoryError: Java heap space

    • java.lang.OutOfMemoryError: PermGen space

    • java.lang.OutOfMemoryError: Requested array size exceeds VM limit

    • java.lang.OutOfMemoryError: request bytes for . Out of swap space?

    • java.lang.OutOfMemoryError: (Native method)

    Java heap space

    出现原因在于堆内存无法为新的对象分配内存了

    • 如果是内存泄漏导致的话,那么堆中肯定会有很多无用但是也无法回收的对象占据了内存,导致内存用尽。

    • 也有可能是要分配的大对象,例如数组,而堆中剩余的内存不足以继续分配。

    PerGen space

    JDK1.8之前的永久代溢出,永久代中存放类的元数据和静态类型等数据,包括字符串对象也是存储在永久区的,如果使用动态代理等动态生成类的功能,可能导致永久代溢出

    Requested array size exceeds VM limit

    错误出现在创建的数组大于堆的最大内存时抛出的异常

    Direct buffer memory

    堆外内存溢出,在JDK1.8以后的方法区都在堆外内存,还有一些框架(NIO)使用堆外内存作为缓存,当堆外内存不足时,或达到配置的大小限制时抛出的异常

    native method

    由于本地内存大小的限制导致的内存溢出,可能是本地内存空间不足,被其他应用程序占用了。

    线上内存溢出排查思路

    • 先看OOM异常输出的错误类型,区别是正常内存资源耗尽还是内存泄漏。

    • 如果是因为大对象的分配导致的溢出,很可能是设计上的不合理,导致对象太大

    • 如果是程序正常执行,但是内存不断稳步增加,就很有可能是内存溢出

    • 如果是本地内存原因,就得排查服务器进程占用内存的情况

    • 如果是堆外内存的异常,就得排查是不是分配的太小了或者没有及时释放。

    • 再根据具体的方向具体分析。

    JVM所占内存大于物理内存原因

    原因在于操作系统的虚存技术,JVM在初始化时参数规定了堆内存的最大大小,这里指的是常驻内存的大小,当JVM继续向操作系统申请内存时会被置换到虚拟内存,所以JVM占用大小可能超过物理内存原因

    [参考]  https://www.cnblogs.com/ynx01/p/10876460.html 

    一些调整参数

    -Xmx:最大heap内存大小,超过报OOM

    -Xms:初始堆大小,最小heap内存大小

    -Xmn:指定新生代内存

    -XX:MaxPermSize:设置方法区最大大小

    -Xss:指定线程栈大小

    -XX:SuriviorRatio:设置新生代Eden区和Surivior大小比例

    -XX:NewRatio:设置新生代和老年代大小的比例

    堆外内存

     

    GC

    几种对象的引用

    1. 强引用,代码当中普遍存在的,类似Object obj = new Object();。只要强引用还在GC永远不会回收被引用的对象。

    2. 软引用,用来描述有用但是非必须的对象(缓存),直到内存空间不够用时才会被GC。

    3. 弱引用,用来描述非必须的对象,只能生存到下一次GC之前,无论内存是否足够。

    4. 虚引用,随时可能被回收,常用于跟踪垃圾回收过程

    判断对象可回收

    引用计数法(JVM没用它)

    基本思路: 给对象添加引用计数器,每当有一个地方引用它,计数器就加1。引用失效就减1,当计数器为0时,认为对象是不可用的。

    优点: 实现简单,判定高效

    缺点:难以解决对象循环引用问题,开销大,需要频繁的修改计数器


    可达性分析法

    基本思路: 通过一系列的GC Root为起点,开始向下搜索,当一个对象没用任何的GC Root可以与他相连,就认为这个对象是不可达的。

    GC Root有哪些:

    1. 虚拟机栈中引用的对象

    2. 方法区静态属性引用对象

    3. 方法区常量引用对象

    4. 本地方法中引用对象

    优点:对垃圾对象的判定更加精确严禁,可以处理循环引用的情况。

    缺点:实现比较复杂,需要花费大量时间分析数据,会产生STW。

    垃圾回收算法

    标记清除法

    步骤:

    1. 根据可达性分析算法标记出没有任何引用链的对象。

    2. 将被标记的对象清除JAVA面试(全)14

     

    缺点: 会产生大量不连续的内存碎片,会导致大对象分配时可能无法找到连续的空间。标记和清除的效率都不高

    场景: 适合用于垃圾较少的老年代回收

    标记压缩法

    步骤:

    1. 标记阶段和标记清除法相同

    2. 将所有存活对象整理到内存的一端连续的排列,然后清除掉边界以外的内存。

     

    优点:可以减少内存碎片的产生

    缺点:效率问题,还多了一次的整理步骤

    场景: 适合用于垃圾较少的老年代回收

    复制法

    步骤:

    1. 内存划分成大小相同的两块,每次只使用其中的一块。

    2. 当一块内存快用完时触发GC,将存活对象复制到另一块空间中

    3. 清除原空间的信息。

    4. JAVA面试(全)15

     

    优点: 实现简单,运行高效

    缺点:空间会被折半,浪费空间,效率会随着生存对象的数量增加而降低(需要更多的复制操作)。

    场景:适合用于垃圾较多的新生代

    分区算法

    思路:

    将内存空间分成若干个小区间,每一个小区间独立使用,独立回收,并可以通过控制一次回收多少个区间来控制STW产生的停顿时间。

    分代算法

    分代算法是针对新生代和老年代的特点,使用不同的算法进行回收。

    思路:

    1. 新生代中存活的对象较少,使用复制算法较为合适

    2. 老年代中对象存活率高,使用标记压缩法、标记清除法更合适

    垃圾收集器

    几种垃圾收集器

    新生代收集器:serial、ParNew、Parallel Scavenge

    老年代收集器:serial Old、Parallel Old、CMS

    整堆收集器: G1

    并发收集器和并行收集器

    并行收集器:

    指多线程的回收垃圾,但用户线程处于等待状态。如ParNew、Parallel Scavenge、Parallel Old

    并发收集器:

    指用户线程和垃圾回收线程同时都在执行、也可能是交替。如CMS、G1

    Minor GC和MajorGC

    Minor GC

    新生代GC,发生频繁,但是回收速度也比较快。

    Major GC

    老年代GC。出现Major GC通常也会附带至少一次的Minor GC,MajorGC速度较慢

    Full GC

    新生代老年代都回收,全部执行

    Serial收集器

    串行收集器

    特点:

    • 新生代收集器、采用复制算法

    • 单线程回收垃圾

    • 会产生STW

    • 在CPU性能较弱的机器上serial收集器没有线程切换的开销,效率要由于并行收集器

    ParNew收集器

    ParNew收集器是serial收集器的多线程版本

    特点:

    • 除了多线程,其他和serial相同

    Parallel Scavenge收集器

    这是个关注吞吐量的收集器

    特点:

    • 新生代收集器、复制算法、多线程

    • 关注系统的吞吐量,根据吞吐量要求动态调整堆中新生代老年代的大小。

    如何调整?

    如果设定较短的停顿时间,那么可能会将新生代的大小缩小,从而缩短单次GC的时间,但是会增加GC的频率。

    如果设定较长的停顿时间,那么新生代可能较大,可以减少GC的频率,但是单次停顿时间长,占用内存多。

    所以需要在保证停顿时间和吞吐量的前提下,尽量减小堆的占用空间。

    Serial old

    老年代的串行收集器

    特点:

    • 针对老年代、单线程手机

    • 采用标记清除法、标记压缩法

    Parallel Old

    老年代关注吞吐量的收集器

    特点:

    • 针对老年代、多线程、采用标记压缩法

    • 关注吞吐量(同Parallel Scavenge)

    CMS收集器

    老年代的并行收集器

    特点:

    • 针对老年代,基于标记清除法(不压缩)

    • 以最短回收停顿时间为目标,并发收集

    回收流程

    1. 初始标记,只标记GCRoot能直接关联到的对象,速度快,但是有STW。

    2. 并发标记,与用户线程并发执行,从上一步中标记的对象中继续进行可达性分析,不保证标记出所有的存活对象,因为程序还在运行。

    3. 重新标记,修正并发标记中因为程序运行而导致变动的那一部分对象,会STW

    4. 并发清除,多线程回收垃圾对象,耗时最长,会和用户线程一起工作。

    大白话

    CMS就是把整个标记清除法的步骤给细化了,并且根据预计的执行时间控制每一个步骤之间的间隔,让程序看似无感知。

    JAVA面试(全)16

    缺点

    1. 对CPU资源敏感,虽然只有少量的STW,但是因为会占用一部分线程,还是会导致程序变慢。

    2. 无法处理浮动垃圾,在并发清除时,用户线程还会产生新的垃圾,成作浮动垃圾,这就使得CMS不能在老年代接近满时再进行收集,必须预留一定的内存空间。

      当CMS预留的空间无法满足需要时,就会出现Concurrent Mode Failure,这时就会启动Serial Old,从而导致另一次的Major GC产生,非常影响性能。

    3. 会有内存碎片,为了减少对用户吞吐量的影响,CMS采用标记清除法,没有压缩。

    G1收集器

    特点:

    1. 多线程清理,可以实现伪并发GC。

    2. 分代收集,它是唯一一个全堆使用的垃圾回收器,将堆分成很多块分区。

    3. 采用多种垃圾回收算法,不产生碎片,整体看是标记清除法,局部看又是采用复制算法,堆的结构用的又是分区算法的思想。

    4. 可预测停顿,适合运存大、处理器多的服务器使用。

    结构:

    G1收集器将堆内存按照多个分区块划分,新生代和老年代不是之前模型中的物理隔离(各自有用一定范围的内存区域),类似于分区算法。

    回收流程

    1. 初始标记,仅仅标记GC Root能直接关联的对象,需要STW,但是很快。

    2. 并发标记,从上一步标记的对象进行可达性分析,标记存活对象,与用户线程并发执行,不保证标记出所有存活对象。

    3. 最终标记,修正上一阶段中用户线程变动的对象,需要STW。

    4. 筛选回收,根据分区的回收价值成本排序,然后根据期望的GC停顿执行回收计划,最后回收计划中分区的垃圾。采用复制算法,此过程中压缩避免内存碎片

    G1如何实现停顿预测

    因为分成多个区,可以避免对Java堆进行全区域的垃圾收集,通过回收分区大小和垃圾对象的数量可以预测清理一个分区所需的时间,通过控制每次清理分区的数量来控制停顿时间。

    一个对象被不同区域引用的问题

    引用类型可能出现A区域引用B区域的情况,那么在可达性分析时应该避免全堆的扫描。

    JVM采用的是Remembered Set来避免全局扫描,其实就是对每个分区内的对象和引用的对象建立索引,在操作时可以直接从Set中获取到具体的地址。

    [垃圾收集器参考]  https://blog.csdn.net/tjiyu/article/details/53983650 

    垃圾回收策略

    非G1回收器的情形下。

    JAVA面试(全)17

     

    堆结构

    • 新生代和老年代的大小默认1:2。

    • 新生代中又分为一个Eden和两个surivor区域,大小比例为8:1:1

    新对象优先在新生代分配

    • 大多数情况下,对象在Eden区分配,当Eden没有足够空间时会发起一次Minor GC

    • 当回首时,将Eden和survivor中的存活对象复制到另一个survivor中,然后清空前两个。

    大对象直接进入老年代

    • 大对象指的是需要大量连续内存空间的对象,如字符串、数组等。

    • 大对象对容易导致提前触发GC获取足够的连续空间来分配他们。

    • 如果大于一定阈值会直接进入老年代

    长期存活对象进入老年代

    • 在对象头中有一个标记,存对象的年龄

    • 每经历一个Minor GC后存活的对象,它的年龄就会+1,默认到达15后进入老年代

      但是也会出现例外,如果survivor空间中相同年龄的对象占用的空间大于survivor空间的一半,也会将大于等于这个年龄的移入老年代,避免频繁的Minor GC

    空间分配担保

    1. 发生Minor GC之前,JVM检查老年代最大可用的连续空间是否大于新生代所有对象空间(最坏打算全部进老年代)

    2. 如果大于则Minor GC是安全的。

    3. 如果不大于,那么JVM会检查HandlePromotionFailure配置的值是否允许担保失败。

    4. 如果不允许则进行Full GC。

    5. 如果允许,接着检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小

    6. 如果大于则进行Minor GC,这是有风险的,成功就结束。

    7. 如果失败,失败的情况就是Minor GC后进入老年代的对象大小大于老年代可用的空间,那么将会再触发一次Full GC,相当于连着两次,影响性能。

    Minor GC何时发生?

    新生代GC

    1. Eden区满、或新申请对象在Eden区无法分配时会触发

    2. 伴随Full GC一起触发

    Major GC何时发生?

    1. 老年代空间不足时触发

    2. 调用System.gc()时触发,但是不必然执行

    3. 出现分配空间担保失败时触发。

    4. JDK1.8之前的方法区内存不足时触发

    JVM GC调优思路

    首先应该明确系统的关注点

    1. 停顿时间,常见的是WEB服务器,直接参与用户交互的程序,他们不希望停顿时间太长,影响用户体验。

    2. 吞吐量,常见的是异步处理的服务器程序、科学计算等,他们只希望能够高效率的运用CPU的时间。

    3. 根据前两个目标控制GC次数和GC停顿时长达到一个平衡点,然后减少堆内存的占用,以获得更好的空间局部性

    减小堆的大小可以缩短单次GC时间,但是频率增加。

    反之,增大堆大小,可以减少GC次数,但是单次清理的垃圾更多,会增加GC时间。

    4.基于以上的思路选择合适的垃圾收集器,通过输出的GC日志合理配置调优的参数即可。

    异常处理机制JAVA面试(全)18

  2. Error

    • Error表示的是程序无法处理的错误,大多是比较严重的问题,并且不是在代码中可以处理的。

    • 例如:内存溢出OutOfMemoryError,JVM虚拟机错误VirtualMachineError,类定义错误NoClassDefFoundError等。

    Exception

    • 表示程序可处理的异常,又分为运行时异常和非运行时异常

    运行时异常:是RuntimeException类及其子类异常,例如空指针,数组越界等等,这些异常在编译时不会去检查它,程序中可以捕获处理也可以不处理。

    非运行时异常: 它在程序编译时会检查,规则上规定RuntimeException以外的异常都属于受检异常,例如IOException、InterruptedException等等,如果代码中不捕获就无法编译通过

    JAVA面试(全)19

     

     

  3.  

 

零散问题

为何重写equals要重写hashCode方法

  • hashCode这个方法在JDK中的约定是,两个对象调用equals返回是true的话他们的hashCode必须是相同的。

  • Object中的equals比较的就是hashCode

  • 如果重写了equals却没有重写hashCode方法可能会导致使用HashMap、HashSet这些基于散列的工具类时可能会没办法得到正确的对象;你逻辑上相同的对象在hashCode可能是不同的。

几种修饰符

Public:对所有类公开

private:只为本类提供

protected:只能用于同一个包下及其子类使用

default:只有相同包下的类是可用的

注意:类和类中方法的默认修饰符为default;接口类的默认修饰符为default,但是方法默认是public的。

JAVA面试(全)20

 

final

  • 修饰类,这个类不能被继承

  • 修饰变量,如果是基本数据类型则值无法更改,引用数据类型的引用地址不可修改。

  • 修饰方法,这个方法将无法被重写

static

静态方法

  • 静态方法可以不依赖任何对象,直接通过类名.方法调用。

  • 因此静态方法内部不能使用this指针

  • 同理静态方法中不能使用非静态的成员变量和非静态的成员方法

静态变量

  • 静态变量可以直接通过类名.变量名访问,也可以被静态方法使用

  • 静态变量的存储位置和普通成员变量不同,它存在于方法区中,普通成员变量存在栈帧中随着对象变化而变化。

静态代码块

  • 静态代码块在类初次加载时会被执行,并且只会执行一次。

静态导包

  • 可以在import中静态导入某个类,调用时可以直接调用类中的静态变量、方法、内部类。

import static com.example.Demo.*;

静态内部类

  • 静态内部类可以不依赖外部对象被创建

  • 它对外部类的访问也有所限制,不能访问非静态的成员变量和方法

内部类

Java中把一个类定义在另一个类或者方法里面,这样的类成为内部类。分别有:成员内部类、局部内部类、匿名内部类、静态内部类。

成员内部类

  • 它是声明在一个类中,看起来像是一个类的成员变量。

  • 成员内部类可以无条件访问外部类的所有成员变量和方法

  • 需要注意,如果内部类和外部类的变量名重复时,默认访问的是内部类的。

  • 外部类访问内部类的变量和方法,需要创建内部类变量(也是可以访问所有)。

  • 内部类也可以被几种修饰符修饰,含义也相同。

局部内部类

  • 同局部变量,它是定义在方法体中的类,它的作用域也只有在方法中有效。

  • 它也可以无条件访问外部类的所有成员变量和方法。

匿名内部类

  • 它经常使用在方法的参数是抽象类或者接口时,使用匿名内部类可以在实现抽象类/接口的方法的基础上生成一个对象

静态内部类

  • 静态内部类可以不依赖外部对象被创建

  • 它对外部类的访问也有所限制,不能访问非静态的成员变量和方法

JDK1.8新特性

default关键字

接口内可以设定默认的实现方法。

public interface NewCharacter {
 public void test1();
 public default void test2(){
     System.out.println("我是新特性1");
 }
}

Lambda表达式

可以简化匿名内部类的写法,也可以实现函数式编程,将定义的函数作为变量传递。

Stream流

  • 它像是一个高级的迭代器,可以有更多的功能,比如过滤,排序,求和,迭代等等

  • 流的数据来源可以是集合、IO等等。

  • 支持并行操作,它内部可以通过线程池,多线程的处理流数据提高效率。适用于多核CPU、数据量大、处理中有等待的场景。使用起来很方便

对集合类实现的改变

方法区全部进堆外

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:maven 打包命令的使用

下一篇:Maven配置 settings.xml 配置阿里云镜像

CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。