【面试题】不知名网友透露的面试题一

1、为什么说非静态内部类持有外部类的引用。

  1. 通过查看编辑后的class文件我们可以看到内部类中有一个this$0对象,是指向外部类的,具体实现为:
  2. 编译器会为内部类添加一个成员变量,该成员变量类型与外部类类型相同,这个成员变量就是指向外部类(this)的引用。
  3. 编译器会为内部类的构造函数增加一个参数,类型和外部类相同,构造函数内部,会把该参数赋值给上一步创建的成员变量。
  4. 在执行内部类构造函数的时候,默认传入外部类的引用。

2、ViewModel在Activity生命周期变化的时候怎么保存数据?和onSaveInstance()相同吗?

viewModel2.0以前,是通过一个hoderFragment来保持和Activity生命周期同步的,设计思路类似于Glide。设置了saveRetainInstance(true)保证configChange改变的时候生命周期不被改变,让这个Fragment在activity重建的时候可以存活下来。

2.0以后,利用componetActivity,重写onRetainNonConfigurationInstance方法保存viewModelStore,在重建Activity的时候通过getLastNonConfigurationInstance方法获取到viewModelStore实例。另外通过实现LifeCycleOwner接口,在onDestory的时候回调实现viewModelStore的clear工作。

onRetainNonConfigurationInstance和onSaveInstance不同的是,onSaveInstance只能保存bundle,bundle是有类型和大小限制的,并且需要在主线程序列化,onRetainNonConfigurationInstance是没有限制的。

viewModelScope 是一个 ViewModel 的 Kotlin 扩展属性。它能在ViewModel销毁时 (onCleared() 方法调用时) 退出。所以只要使用了 ViewModel,就可以使用 viewModelScope在 ViewModel 中启动各种协程,而不用担心任务泄漏。

 

3、手写单例模式,为什么要用volatile修饰变量?

class SingleTon {
    
    private SingleTon() {};
    
    public volatile static SingleTon single;

    public static SingleTon getInstance() {

        if (single == null) {
            synchronized(SingleTon.class) {
                if (single == null) {
                    single = new SingleTon();
                }
            }
        }

        return single;
    }
}

为什么要用volatile修饰变量,请大声说出指令重排序五个大字。

new一个对象一般有四个步骤:

  1. new : 在java堆上开辟内存空间,并将该地址压入操作数栈顶
  2. dup:复制操作数栈顶值,并将其压入栈顶,这时操作数栈有连续相同的两个对象地址
  3. invokespecial:调用构造函数,弹出栈顶一个元素
  4. putstatic:对象地址赋值给变量,弹出栈顶一个元素

CPU为了优化程序,可能会进行指令重排序,3、4两步会打乱。如果不使用volatile的话,会出现如下情况。线程A执行到new SingleTon(),开始初始化实例对象,由于指令重排序,先把引用赋值了,没有调用构造函数。这时候时间片结束了,线程B接手,调用new SingleTon()的时候发现引用不为空,直接返回引用地址了,然后线程B执行了一些其他操作,导致使用了一个没有被初始化的变量。加了volatile以后,保证不会发生指令重排序。

 

4、静态内部类模式实现的单例模式,怎么保证线程安全的?

class SingleTon {
    private SingleTon() {}
    
    public static SingleTon getInstance() {
        return SingleTonHolder.Instance;
    }

    public static class SingleTonHolder {
        public final static Instance = new SingleTon();
    }
}

调用getInstance()的时候,SingleTonHolder才在SingleTon的运行时常量池中把符号引用替换成直接饮用,这时静态对象Instance真正被创建。如何保证线程安全的呢?虚拟机会保证一个类的<cinit>方法在多线程环境中被正确地加锁、同步。

5、类加载器是什么?如何打破ClassLoader的双亲委托模型?

