面试题

  • 1、应用程序在什么情况需要创建Context对象的?应用程序App共有多少个Context?
    应用程序创建Context实例的 情况有如下几种情况:

    1、创建Application 对象时, 而且整个App共一个Application对象
    
    2、创建Service对象时
    
    3、创建Activity对象时
    

总Context实例个数 = Service个数 + Activity个数 + 1(Application对应的Context实例)

  • 2、Context和activity、service、Application关系


    image.png
  • 3、Application里面attachBaseContext和onCreate函数调用顺序
Application-> attachBaseContext ();
 
ContentProvider:onCreate()
 
Application:onCreate()
  • 4、你是怎么理解多线程间的有序性,可见性和原子性的?
    原子性:执行一个或者多个操作的时候,要么全部执行,要么都不执行,并且中间过程中不会被打断。Java中的原子性可以通过独占锁和CAS去保证。
    可见性:指多线程访问同一个变量的时候,一个线程修改了变量的值,其他线程能够立刻看得到修改的值。锁和volatile能够保证可见性。
    有序性:程序执行的顺序按照代码先后的顺序执行。锁和volatile能够保证有序性。
  • 5、compileSdkVersion, minSdkVersion 和 targetSdkVersion区别是什么?
compileSdkVersion:SDK编译版本,即:告诉 Gradle 用哪个 Android SDK 版本编译你的应用
minSdkVersion:App安装时要求的设备的最低android版本。低于该版本,无法安装
targetSdkVersion:一个用于指定应用的目标API级别的整数。如果未设置,默认与miniSdkVersion相同。一个用于指定应用的目标API级别的整数。如果未设置,默认与miniSdkVersion相同。
该属性用于通知系统,您已针对目标版本进行测试,并且系统不应通过启用任何兼容性行为,以保持您的应用与目标版本的向前兼容性。应用仍可在较低版本上运行。
如果平台的 API 级别高于应用 targetSdkVersion 所声明的版本,系统便可通过启用兼容性行为,确保应用继续以您所期望的方式工作(即应用的外观和行为保持为targetSdkVersion的)。
一般来说,miniSdkVersion要低于targetSdkVersion。
  • 6、String,StringBuffer和StringBuilder的区别?
String:String属于不可变对象,每次修改都会生成新的对象。
StringBuilder:可变对象,非多线程安全,效率高于StringBuffer
StringBuffer:可变对象,多线程安全。
效率:StringBuilder>StringBuffer>String
  • 7、equals()和==的区别
(1)基本类型比较:
①使用双等号 == 比较的是值是否相等。
②基本数据类型无equals方法(没有意义)。
(2)引用类型比较:
①重写了equals方法,比如String。
第一种情况:使用==比较的是String的引用是否指向了同一块内存。
第二种情况:使用equals比较的是String的引用的对象内容是否相等。
②没有重写equals方法,比如User等自定义类。
==和equals比较的都是引用是否指向了同一块内存。
  • 8、Activity生命周期及横竖屏切换


    image.png
1、新建一个Activity,并把各个生命周期打印出来

2、运行Activity,得到如下信息

onCreate-->
onStart-->
onResume-->

3、按crtl+f12切换成横屏时

onSaveInstanceState-->
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->

4、再按crtl+f12切换成竖屏时,发现打印了两次相同的log

onSaveInstanceState-->
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->
onSaveInstanceState-->
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->

5、修改AndroidManifest.xml,把该Activity添加 android:configChanges="orientation",执行步骤3

onSaveInstanceState-->
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->

6、再执行步骤4,发现不会再打印相同信息,但多打印了一行onConfigChanged

onSaveInstanceState-->
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->
onConfigurationChanged-->

7、把步骤5的android:configChanges="orientation" 改成 android:configChanges="orientation|keyboardHidden",执行步骤3,就只打印onConfigChanged

onConfigurationChanged-->

8、执行步骤4

onConfigurationChanged-->
onConfigurationChanged-->

“mcc“ 移动国家号码,由三位数字组成,每个国家都有自己独立的MCC,可以识别手机用户所属国家。
“mnc“ 移动网号,在一个国家或者地区中,用于区分手机用户的服务商。
“locale“ 所在地区发生变化。
“touchscreen“ 触摸屏已经改变。(这不应该常发生。)
“keyboard“ 键盘模式发生变化,例如:用户接入外部键盘输入。
“keyboardHidden“ 用户打开手机硬件键盘
“navigation“ 导航型发生了变化。(这不应该常发生。)
“orientation“ 设备旋转,横向显示和竖向显示模式切换。
“fontScale“ 全局字体大小缩放发生改变
 总结:

