类构造顺序
- 先父亲,再孩子
- 先静态再非静态
- 先字段,后构造器(字段先后有定义顺序决定)
- 先代码块 后构造方法
HashMap
- 存储结构是开散列表:地址向量+同义词子表=数组+单链表。
- 解决哈希冲突的办法是拉链法:将相同散列地址的键值存放在同义词子表中。
- capacity为啥要为2的幂次,是为了用位与运算代替取模运算,提高性能。
- 为啥loadFactor 是0.75,因为中庸,若为1,则频繁冲突,若为更小值,则会频繁扩容。
- 构造~时,并没有初始化地址向量,而是要等到put操作是才构造
- 遍历HashMap的顺序是从地址向量的第一个开始,先从前到后遍历同义词子表,然后下一个同义词子表
- HashMap通过hash算法先定位到地址向量中对应的位置,然后遍历同义词子表
- HashMap不是线程安全的,当~扩容的时候要进行迁移,多线程并发put会导致迁移出环。建议使用Hashtable或者ConcurrentHashMap。Hashtable将put和get方法都加上了synchronized,性能较差
WeakHashMap
- 用于存放键值对,当发生gc时,其中的键值对可能被回收。适用于对内存敏感的缓存
- 存放键值对的Entry继承自WeakReference。当发生gc时,Entry被回收并加入到ReferenceQueue中
- 访问~时,会将已经gc的键值对从中删除(通过遍历ReferenceQueue)
LinkedHashMap
- 是一个有序 map,可以按插入顺序或者访问顺序排列
- 在 hashMap 基础上增加了头尾指针形成双向链表,继承 Node 添加前后结点的指针,每次构建结点时会将他链接到链尾。
- 若是按访问顺序排序,存取键值对的时候会将其拆下插入到链尾,链头是最老的结点,满时会被移出
- 按访问顺序来排序是LRU缓存的一种实现。
ThreadLocal
- 用于将对象和当前线程绑定(将对象存储在当前线程的ThreadLocalMap结构中)
- ThreadLocalMap是一个类似HashMap的存储结构,键是ThreadLocal对象的弱引用,值是要保存的对象
- set()方法会获取当前线程的ThreadLocalMap对象
- threadLocal内存泄漏:key是弱引用,gc后被回收,value 被entry持有,再被ThreadLocalMap持有,再被线程持有,如果线程没有结束,则value无法访问到,也无法回收,方案是及时remove掉不用的value
- threadlocal 会自动清理key为null 的entry
内存泄漏
内存泄漏是因为堆内存无法释放 android内存泄漏就是生命周期长的对象持有了生命周期较短对象的引用
- 静态成员变量(单例)
- 静态成员变量的生命周期和整个app一样,如果它持有短生命周期的对象则会导致这些对象内存泄露
- 静态变量在不使用时需要置空
- 静态变量使用弱引用持有Activity
- 单例持有App context而不是Activity Contex
- 静态方法可以被子类隐藏,而不是重写
- 非静态内部类
- 匿名内部类持有外部类引用
- handler是典型的匿名内部类,handler中的消息持有handler引用,如果有未处理完的消息,则会导致handler外层类内存泄露,Looper -> MessageQueue -> Message -> Handler -> Activity,解决办法是静态内部类+Activity弱引用,并且在activity退出时清除所有消息
- new Thread()是典型的匿名内部类,如果Activity退出后Thread还在执行则会引起Activity内存泄露
- 集合类
- 集合对象会持有孩子的引用,需要及时清除且置空
- webview内存泄露
- WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存
引用
- 强引用
- 通过=显式的将对象A赋值给变量a,则A就存在一个强引用a
- 强引用需要显式的置null 以告诉gc该对象可以被回收
- 在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个Object会被回收。但是如果这个object是全局的变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收
- 清空list时需要遍历所有元素将其置null
- 软引用
- 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用
- 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
- 虚引用
- 虚引用主要用来跟踪对象被垃圾回收器回收的活动,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。用于在对象被回收时做一些事情
软引用、弱引用、虚引用的构造方法均可以传入一个ReferenceQueue与之关联。在引用所指的对象被回收后,引用(reference)本身将会被加入到ReferenceQueue之中,此时引用所引用的对象reference.get()已被回收 (reference此时不为null,reference.get()此时为null)。在一个非强引用所引用的对象回收时,如果引用reference没有被加入到被关联的ReferenceQueue中,则表示还有引用所引用的对象还没有被回收。如果判断一个对象的非强引用本该出现在ReferenceQueue中,实际上却没有出现,则表示该对象发生内存泄漏。
接口和抽象类
- 类可以实现很多个接口,但是只能继承一个抽象类
- 类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
字符串常量池
- JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池
- 字符串常量池实现的前提条件是java中的String对象是不可变的,否则多个引用指向同一个变量的时候并改变了String对象,就会发生错乱
- 字符串常量池是用时间换空间,cpu需要在常量池中寻找是否有相同字符串
- 字符串构造方式
- 字面量形式:String str = "droid" 使用这种形式创建字符串时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用
- 新建对象形式:String str = new String("droid"); 使用这种形式创建字符串时,不管字符串常量池中是否有相同内容,新的字符串总是会被创建。 对于上面使用new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池,可以使用intern方法。调用intern后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。
String str4 = str3.intern();
异常
- Exception和Error都继承于Throwable
- Exception是程序错误
- Exception又分为checked Exception(编译时异常)和unchecked Exception(运行时)。checked Exception在代码里必须显式的进行捕获,这是编译器检查的一部分。unchecked Exception也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误
- Error是比程序更加低层的错误, 包括虚拟机错误OutOfMemoryError,StackOverFlowError
注解
注解为代码添加一些额外的信息,以便稍后可以读取这些信息。这些信息可以帮助代码检查,编译时生成代码以减少模板代码
- 元注解
- @Retention:定义注解生命周期
- RetentionPoicy.SOURCE注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;用于做一些检查性的操作,比如 @Override
- RetentionPoicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;用于在编译时进行一些预处理操作,比如生成一些辅助代码(编译时注解即是编写生成代码的代码),ButterKnife 使用编译时注解,即在编译时通过自定义注释解析器AbstractProcessor读取注解并由此生成java文件(在里面调用了 findViewById)
- RetentionPoicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;用于在运行时去动态获取注解信息
- @Target:定义了Annotation所修饰的对象范围
内存模型
dalvik虚拟机内存空间被划分成多个区域 = 虚拟机栈+ 程序计数器+ 方法区+ 堆+ 本地方法栈
- 方法区:方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域被各个线程共享的内存区域。
- 堆区:又称动态内存分配,存放所有用通过new创建的类对象(包括该对象其中的所有成员变量),也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收
- 堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError
- 堆内存分为新生代和老年代和永生代,新生代又分为Eden、From Survivor、To Survivor三个区域
- 永生代用于存放class信息
- 虚拟机栈 :虚拟机栈是线程私有的数据结构,它用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧
- 栈帧(Stack Frame)
- 一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等
- 局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。
- 在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。
- 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError
- 本地方法栈和虚拟机栈类似,只不过用于执行native方法
- 程序计数器:每个线程都需要一个程序计数器,用于记录正在执行指令的地址
GC
- 垃圾定义:有两种定义垃圾的方法
- 引用计数:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器为0的对象就是垃圾
- 可到达性:从GC Roots作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。不可到达的对象是垃圾,被定义为垃圾的对象不代表马上会被回收,还会去检查是否要执行finalize方法
- GC种类:Minor GC、Full GC ( 或称为 Major GC )
- 垃圾回收回收不可到达的对象,即没有引用的对象,可到达的对象一定被根引用
- Minor GC 是发生在新生代中的垃圾收集动作,所采用的是copy and sweep(经过6次gc还存活的对象会被放到老年代)
- Full GC 是发生在老年代的垃圾收集动作,所采用的是mark and sweep
- 分代回收(generational collection):每个对象记录有它的世代(generation)信息。所谓的世代,是指该对象所经历的垃圾回收的次数。世代越久远的对象,在内存中存活的时间越久
- GC回收算法
- copy and sweep:内存被分为两个区域。对象总存活于两个区域中的一个。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到可到达对象,将可到达对象复制到空白区域中并紧密排列,修改由于对象移动所造成的引用地址的变化。最后,直接清空对象原先存活的整个区域,使其成为新的空白区域。适用于存活对象少,垃圾对象多的场景
- mark and sweep:每个对象将有标记信息,用于表示该对象是否可到达。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到所有的可到达对象,并标记(mark)。随后,JVM需要扫描整个堆,找到剩余的对象,并清空这些对象所占据的内存堆,缺点是容易产生内存碎片。适用于存活对象多,垃圾对象少的场景
- 分代回收算法:老年代每次gc只有少量对象被回收,而新生代有大量对象被回收,对于新生代采用copy and sweep,对老年代采用mark and sweep。
OOM类型
- 堆内存不足
- 无足够的连续内存空间
- 文件描述符超过数量限制
- 线程数量超过限制
- 虚拟内存不足
内存优化
- 使用内存友好的数据结构 SpareseArray,ArrayMap
- 避免内存泄漏,避免长生命周期对象持有短生命周期对象
- 使用池结构,复用对象避免内存抖动。
- 根据手机内存大小,设置内存缓存的大小。
- 多进程,扩大可使用内存。
- 通过ComponentCallback2 监听内存吃紧,进行内存缓存的释放。
LeakCanary
- 通过ActivityLifecycleCallbacks监听Activity生命周期,在onActivityDestroy时获取Activity实例,并为其构建弱引用并关联引用队列。
- 起异步线程,观察ReferenceQueue是否有Activity的弱引用,如果有则说明回收成功,否则回收失败
- 回收失败后会手动触发一次gc,再监听ReferenceQueue,如果还是回收失败,则dump内存
- LeakCanary 通过contentProvider安装
- 当一个Activity的onDestory方法被执行后,说明该Activity的生命周期已经走完,在下次GC发生时,该Activity对象应将被回收
equals()
- equals() 定义在JDK的Object.java中。可以定义两个对象是否相等的逻辑
- "=="相等判断符用于比较基本数据类型和引用类型数据。 当比较基本数据类型的时候比较的是数值,当比较引用类型数据时比较的是引用(指针)即指向堆内存的地址
- ==的语义是固定的,而equals()的语义是自定义的
hashCode()
- hashCode() 的作用是获取哈希码,它实际上是返回一个int整数。仅仅当创建并某个“类的散列表”(关于“散列表”见下面说明)时,该类的hashCode() 才有用,作用是:确定该类的每一个对象在散列表中的位置,Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet
- HashMap 如果使用equals判断key是否重复,需要逐个比较,时间复杂度为O(n),但如果使用hashCode(),因为它是一个int值。所以可以直接作为数组结构的某个索引值,如果该索引位置没有内容则表示key没有重复,复杂度为O(1)
- 如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。
- 如果两个对象hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。
sealed class
- 是一个继承结构固定的抽象类,即在编译时已经确定了子类数量,不能在运行时动态新增
- 它的子类只能声明在同一个包名下
- 是一个抽象类,且构造方法是私有的,它的孩子是final类,而且孩子声明必须嵌套在sealed class内部。
- 枚举的局限性 限制枚举每个类型只允许有一个实例 限制所有枚举常量使用相同的类型的值
crossinline
在具有inline 特性的同时,避免非局部返回,因为直接return掉函数会影响原有功能,crossinline的lambda内部必须使用局部返回,比如return@foo
sequence
- ~是惰性的:中间操作不会被执行,只有终端操作才会(toList())
- ~的计算顺序和迭代器不同:~是对一个元素应用全部的操作,然后第二个元素应用全部操作,而迭代器是对列表所有元素应用第一个操作,然后对列表所有元素应用第二个操作
虚拟内存
- 每个应用访问的地址空间是虚拟地址空间,所以可以无穷大,Linux负责将虚拟内存转换为物理地址。
- 为了方便将虚拟内存地址和物理地址进行映射,内存空间被分割成若干个页(通常是4kb大小)
- Memory Management Unit(MMU)这个硬件专门用于将虚拟地址转换为物理地址。它通过查询映射表得到物理地址
- 虚拟地址分为高4位的页号,和后面的偏移量,每次通过页号查询映射表得到物理地址的页号,然后再将偏移量拼在后面得到物理地址
kotlin 空安全
- 编译成java后是通过if判空实现空安全的
- kotlin和java交互的时候空安全被破坏,可以通过在java代码添加@NotNull注解进行非空约束
Channel
- 是一个挂起队列,和java 中 blocking queue 类似
- 生产者叫 SendChannel,消费者叫 ReceiveChannel
- 生产者和消费者之间有一条缓冲通道
- 执行多线程同时生产,多线程同时消费
- flow 只有订阅才生产数据,Channel 发送数据和订阅无关,所以是热流
协程
- 是建立在线程之上的,更轻量级的(用户态),更容易控制生命周期(结构化并发)的计算单元。
- 借助于suspend方法实现用户态非抢占式的并发调度,协程通过挂起主动让出执行权(普通的线程映射为内核线程,内核线程的调度是抢占cpu时间片)
- 比使用线程池更容易取消异步操作,享受结构化并发,异常处理
- 挂起方法并不会挂起线程,因为就像调用一个带回调的方法一样,它挂起的是协程剩下的代码。
结构化并发
java 线程间的并发是没有级联关系的,所以是非结构的
- 结束一个线程时,怎么同时结束这个线程中创建的子线程?
- 当某个子线程在执行时需要结束兄弟线程要做怎么做?
- 如何等待所有子线程都执行完了再结束父线程? 这些问题都可以通过共享标记位、CountDownLatch 等方式实现。但这两个例子让我们意识到,线程间没有级联关系;所有线程执行的上下文都是整个进程,多个线程的并发是相对整个进程的,而不是相对某一个父线程。
CPS
- Continuation Passing Style,传递剩余的计算,将剩余的计算作为一个回调传递给方法。
suspend
- cps+状态机: 每个suspend 方法都构建一个continuation不经济,一个协程块中的suspend方法会共用一个 continuation(持有一个label)。将原先不同的continuation写在了不同的 switch case 分支内,以挂起点为分割点。每执行一个分支点,label 就+1,表示进入下一个分支。挂起方法会被多次调用(invokeSuspend),因为label值不同每次都会走不同的分支
- suspend 的返回值标志着挂起方法有没有被挂起
Dispatcher
- 调度器CoroutineDispatcher是一个ContinuationInterceptor。通过interceptContinuation()将continuation包装成DispatchedContinuation
- 不同的调度器通过重写 dispatch方法实现不同的线程调度。有些是通过handler抛一个runnable,有些是向线程池抛一个
- default 属于cpu运算密集型:线程被阻塞时,cpu是在忙着运算
- io 属于io型:线程被阻塞时,cpu是闲着。
Job
- 可以被取消的任务
- 他有六种状态:new-active-completing-completed-cancelling-canceled
- 只有延迟启动的协程的job才会处于new 状态,其他都处于active状态,completing意味着自己的活干完了,在等子协程。 cancelling 是在取消协程之前最后的清理资源的机会。
- 新建的协程会继承父亲的CoroutineContext,除了其中的job,新协程会新建job并成为父job的子job
背压
- 生产速度大于消费速度
- 使用缓冲区,阻塞队列
- 缓冲区大小 & 缓冲区满之后的策略(丢弃最新,最久,挂起)
异常处理
- 在 coroutineScope中,异常是向上传播的,只要任意一个子协程发生异常,整个scope都会执行失败,并且其余的所有子协程都会被取消掉;
- 在 supervisorScope中,异常是向下传播的,一个子协程的异常不会影响整个 scope的执行,也不会影响其余子协程的执行;(重写了childCancelled并返回false) CancellationException 异常总是被忽略
取消协程
- 每个启动的协程都会返回一个job,调用 job.cancel()会让job处于canceling状态,然后在下一个挂起点抛出CancellationException ,如果协程中没有挂起点,则协程不能被取消。因为每个suspend 方法都会检查job是否活跃,若不活跃则抛出CancellationException ,这个异常只是给上层一次关闭资源的机会,可以通过try-catch 捕获
- 对于没有挂起的协程,需要通过while(isActive)来检查job是否被取消 或者 yield()
- 当协程抛出CancellationException 后,在启动协程将会被忽略