类加载-时机:

  1. 主动引用——会触发初始化:
    1. 虚拟机启动的时候,初始化包括main函数的类
    2. 遇到new  getStatic  putStatic invokeStatic四条指令的时候,如果类没有执行初始化,则先触发类的初始化操作。具体出现的场景是:new一个实例对象,读取一个类的静态字段(用final修饰,编译器放进常量的静态字段除外),设置一个类的静态字段,调用一个类的静态方法。
    3. 对类进行反射调用的时候,如果类没有进行过初始化,则需要尽心初始化。
    4. 初始化一个类的时候,如果其父类没有进行过初始化,则需要对父类进行初始化。
  2. 被动引用——不会触发初始化:
    1. 子类调用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化。
    2. 子类调用父类的常量(final修饰),不会触发父类的初始化。

类加载-过程:

  1. 加载:读取类的二进制字节流
  2. 验证:文件格式验证/元数据验证/字节码验证/符号引用验证
  3. 准备:为类变量在方法区中分配内存并设置初始值,比如int类赋值为0
    public static int value=123;

    变量value在准备阶段的初始值为0,把value赋值为123的操作是putStatic指令,程序被便衣后,存放于类构造器<clinit>()方法中,

  4. 解析:将常量池中的符号引用转换为直接引用的过程

  5. 初始化:开始执行类中的java代码,初始化阶段是执行类构造器<clinit>()方法的过程。

    1. 类构造器<clinit>()和实例构造起<init>()方法不同,不需要显式调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法已经执行完成。一次虚拟机中第一个被执行的<clinit>()方法的类一定是java.lang.Object类。

    2. 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

    3. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

类加载器-分类:

启动类加载器:加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定路径中的,并且是虚拟机能识别的类库加载到虚拟机内存中

扩展类加载器:加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序加载器:加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器:自定义

自定义类加载器的时候,需要继承ClassLoader,实现findClass方法。要打破双亲委派模型,需要重写loadClass方法。

参考资料:https://blog.csdn.net/w372426096/article/details/81901482

6、为什么主线程Looper的循环体消息已经为空了,却没有发生ANR?

此处使用了pipe管道机制和epoll:

pipe机制:在没有消息的时候,阻塞线程并进入休眠,释放CPU资源,有消息时唤醒线程。直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。

Linux pipe/epoll机制:主线程的messageQueue没有消息的时候,阻塞在loop的queue.next()中的nativePollOnce()方法里。

 

7、简单描述为什么Handler在Activity中会出现内存泄露?

这里相当于在Activity中声明了一个内部类,内部类默认会持有外部类的引用,也就是handler持有activity的引用而Message持有handler引用,如果在你退出Activity的时候,有一个消息还没有处理的话,那么这时候Activity是没法回收的。因为在方法区里面有static final ThreadLocal<Looper> sThreadLocal持有Looper引用,而Looper持有MessageQueue引用,MessageQueue持有Message引用,Message持有Handler引用,handler持有Activity引用,所以Activity无法回收,造成内存泄露。引用链如下

方法区-》sThreadLocal-》Loop-》MessageQueue-》Message-》Handler-》Activity
 

8、线程池工作原理?

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

提交任务到线程池,先判断核心线程数是否已达最大值,如果不是,则创建一个线程执行任务;如果是,则判断任务队列workQueue是否已满,如果未满,则进入队列等待,如果满了,则判断总线程数是否已达最大值,如果不是,则创建一个线程执行任务,如果是,则拒绝任务。

9、LeakCanary原理

监听:在Android中,当一个Activity走到onDestroy方法的时候,说明页面已经销毁,应该被系统GC回收。通过 Application.registerActivityLifecycleCallbacks()注册activity生命周期的监听,检测到onDestroy方法的时候,获取到这个activity,判断是否被系统GC正常回收。

检测:通过WeakReference+ReferenceQueue来判断对象是否被真的回收。WeakReference创建的时候,传入一个ReferenceQueue对象,当WeakReference引用的对象生命周期结束的时候,一旦对GC检查到,GC就会将该对象放入ReferenceQueue中,当GC过去对象一直无法放入ReferenceQueue,说明可能存在内存泄露。当有怀疑对象的时候,手动触发GC,二次确认。