1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次

2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次

3、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

对android:configChanges属性,一般认为有以下几点:
1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次

2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次

3、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

但是,自从Android 3.2(API 13),在设置Activity的android:configChanges="orientation|keyboardHidden"后,还是一样会重新调用各个生命周期的。因为screen size也开始跟着设备的横竖切换而改变。所以,在AndroidManifest.xml里设置的MiniSdkVersion和 TargetSdkVersion属性大于等于13的情况下,如果你想阻止程序在运行时重新加载Activity,除了设置"orientation",你还必须设置"ScreenSize"。

  • 8-1、onSaveInstanceState()和onRestoreInstanceState()详解
1、什么情况下执行:

    1.1、如果是用户自动按下返回键,或程序调用finish()退出程序,是不会触发onSaveInstanceState()和onRestoreInstanceState()的。
    1.2、每次用户旋转屏幕时,您的Activity将被破坏并重新创建。当屏幕改变方向时,系统会破坏并重新创建前台Activity,因为屏幕配置已更改,您的Activity可能需要加载替代资源(例如布局)。即会执行onSaveInstanceState()和onRestoreInstanceState()的。

2、onRestoreInstanceState()什么情况下执行?
    2.1、onCreate()您可以选择执行onRestoreInstanceState(),而不是在系统调用onStart()方法之后恢复状态。
    2.2、系统onRestoreInstanceState()只有在存在保存状态的情况下才会恢复,因此您不需要检查是否Bundle为空
  • 8-2、android:configChanges属性

  • 9、Service启动方式
1.startService

①.定义一个类继承service
②.在manifest.xml文件中配置该service
③.使用context的startService(intent)启动该service
④.不再使用时,调用stopService(Intent)停止该服务

2.bindService

①.创建bindService服务段,继承自service并在类中,创建一个实现binder接口的实例对象并提供公共方法给客户端调用
②.从onbind()回调方法返回此binder实例
③.在客户端中,从onserviceconnected()回调方法接收binder,并使用提供的方法调用绑定服务
  • 10、Activity的启动模式
①.standard模式

a.Activity的默认启动模式

b.每启动一个Activity就会在栈顶创建一个新的实例。例如:闹钟程序

缺点:当Activity已经位于栈顶时,而再次启动Activity时还需要在创建一个新的实例,不能直接复用。

②.singleTop模式

特点:该模式会判断要启动的Activity实例是否位于栈顶,如果位于栈顶直接复用,否则创建新的实例。 例如:浏览器的书签

​缺点:如果Activity并未处于栈顶位置,则可能还会创建多个实例。

③.singleTask模式

特点:使Activity在整个应用程序中只有一个实例。每次启动Activity时系统首先检查栈中是否存在当前Activity实例,如果存在

则直接复用,并把当前Activity之上所有实例全部出栈。例如:浏览器主界面

④.singleInstance模式

特点:该模式的Activity会启动一个新的任务栈来管理Activity实例,并且该势力在整个系统中只有一个。无论从那个任务栈中启动该Activity,都会是该Activity所在的任务栈转移到前台,从而使Activity显示。主要作用是为了在不同程序中共享一个Activity
  • 11、Touch事件传递机制
在我们点击屏幕时,会有下列事件发生:

Activity调用dispathTouchEvent()方法,把事件传递给Window;

Window再将事件交给DecorView(DecorView是View的根布局);

DecorView再传递给ViewGroup;

Activity ——> Window ——> DecorView ——> ViewGroup——> View

事件分发的主要有三个关键方法

dispatchTouchEvent() 分发

onInterceptTouchEvent() 拦截 ,只有ViewGroup独有此方法

onTouchEvent() 处理触摸事件

Activity首先调用dispathTouchEvent()进行分发,接着调用super向下传递

ViewGroup首先调用dispathTouchEvent()进行分发,接着会调用onInterceptTouchEvent()(拦截事件)。若拦截事件返回为true,表示拦截,事件不会向下层的ViewGroup或者View传递;false,表示不拦截,继续分发事件。默认是false,需要提醒一下,View是没有onInterceptTouchEvent()方法的

事件在ViewGroup和ViewGroup、ViewGroup和View之间进行传递,最终到达View;

