美文网首页
Android面试相关问题记录

Android面试相关问题记录

作者: 丹丹无敌 | 来源:发表于2023-04-02 16:49 被阅读0次
    1. android中systemserver为什么不能使用binder机制,而是使用socket通讯机制

    Android系统中的System Server是一个特殊的进程,它是Android系统中的核心服务管理进程,负责启动和管理大部分系统服务。System Server不能直接使用Binder,而是使用Socket进行进程间通信。

    这是因为System Server需要在系统启动时就启动,并且要在整个系统运行期间一直存在,而Binder是一种基于内核的I/O机制实现的进程间通信机制,需要在系统启动时才能创建。因此,为了让System Server能够在系统启动时就正常运行,Android系统采用了Socket机制来实现进程间通信。

    另外,Socket机制也具有很多优点,例如可以跨越不同的进程和计算机,支持网络通信等。因此,在Android系统中,Socket和Binder机制都被广泛使用,它们各自适用于不同的场景和需求。

    1. 跨进程通信有哪些方式:
    • Bundle/Intent传递数据
    • Messenger:Messenger是基于AIDL实现的,服务端(被动方)提供一个Service来处理客户端(主动方)连接,维护一个Handler来创建Messenger,在onBind时返回Messenger的binder。
    • AIDL
    • ContentProvider
    • brocast
    • Socket
    • binder管道信号
    1. JAVA实现多个线程顺序执行的几种方式,以下几种方式的区别?
    • join方式实现:join方式即使用Thread.join方法来实现。Thread.join含义当前线程需要等待previousThread线程终止之后才从thread.join返回。简单来说,就是线程没有执行完之前,会一直阻塞在join方法处。
    • Lock锁方法:通过ReentrantLock我们可以很方便的进行显式的锁操作,即获取锁和释放锁,对于同一个对象锁而言,统一时刻只可能有一个线程拿到了这个锁,此时其他线程通过lock.lock()来获取对象锁时都会被阻塞,直到这个线程通过lock.unlock()操作释放这个锁后,其他线程才能拿到这个锁。
    • ReentrantLock结合Condition:与ReentrantLock搭配的通行方式是Condition,如下:
    private Lock lock = new ReentrantLock();  
    private Condition condition = lock.newCondition(); 
    condition.await();//this.wait();  
    condition.signal();//this.notify();  
    condition.signalAll();//this.notifyAll();
    
    • Semaphore信号量方式
    import java.util.concurrent.Semaphore;
    
    public class ABC_Semaphore {
        // 以A开始的信号量,初始信号量数量为1
        private static Semaphore A = new Semaphore(1);
        // B、C信号量,A完成后开始,初始信号数量为0
        private static Semaphore B = new Semaphore(0);
        private static Semaphore C = new Semaphore(0);
     
        static class ThreadA extends Thread {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        A.acquire();// A获取信号执行,A信号量减1,当A为0时将无法继续获得该信号量
                        System.out.print("A");
                        B.release();// B释放信号,B信号量加1(初始为0),此时可以获取B信号量
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
     
        static class ThreadB extends Thread {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        B.acquire();
                        System.out.print("B");
                        C.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
     
        static class ThreadC extends Thread {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        C.acquire();
                        System.out.println("C");
                        A.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
     
        public static void main(String[] args) throws InterruptedException {
            new ThreadA().start();
            new ThreadB().start();
            new ThreadC().start();
        }
    }
    
    1. 启动线程的方式有几种?线程的状态?

    启动线程的方式只有:

    • X extends Thread;,然后X.start
    • X implements Runnable;然后交给Thread运行

    Java中线程的状态分为6种:

    • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
    • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
      线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
    • 阻塞(BLOCKED):表示线程阻塞于锁。
    • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。1
    • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
    • 终止(TERMINATED):表示该线程已经执行完毕。

    状态之间的变迁如下图所示:


    image.png
    1. CAS实现原子操作的三大问题
    • ABA问题。
      因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
      ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
      如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
    • 循环时间长开销大。
      自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
    • 只能保证一个共享变量的原子操作。
      当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
      还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
    1. volatile的实现原理

    volatile关键字修饰的变量会存在一个“lock:”的前缀。

    Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

    同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

    1. synchronized的实现原理

    Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

    对同步块,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁:

    • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

    JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

    synchronized使用的锁是存放在Java对象头里面,Java对象的对象头由 mark word 和 klass pointer 两部分组成:
    1)mark word存储了同步状态、标识、hashcode、GC状态等等。
    2)klass pointer存储对象的类型指针,该指针指向它的类元数据

    另外对于数组而言还会有一份记录数组长度的数据。


    image.png

    锁信息则是存在于对象的mark word中,MarkWord里默认数据是存储对象的HashCode等信息,

    image.png

    但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式


    image.png
    1. 锁的状态

    一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
    偏向锁
    引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

    偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

    偏向锁获取过程:

    • 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
    • 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
    • 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
    • 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
    • 执行同步代码。

    偏向锁的释放:
    偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

    偏向锁的适用场景:
    始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;

    在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。

    jvm开启/关闭偏向锁
    开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    关闭偏向锁:-XX:-UseBiasedLocking

    轻量级锁
    轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

    轻量级锁的加锁过程:
    1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
    2、拷贝对象头中的Mark Word复制到锁记录中。
    3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
    4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
    5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象。重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

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

    但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点:
    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。

    自旋锁时间阈值:
    自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要

    JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

    JDK1.6中-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;

    不同锁的比较

    image.png
    1. sychronied修饰普通方法和静态方法的区别?什么是可见性?

    对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

    要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。

    1. 锁分哪几类?
    image.png
    1. CAS无锁编程的原理。

    使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

    CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
    还可以说说CAS的三大问题。

    1. ReentrantLock的实现原理。

    线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。

    底层则是利用了JUC中的AQS来实现的。

    1. AQS原理

    是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。

    AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。

    这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。

    在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。

    1. Synchronized的原理以及与ReentrantLock的区别。

    synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

    JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

    1. Synchronized做了哪些优化

    引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析等技术来减少锁操作的开销。

    逃逸分析:如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:
    同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

    锁消除和粗化:

    锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。

    锁粗化:将临近的代码块用同一个锁合并起来。消除无意义的锁获取和释放,可以提高程序运行性能。

    1. sleep 、wait、yield 的区别,wait 的线程如何唤醒它?

    yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

    yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

    调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。

    Wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。

    wait 的线程使用notify/notifyAll()进行唤醒。

    1. sleep是可中断的么?

    sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。

    1. 线程池的创建各个参数含义
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
    

    corePoolSize
    线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;

    如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

    如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

    maximumPoolSize
    线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize

    keepAliveTime
    1)keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间
    2)超过keepAliveTime之后,则会被回收。
    3)如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长

    TimeUnit
    keepAliveTime的时间单位

    workQueue
    workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。

    一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。

    1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
    2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
    3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
    4)更重要的,使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。

    threadFactory
    创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。

    Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。

    RejectedExecutionHandler
    线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    (1)AbortPolicy:直接抛出异常,默认策略;
    (2)CallerRunsPolicy:用调用者所在的线程来执行任务;
    (3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    (4)DiscardPolicy:直接丢弃任务;
    当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

    线程池的工作机制
    1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
    2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
    3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
    4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

    线程池大小确定
    有一个简单且使用面比较广的公式:

    • CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线
      程数设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了
      防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影
      响。一旦任务停止,CPU就会出于空闲状态,而这种情况下多出来一
      个线程就可以充分利用CPU的空闲时间。
    • I/O密集型(2N):这种任务应用起来,系统大部分时间用来处理I/O
      交互,而线程在处理I/O的是时间段内不会占用CPU来处理,这时就可
      以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,可以
      配置多一些线程,具体计算方是2N。

    默认四种线程池
    (1)FixedThreadPool:
    Fixed中文解释为固定。结合在一起解释固定的线程池,说的更全面
    点就是,有固定数量线程的线程池。其
    corePoolSize=maximumPoolSize,且keepAliveTime为0,适合线程稳
    定的场所。
    (2)SingleThreadPool:
    Single中文解释为单一。结合在一起解释单一的线程池,说的更全面
    点就是,有固定数量线程的线程池,且数量为一,从数学的角度来看
    SingleThreadPool应该属于FixedThreadPool的子集。其
    corePoolSize=maximumPoolSize=1,且keepAliveTime为0,适合线程同
    步操作的场所。
    (3)CachedThreadPool:
    Cached中文解释为储存。结合在一起解释储存的线程池,说的更通
    俗易懂,既然要储存,其容量肯定是很大,所以他的corePoolSize=0,
    maximumPoolSize=Integer.MAX_VALUE(2^32-1一个很大的数字)
    (4)ScheduledThreadPool:
    Scheduled中文解释为计划。结合在一起解释计划的线程池,顾名思
    义既然涉及到计划,必然会涉及到时间。所以ScheduledThreadPool是一
    个具有定时定期执行任务功能的线程池。

    1. 说一下堆栈的区别?

    物理地址
    堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)

    栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

    内存分别
    堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈

    栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

    存放的内容
    堆存放的是对象的实例和数组。因此该区更关注的是数据的存储

    栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

    PS:

    1. 静态变量放在方法区
    2. 静态的对象还是放在堆。

    程序的可见度
    堆对于整个应用程序都是共享、可见的。

    栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

    1. HotSpot虚拟机对象

    对象的创建流程

    image.png
    虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行<init>方法。

    为对象分配内存
    类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:

    • 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
    • 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

    选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。


    image.png
    1. Java 中都有哪些引用类型?
    • 强引用:发生 gc 的时候不会被回收。
    • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
    • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
    • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
    1. 怎么判断对象是否可以被回收?

    垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

    一般有两种方法来判断:

    • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器+1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
    • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的
    1. JVM中的永久代中会发生垃圾回收吗

    垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
    (译者注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

    1. 说一下 JVM 有哪些垃圾回收算法?
    • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
    • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
    • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
    • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
    1. 说一下 JVM 有哪些垃圾回收器?

    如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。

    • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
    • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
    • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
    • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
    • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
    • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
    • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年
      代),而前六种收集器回收的范围仅限于新生代或老年代。
    1. 简述分代垃圾回收器是怎么工作的?

    分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

    新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

    • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
    • 清空 Eden 和 From Survivor 分区;
    • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

    每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

    老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

    1. 说一下类装载的执行过程?

    类装载分为以下 5 个步骤:

    • 加载:根据查找路径找到相应的 class 文件然后导入;
    • 验证:检查加载的 class 文件的正确性;
    • 准备:给类中的静态变量分配内存空间;
    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
    • 初始化:对静态变量和静态代码块执行初始化工作。

    类加载顺序

    • 首先加载父类的静态字段或者静态语句块
    • 子类的静态字段或静态语句块
    • 父类普通变量以及语句块
    • 父类构造方法被加载
    • 子类变量或者语句块被加载
    • 子类构造方法被加载
    1. JVM内存区域模型

    JVM内存结构图

    image.png
    方法区
    也称”永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -X:MaxPermSize 参数限制方法区的大小。

    运行时常量池:是方法区的一部分,其中的主要内容来自于JVM对Class的加载。

    Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

    虚拟机栈
    描述的是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。

    局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。

    本地方法栈
    与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。


    也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。

    该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

    程序计数器
    是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

    1. Kotlin的泛型和Java的泛型有什么区别

    Kotlin中的泛型参数可以使用协变(out)和逆变(in)关键字,可以使得代码更加灵活。而Java中的泛型参数只能使用通配符来实现类似的功能。

    Kotlin中的泛型参数默认是非空的,不需要使用?来表示可空。而Java中的泛型参数默认是可以为空的,需要使用?来表示可空。

    Kotlin中的泛型参数可以进行推导,不需要显式指定类型。而Java中的泛型参数必须显式指定类型。

    Kotlin中的泛型参数可以在函数、属性、接口等多种场景下使用。而Java中的泛型参数只能在类和方法中使用。

    Kotlin中的泛型参数可以使用reified关键字来获取泛型类型信息,而Java中无法直接获取泛型类型信息。

    相关文章

      网友评论

          本文标题:Android面试相关问题记录

          本文链接:https://www.haomeiwen.com/subject/xryrddtx.html