分析:通过HAHA这个开源库获取内存中的堆信息快照snapshot,通过带分析对象去snapshot中查找强引用。

 

10、Java中同步的方式有哪些,分别有什么优缺点?

Synchronized:获得和释放锁都是自动的,不可中断。

ReentrantLock:实现了lock接口,手动操作获取和释放锁,可中断等待(lockInterruptibly),可实现公平锁(构造参数传入true,内部维护了一个队列,保证最先等待的最先被释放),限时等待(tryLock,传入等待时间),需要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 。

Volatile:保证可见性

 

11、OkHttp源码重点:

  1. 请求放哪里去了???Okhttp内部维护了两个队列,分别为运行中队列runningAsyncCalls、等待中队列readyAsyncCalls,都是基于ArrayDeque实现的,进入运行中队列的条件是:如果运行中队列任务个数小于设置的默认最大值64,并且访问同一个host的任务请求数是小于5的;否则就进入等待中队列。
  2. 请求被谁处理了???加到运行中队列,就会立刻执行。executorService是一个线程池,核心线程数是0,最大线程数是MAX_VALUE,workQueue是一个无容量队列(即无队列),这是因为我们已经有了上面两个队列,线程池中不需要维护队列了,所有的任务都通过上面两个队列来主动控制。
    executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
                SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))

     

  3. 请求交给线程池处理,调用execute方法里,getResponseWithIntercerptorChain方法是通过责任链模式设置拦截器。

  4. 请求是怎么维护的???我们知道进入到运行中队列后,会启动一个线程去执行任务。那么等待中队列是如何被执行的呢?线程任务做的request的操作,并把失败和成功的结果通过responseCallback回调到dispatcher,执行完成后进入不论是否有异常,在finally方法里进入dispatcher的finished方法,该方法里是从readyAsyncCalls里remove一个任务,加入到runningAsyncCalls中。因为ArrayDeque是非线程安全的,所以上述操作是包在synchronized代码块中的。

12、几个集合类的介绍

  1. ArrayDeque:OkHttp内部两个队列使用的,底层使用一个可变数组,没有容量限制,为2^n,线程不安全。可以作为栈来使用,效率高于Stack(是因为非线程安全的原因),也可以作为队列来使用,效率高于LinkedList(采用循环队列,不需要结点Node来支持,Node的频繁创建和销毁对效率是有影响的。)。队列中采用两个int值:默认构造函数里是实习一个长度16的数组,head和tail表示头部和尾部,注意tail不是尾部元素的索引,而是+1,下次即将插入数据的位置。(参考:https://www.jianshu.com/p/1c1c3f24762e
  2. SparseArray:内部两个数组mKeys,mValues,设计精妙之处在于,删除数据的时候,只是设置了一个DELETED的标志位,put的时候如果找到当前位置的value是DELETED,那么直接覆盖。真正删除数据的操作是在gc()中。
  3. ArrayMap:Android传递数据通过bundle,这就是arrayMap的应用;和hashMap相比,读写速度类似,但是内存消耗少30%,内部维护两个数组,mHashes(有序的,保存key的hashcode),mArrays(index*2的位置上存储key,index*2 + 1的位置上存储value);查找的时候通过二分法先找到mHashes中index的位置,再与mArrays中index*2的位置上的key做对比,相同的话,则取index*2+1位置上的value。同时做一些缓存的策略,类似于一个对象池的设计。避免创建不必要的对象,减轻GC的压力。

13、SharedPreference的commit和apply有什么区别?

apply()是先保存到内存,在异步写入磁盘,而commit是直接写入磁盘;commit是有boolean类型返回值的,apply没有;

 

14、内存抖动场景:

  1. 字符串对象大量生成,比如说 str = str + “test”,就会生成一个新的字符串对象;比如在频繁回调的接口里打log
  2. 在onDraw方法里new对象

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值