View调用dispathTouchEvent()方法,然后在OnTouchEvent()进行处理事件;OnTouchEvent() 返回true,表示消耗此事件,不再向下传递;返回false,表示不消耗事件,交回上层处理。
  • 12、介绍下实现一个自定义View的基本流程
①.自定义View的属性 编写attr.xml文件

②.在layout布局文件中引用,同时引用命名空间

③.在View的构造方法中获得我们自定义的属性 ,在自定义控件中进行读取(构造方法拿到attr.xml文件值)

④.重写onMesure

⑥.重写onDraw
  • 13、ANR是什么?怎样避免和解决ANR
Application Not Responding,即应用无响应

出现的原因有三种:

a)KeyDispatchTimeout(5 seconds)主要类型按键或触摸事件在特定时间内无响应

b)BroadcastTimeout(10 seconds)BoradcastReceiver在特定的时间内无法处理

c)ServiceTimeout(20 seconds)小概率类型Service在特定的时间内无法处理完成

避免ANR最核心的一点就是在主线程减少耗时操作。通常需要从那个以下几个方案下手:

a)使用子线程处理耗时IO操作

b)降低子线程优先级,使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同

c)使用Handler处理子线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程

d)Activity的onCreate和onResume回调中尽量避免耗时的代码

e)BroadcastReceiver中onReceiver代码也要尽量减少耗时操作,建议使用intentService处理。intentService是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题
  • 14、内存泄漏和内存溢出是什么?
内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。内存溢出通俗的讲就是内存不够用。

内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光
  • 15 、内存泄露原因以及解决:
一、Handler 引起的内存泄漏。

解决:将Handler声明为静态内部类,就不会持有外部类SecondActivity的引用,其生命周期就和外部类无关,

如果Handler里面需要context的话,可以通过弱引用方式引用外部类

二、单例模式引起的内存泄漏。

解决:Context是ApplicationContext,由于ApplicationContext的生命周期是和app一致的,不会导致内存泄漏

三、非静态内部类创建静态实例引起的内存泄漏。

解决:把内部类修改为静态的就可以避免内存泄漏了

四、非静态匿名内部类引起的内存泄漏。

解决:将匿名内部类设置为静态的。

五、注册/反注册未成对使用引起的内存泄漏。

注册广播接受器、EventBus等,记得解绑。

六、资源对象没有关闭引起的内存泄漏。

在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等方法释放。

七、集合对象没有及时清理引起的内存泄漏。

通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。
  • 16、图片加载框架有哪些?他们之间的区别是什么?
ImageLoader :

优点:

① 支持下载进度监听;

② 可以在 View 滚动中暂停图片加载;

③ 默认实现多种内存缓存算法这几个图片缓存都可以配置缓存算法,不过 ImageLoader 默认实现了较多缓存算法,如 Size 最大先删除、使用最少先删除、最近最少使用、先进先删除、时间最长先删除等;

④ 支持本地缓存文件名规则定义;

缺点:

缺点在于不支持GIF图片加载, 缓存机制没有和http的缓存很好的结合, 完全是自己的一套缓存机制

Picasso:

优点:

① 自带统计监控功能,支持图片缓存使用的监控,包括缓存命中率、已使用内存大小、节省的流量等。

② 支持优先级处理

③ 支持延迟到图片尺寸计算完成加载

④ 支持飞行模式、并发线程数根据网络类型而变,手机切换到飞行模式或网络类型变换时会自动调整线程池最大并发数。

⑤ “无”本地缓存。Picasso 自己没有实现本地缓存,而由okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。

缺点:

于不支持GIF,默认使用ARGB_8888格式缓存图片,缓存体积大。

Glide:

优点:

① 图片缓存->媒体缓存 ,支持 Gif、WebP、缩略图。甚至是 Video。

② 支持优先级处理

③ 与 Activity/Fragment 生命周期一致,支持 trimMemory

④ 支持 okhttp、Volley。Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。

⑤ 内存友好,内存缓存更小图片,图片默认使用默认 RGB565 而不是 ARGB888

缺点:

清晰度差,但可以设置

Fresco:

优点:

① 图片存储在安卓系统的匿名共享内存, 而不是虚拟机的堆内存中,所以不会因为图片加载而导致oom, 同时也减少垃圾回收器频繁调用回收Bitmap导致的界面卡顿,性能更高.

② 渐进式加载JPEG图片, 支持图片从模糊到清晰加载

③ 图片可以以任意的中心点显示在ImageView, 而不仅仅是图片的中心.

④ JPEG图片改变大小也是在native进行的, 不是在虚拟机的堆内存, 同样减少OOM

⑤ 很好的支持GIF图片的显示

缺点:

框架较大, 影响Apk体积,使用较繁琐
  • 17、网络框架有哪些?他们之间的区别是什么?
Xutils

这个框架非常全面,可以进行网络请求,可以进行图片加载处理,可以数据储存,还可以对view进行注解,使用这个框架非常方便,但是缺点也是非常明显的,使用这个项目,会导致项目对这个框架依赖非常的严重,一旦这个框架出现问题,那么对项目来说影响非常大的

OKhttp

Android开发中是可以直接使用现成的api进行网络请求的。就是使用HttpClient,HttpUrlConnection进行操作。okhttp针对Java和Android程序,封装的一个高性能的http请求库,支持同步,异步,而且okhttp又封装了线程池,封装了数据转换,封装了参数的使用,错误处理等。API使用起来更加的方便。但是我们在项目中使用的时候仍然需要自己在做一层封装,这样才能使用的更加的顺手。

Volley

Volley是Google官方出的一套小而巧的异步请求库,该框架封装的扩展性很强,支持HttpClient、HttpUrlConnection, 甚至支持OkHttp,而且Volley里面也封装了ImageLoader,所以如果你愿意你甚至不需要使用图片加载框架,不过这块功能没有一些专门的图片加载框架强大,对于简单的需求可以使用,稍复杂点的需求还是需要用到专门的图片加载框架。Volley也有缺陷,比如不支持post大数据,所以不适合上传文件。不过Volley设计的初衷本身也就是为频繁的、数据量小的网络请求而生。

Retrofit

Retrofit是Square公司出品的默认基于OkHttp封装的一套RESTful网络请求框架,RESTful是目前流行的一套api设计的风格, 并不是标准。Retrofit的封装可以说是很强大,里面涉及到一堆的设计模式,可以通过注解直接配置请求,可以使用不同的http客户端,虽然默认是用http ,可以使用不同Json Converter 来序列化数据,同时提供对RxJava的支持,使用Retrofit + OkHttp + RxJava + Dagger2 可以说是目前比较潮的一套框架,但是需要有比较高的门槛。

Volley VS OkHttp

Volley的优势在于封装的更好,而使用OkHttp你需要有足够的能力再进行一次封装。而OkHttp的优势在于性能更高,因为 OkHttp基于NIO和Okio ,所以性能上要比 Volley更快。IO 和 NIO这两个都是Java中的概念,如果我从硬盘读取数据,第一种方式就是程序一直等,数据读完后才能继续操作这种是最简单的也叫阻塞式IO,还有一种是你读你的,程序接着往下执行,等数据处理完你再来通知我,然后再处理回调。而第二种就是 NIO 的方式,非阻塞式, 所以NIO当然要比IO的性能要好了,而 Okio是 Square 公司基于IO和NIO基础上做的一个更简单、高效处理数据流的一个库。理论上如果Volley和OkHttp对比的话,更倾向于使用 Volley,因为Volley内部同样支持使用OkHttp,这点OkHttp的性能优势就没了, 而且 Volley 本身封装的也更易用,扩展性更好些。

OkHttp VS Retrofit

毫无疑问,Retrofit 默认是基于 OkHttp 而做的封装,这点来说没有可比性,肯定首选 Retrofit。

Volley VS Retrofit

这两个库都做了不错的封装,但Retrofit解耦的更彻底,尤其Retrofit2.0出来,Jake对之前1.0设计不合理的地方做了大量重构, 职责更细分,而且Retrofit默认使用OkHttp,性能上也要比Volley占优势,再有如果你的项目如果采用了RxJava ,那更该使用 Retrofit 。所以这两个库相比,Retrofit更有优势,在能掌握两个框架的前提下该优先使用 Retrofit。但是Retrofit门槛要比Volley稍高些,要理解他的原理,各种用法,想彻底搞明白还是需要花些功夫的,如果你对它一知半解,那还是建议在商业项目使用Volley吧。
  • 18、两个Activity 之间跳转时必然会执行的是哪几个方法?
首先定义两个Activity,分别为A和B。

当我们在A中激活B时,A调用onPause()方法,此时B出现在屏幕时,B调用onCreate()、onStart()、onResume()。

这个时候B【B不是一个透明的窗体或对话框的形式】已经覆盖了A的窗体,A会调用onStop()方法。
  • 19、Android Handler消息机制
作用:
跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。

四要素:
1、 Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
2、MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
3、 Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
4、 Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。

Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息; 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next(); 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息
  • 20、synchronized volatile关键字有什么区别?以及还有哪些同样功能的关键字
(1) volatile是变量修饰符,而synchronized则作用于一段代码或者方法。
(2) volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
const、final、lock
  • 21、界面卡顿的原因有哪些?
UI线程(main)有耗时操作
视图渲染时间过长,导致卡顿
  • 22、GC机制
    垃圾回收需要完成两件事:找到垃圾,回收垃圾。
找到垃圾一般的话有两种方法:

引用计数法
当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
可达性分析法
我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。

回收垃圾的话有这么四种方法:

标记清除算法
顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的碎片化。
复制算法
复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要两倍的内存。
标记整理算法:
标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会将存活的对象压缩,避免了内存的碎片化。
分代算法
分代算法将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率。
新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。
老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法。

  • 23、JVM内存区域的划分,哪些区域会发生 OOM
JVM 的内存区域可以分为两类:线程私有和=区域和线程共有的区域。
线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈
线程共有的区域:堆、方法区、运行时常量池

1、程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。

2、JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。

3、本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。

4、堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代。

5、方法区。方法区主要存放类的结构信息,比如静态属性和方法等等。

6、运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息。

其实除了程序计数器,其他的部分都会发生 OOM。

1、堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。

2、JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。
方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。
  • 24、类加载过程
Java 中类加载分为 3 个步骤:加载、链接、初始化。

加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 生成Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。
链接。 链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。
验证。 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。
准备。 这一步会创建静态变量,并为静态变量开辟内存空间。
解析。 这一步会将符号引用替换为直接引用。
初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。
  • 25、双亲委派模型
类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。

启动类加载器主要加载 jre/lib下的jar文件。
扩展类加载器主要加载 jre/lib/ext 下的jar文件。
应用程序类加载器主要加载 classpath 下的文件。
所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。
  • 26、HashMap 的原理
HashMap 的内部可以看做数组+链表的复合结构。数组被分为一个个的桶(bucket)。哈希值决定了键值对在数组中的寻址。具有相同哈希值的键值对会组成链表。需要注意的是当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构。

把握HashMap的原理需要关注4个方法:hash、put、get、resize。

1、hash方法。 将 key 的 hashCode 值的高位数据移位到低位进行异或运算。这么做的原因是有些 key 的 hashCode 值的差异集中在高位,而哈希寻址是忽略容量以上高位的,这种做法可以有效避免哈希冲突。

2、put 方法。 put 方法主要有以下几个步骤:
- 通过 hash 方法获取 hash 值,根据 hash 值寻址。
- 如果未发生碰撞,直接放到桶中。
- 如果发生碰撞,则以链表形式放在桶后。
- 当链表长度大于阈值后会触发树化,将链表转换为红黑树。
- 如果数组长度达到阈值,会调用 resize 方法扩展容量。

3、get方法。 get 方法主要有以下几个步骤:
- 通过 hash 方法获取 hash 值,根据 hash 值寻址。
- 如果与寻址到桶的 key 相等,直接返回对应的 value。
- 如果发生冲突,分两种情况。如果是树,则调用 getTreeNode 获取 value;如果是链表则通过循环遍历查找对应的 value。

4、resize 方法。 resize 做了两件事:
- 将原数组扩展为原来的 2 倍
- 重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。
  • 27、ConcurrentHashMap、HashMap,HashTable区别
1. HashMap不是线程安全:
在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的

2. HashTable是线程安全的:
HashTable和HashMap的实现原理几乎一样,
差别:
    1.HashTable不允许key和value为null;
    2.HashTable是线程安全的。

HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞

3、ConcurrentHashMap是线程安全的:
JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想

DK1.8版本:做了2点修改:

    1、取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
    2、将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.
  • 27、sleep 和 wait 的区别
- sleep 方法是 Thread 类中的静态方法,wait 是 Object 类中的方法
- sleep 并不会释放同步锁,而 wait 会释放同步锁
- sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步代码块中使用
- sleep 中必须传入时间,而 wait 可以传,也可以不传,不传时间的话只有 notify 或者 notifyAll 才能唤醒,传时间的话在时间之后会自动唤醒
  • 28、join 的用法
    join 方法通常是保证线程间顺序调度的一个方法,它是 Thread 类中的方法。比方说在线程 A 中执行线程 B.join(),这时线程 A 会进入等待状态,直到线程 B 执行完毕之后才会唤醒,继续执行A线程中的后续方法。

 join 方法可以传时间参数,也可以不传参数,不传参数实际上调用的是 join(0)。它的原理其实是使用了 wait 方法,join 的原理如下:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
  • 29、final、finally、finalize区别
final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。

finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。

finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。
  • 30、Java 中单例模式
    Java 中常见的单例模式实现有这么几种:饿汉式、双重判断的懒汉式、静态内部类实现的单例、枚举实现的单例。
双重判断的懒汉式:
public class SingleTon {
    //需要注意的是volatile
    private static volatile SingleTon mInstance;

    private SingleTon() {

    }
    public static SingleTon getInstance() {
        if (mInstance == null) { 
            synchronized (SingleTon.class) {
                if (mInstance == null) {
                    mInstance=new SingleTon();
                }
            }
        }

        return mInstance;
    }
}

双重判断的懒汉式单例既满足了延迟初始化,又满足了线程安全。通过 synchronized 包裹代码来实现线程安全,通过双重判断来提高程序执行的效率。这里需要注意的是单例对象实例需要有 volatile 修饰,如果没有 volatile 修饰,在多线程情况下可能会出现问题。原因是这样的,mInstance=new SingleTon()这一句代码并不是一个原子操作,它包含三个操作:
- 给 mInstance 分配内存
- 调用 SingleTon 的构造方法初始化成员变量
- 将 mInstance 指向分配的内存空间(在这一步 mInstance 已经不为 null 了)

我们知道 JVM 会发生指令重排,正常的执行顺序是1-2-3,但发生指令重排后可能会导致1-3-2。我们考虑这样一种情况,当线程 A 执行到1-3-2的3步骤暂停了,这时候线程 B 调用了 getInstance,走到了最外层的if判断上,由于最外层的 if 判断并没有 synchronized 包裹,所以可以执行到这一句,这时候由于线程 A 已经执行了步骤3,此时 mInstance 已经不为 null 了,所以线程B直接返回了 mInstance。但其实我们知道,完整的初始化必须走完这三个步骤,由于线程 A 只走了两个步骤,所以一定会报错的。

解决的办法就是使用 volatile 修饰 mInstance,我们知道 volatile 有两个作用:保证可见性和禁止指令重排,在这里关键在于禁止指令重排,禁止指令重排后保证了不会发生上述问题。
静态内部类实现的单例
class SingletonWithInnerClass {

    private SingletonWithInnerClass() {

    }
    private static class SingletonHolder{
        private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
    }
    public SingletonWithInnerClass getInstance() {
        return SingletonHolder.INSTANCE;
    }

}
由于外部类的加载并不会导致内部类立即加载,只有当调用 getInstance 的时候才会加载内部类,所以实现了延迟初始化。由于类只会被加载一次,并且类加载也是线程安全的,所以满足我们所有的需求。静态内部类实现的单例也是最为推荐的一种方式。
  • 31、Java中引用类型的区别,具体的使用场景
Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。

强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。
软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。
弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。
虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。
  • 32、Exception 和 Error的区别
Exception 和 Error 都继承于 Throwable,在 Java 中,只有 Throwable 类型的对象才能被 throw 或者 catch,它是异常处理机制的基本组成类型。

Exception 和 Error 体现了 Java 对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。

Error 是指在正常情况下,不大可能出现的情况,绝大部分 Error 都会使程序处于非正常、不可恢复的状态。既然是非正常,所以不便于也不需要捕获,常见的 OutOfMemoryError 就是 Error 的子类。

Exception 又分为 checked Exception 和 unchecked Exception。

checked Exception 在代码里必须显式的进行捕获,这是编译器检查的一部分。
unchecked Exception 也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误,具体根据需求来判断是否需要捕获,并不会在编译器强制要求。
  • 33、Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗?
然后我们分解问题,一点点去看这个问题。

## Looper.loop和主线程的关系

分析Handler和应用启动的时候讲过,在创建ActivityThread时,主线程会创建Looper,并且进入Loop循环:

<pre class="prism-token token  language-javascript" style="font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background-color: rgb(80, 85, 107); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; overflow: auto; font-family: Consolas, Monaco, &quot;Andale Mono&quot;, &quot;Ubuntu Mono&quot;, monospace; word-wrap: normal; text-align: left; white-space: pre; word-break: normal; line-height: 1.5; tab-size: 4; -webkit-hyphens: none; font-size: 14px;">public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        SamplingProfilerIntegration.start();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        // 这里sMainLooper赋值
        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();  // Looper进入循环

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }</pre>

loop()函数代码就不贴了,就是一个`while(true)`的死循环。

所以Looper和主线程的关系是,主线程创建的时候会创建一个MainLooper,并且进入循环。

<pre class="prism-token token  language-javascript" style="font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background-color: rgb(80, 85, 107); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; overflow: auto; font-family: Consolas, Monaco, &quot;Andale Mono&quot;, &quot;Ubuntu Mono&quot;, monospace; word-wrap: normal; text-align: left; white-space: pre; word-break: normal; line-height: 1.5; tab-size: 4; -webkit-hyphens: none; font-size: 14px;">Looper.prepareMainLooper();
Looper.loop();</pre>

## 主线程阻塞

### 关于死循环

主线程进入一个死循环,是不是就会被阻塞?

首先,思考一下,如果我们创建一个线程做定时检查某个状态,是不是也会给这个子线程做一个死循环,不断地去循环检查状态。当不需要这个线程的时候,改变flag让这个子线程退出循环并销毁。

想到这就理解,主线程也是一个线程,它也要维持自己的周期,所以也是需要一个死循环的。所以死循环并不是那么让人担心。

### 关于阻塞

这就涉及到Looper的作用了,Looper里持有一个MessageQueue,这个[消息队列](https://cloud.tencent.com/product/cmq?from=10680)存放着外部发来的消息,当有消息过来的时候,Looper就会按顺序把消息一个一个拿出来,进行处理。Looper的loop循环就是一个拿消息的循环。

也就是说,如果你给我发消息,我会立即去拿消息并且做响应。你不给我消息,我就会阻塞,减少CPU消耗(涉及到epoll)。

那么主线程会响应什么消息呢?在ActivityThread里有一个命名为H的handler,它处理所有Activity生命周期有关的事件。因为主线程就是UI线程,当UI发生变化,相关消息就会传进来,Looper就会处理消息。

所以:

**Looper的阻塞,前提是没有输入事件,此时MessageQueue是空的,Looper进入空闲,线程进入阻塞,释放CPU,等待输入事件的唤醒。**

## 聊聊ANR

其实担心这个问题的人很多都是被ANR搞怕了,因为ANR就是UI线程做耗时操作了导致卡死状态,然后很多人就在想是不是UI线程进入Loop死循环后,就出现卡死,其实这两个并不是一个问题。

先上结论和上面的做个对比:

**UI耗时导致卡死,前提是要有输入事件,此时MessageQueue不是空的,Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长(一般5秒),而且与此期间其他的事件(按键按下,屏幕点击..)都没办法处理(卡死),然后就ANR异常了。**

ANR机制的目的还是为了监测主线程的耗时操作,譬如密集CPU运算、大量IO、复杂界面布局等,因为这些都会降低应用程序的响应能力。所以从理念上也能理解,loop死循环只是简单地处理轻量的消息操作,和ANR并没有关系。

  • 34、类的成员变量按照什么顺序初始化的?
成员变量按照其声明的顺序会被初始化,并且立刻被初始化为二进制的0,这个动作发生在所有事件之前,也就是编译器会立刻将分配给对象的空间初始化。

最后就是调用类的构造方法了。

执行顺序:
执行父类静态代码 执行子类静态代码
初始化父类成员变量(我们常说的赋值语句)
初始化父类构造函数
初始化子类成员变量
初始化子类构造函数

35、java中public,private,protected和default的区别

作用域 当前类 同一package 子孙类 其他package
public
protected ×
default × ×
private × × ×

注意:java的访问控制是停留在编译层的,也就是它不会在.class文件中留下任何的痕迹,只在编译的时候进行访问控制的检查。其实,通过反射的手段,是可以访问任何包下任何类中的成员,例如,访问类的私有成员也是可能的。

区别:

public:可以被所有其他类所访问
private:只能被自己访问和修改
protected:自身、子类及同一个包中类可以访问
default:同一包中的类可以访问,声明时没有加修饰符,认为是friendly

  • 36、

推荐阅读更多精彩内容