《Android秘籍.第五卷》
目录
谈谈垃圾回收机制?为什么引用计数器判定对象是否回收不可行?知道哪些垃圾回收算法?
Java中引用有几种类型?在Android中常用于什么情景?
工作内存和主内存的关系?在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?
synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?
HashMap在put、get元素的过程?体现了什么数据结构?
《JVM篇》
JVM是什么?
- JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM内存是如何划分的?
具体划分为如下5个内存空间如运行时数据区图所示。
- 栈(虚拟机栈):存放局部变量
- 堆:存放所有new出来的东西
- 方法区:被虚拟机加载的类信息、常量、静态常量等。
- 程序计数器(和系统相关)
- 本地方法栈
谈谈垃圾回收机制?为什么引用计数器判定对象是否回收不可行?知道哪些垃圾回收算法?
- 垃圾回收机制:收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。
- java垃圾回收算法之-引用计数器,这个算法其中一个优点便是,实时性,只要对象的引用计数器的值为0,则立刻回收。接下来介绍的标记清除算法,当对象的引用计数器的值为0时,不会立刻被回收的。
- 常见的算法:
- 引用计数法(具体是对于对象设置一个引用计数器,每增加一个变量对它的引用,引用计数器就会加1,没减少一个变量的引用, 引用计数器就会减1,只有当对象的引用计数器变成0时,该对象才会被回收)
- 标记清除法:将垃圾回收分成了两个阶段:标记阶段和清除阶段。在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。 在清除阶段,清除掉所以的未被标记的对象。
- 标记压缩清除法(Java中老年代采用)
- 复制算法(Java中新生代采用)。
- 分代法(Java堆采用)(根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率)
- 分区算法(将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间)
Android GC机制以及其工作方式
- GC说白了就是区分出有用的对象和无用的对象,标记无用的对象并对其进行回收。所谓“有用的对象”其实就是存在其他的程序持有此对象的引用,反之则是无用的对象。而标记无用的对象的方法称为“垃圾标记算法”,可分为“引用计数算法”和“根搜索算法”。
- 垃圾收集主要有两种形式:(自动)自动会不定期进行回收,以释放无用的空间,(手动)手工调用的是System类中的gc()方法.
- GC执行时机:eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM。(注:java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。年轻代可以分为3个区域:Eden区(伊甸园,即首次分配的区域)和两个存活区(Survivor 0(幸存者) 、Survivor 1))
Java中引用有几种类型?在Android中常用于什么情景?
- 强引用:指在程序代码中普遍存在的,类似于Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:是用来描述一些还有用但并非必要的对象,对于软引用关联的对象,在系统将要发生溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够内存,才会抛出内存溢出异常。
- 弱引用:也用来描述非必须对象的,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集工作时,无论当前内存是否足够,都会回收掉只被弱引用的对象(Android多用于静态内部类+弱引用的方式创建Handler,防止内存泄漏)
- 虚引用:也成为幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置成虚引用关联的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
类加载的全过程是怎样的?什么是双亲委派模型?
类的加载分为三步:加载、连接、初始化。(加载(Loading)>验证(Verification)>准备(Preparation)>解析(Resolution)>初始化(Initialization)>使用(Using)>卸载(Unloading))
双亲委派模型是JVM中类加载的机制,双亲委派模型要求除顶层启动类加载器外其余类加载器都应该有自己的父类加载器;类加载器之间通过复用关系来复用父加载器的代码。(优点:每一个类都只会被加载一次,避免了重复加载,每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它),有效避免了某些恶意类的加载)
双亲委派机制、意义、方法
机制 | 启动(Bootstrap)类加载器----->标准扩展(Extension)类加载器--->系统(System)类加载器---->上下文(Custom)类加载器 从左到右加载:首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
|
意义 | 防止内存中出现多份同样的字节码
使用委托机制,会递归的向父类查找,如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器去加载类B,如果A加载器已加载类A,那么B使用A的类加载器进行加载时,就不会在加载类A的字节码了 |
方法 | 《1》启动(Bootstrap)类加载器 《2》标准扩展(Extension)类加载器 《3》应用程序类加载器(Application ) 《4》上下文(Custom)类加载器 |
工作内存和主内存的关系?在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?
- 所有线程共享主内存(主存);每个线程有自己的工作内存(寄存器)
- 原子性:是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行.(Java内存模型还提供了lock和unlock操作来满足这种需求)
- 可见性:就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。(Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的)
- 有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。(Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性)
JVM、Dalvik、ART的区别?
- JVM即Java Virtual Machine(Java虚拟机)
- Dalvik是Google公司自己设计用于Android平台的Java虚拟机。它可以支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik应用作为独立的Linux进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
- ART代表AndroidRuntime,Dalvik是依靠一个Just-In-Time(JIT)编译器去解释字节码,运行时编译后的应用代码都需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。
- ART则完全改变了这种做法,在应用安装的时候就预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)预编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。
- Dalvik与JVM的区别
- Dalvik指令集是基于寄存器的架构,执行特有的文件格式——dex字节码(适合内存和处理器速度有限的系统)。而JVM是基于栈的。相对于基于栈的JVM而言,基于寄存器的Dalvik VM实现虽然牺牲了一些平台无关性,但是它在代码的执行效率上要更胜一筹。
- 每一个Android 的App是独立跑在一个VM中的。因此一个App crash只会影响到自身的VM,不会影响到其他。Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个 Dalvik应用作为一个独立的Linux进程执行。
- 2. Dalvik与ART的区别
- 在Dalvik下,应用每次运行都需要通过即时编译器(JIT)将字节码转换为机器码,即每次都要编译加运行,这虽然会使安装过程比较快,但是会拖慢应用的运行效率。而在ART 环境中,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,这样的话,虽然设备和应用的首次启动(安装慢了)会变慢,但是以后每次启动执行的时候,都可以直接运行,因此运行效率会提高。
- ART占用空间比Dalvik大(原生代码占用的存储空间更大,字节码变为机器码之后,可能会增加10%-20%),这也是著名的“空间换时间大法"。
- 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。
Java中堆和栈的区别?
- 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。应该主要想描述的是堆内存和栈内存的区别(java把内存分为两种)。
- 堆和栈是两个不同的概念。堆(heap)上分配的内存,系统不释放,而且是动态分配的。栈(stack)上分配的内存系统会自动释放,它是静态分配的。
- 堆内存是是Java内存中的一种,它的作用是用于存储Java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。
- 栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量
- 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享
《并发篇》
开启一个线程的方法有哪些?销毁一个线程的方法呢?
- Android启动线程和JAVA一样有两种方式:
- 直接Thread类的start方法,也就是一般写一个自己的类来继承Thread类。
- 另外一种方式其实和这个差不多啊! 那就是Runnable接口,然后把Runnable的子类对象传递给Thread类再创建Thread对象.总之都是需要创建Thread对象,然后调用Thread类的start方法启动线程。
- 区别就是,一个是直接创建Thread对象,另外一个是需要implement了Runnable接口对象作为创建Thread对象的参数。Runnable其实我们称为线程任务。
- 可以通过销毁Handler方法或者销毁Thread方法(设置一个全局变量,在退出时将这个变量设置为true,thread中判断到这个值为true时退出thread的循环)
同步和非同步、阻塞和非阻塞的概念
- 同步(synchronization):调用时,在没有得到结果之前,该调用就不返回 (SendMessage。)
- 异步:调用者不会立刻得到结果,调用发出后,通过状态(效率低)、通知,或回调函数来通知调用者(PostMessgae)
- 同步异步的区别:如何通知调用者得到调用结果,同步等待调用完成,异步通过状态、通知,或回调函数。
- 阻塞:调用结果返回之前,当前线程会被挂起。socket的read/write/recv
- 同步与阻塞的区别:同步当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
- 非阻塞:不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
- 异步与非阻塞的区别:非阻塞是立刻返回调用结果,异步不会立刻返回调用结果,而是通过状态,通知,回调。
- 异步与阻塞的关系:异步也会调用阻塞的函数,比如用select 函数,当select 返回可读时再去read 一般都不会被阻塞,但是read是一个阻塞的函数。
- 同步异步和阻塞非阻塞之间的区别:同步异步是如何通知的概念,阻塞和非阻塞是如何执行的概念。
Thread的join()有什么作用?
- 让父线程等待子线程结束之后才能继续运行。( 当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。)
yield()和join()方法
- yield()暂停当前正在执行的线程对象。让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。
- join()方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
线程有哪些状态?
- 新建状态(new)、就绪状态、运行状态(running)、阻塞状态(blocked)及死亡状态(dead)。
什么是线程安全?保障线程安全有哪些手段?
- 存在竞争的线程不安全,不存在竞争的线程就是安全的。
- 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
- 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
- java监视器模式。 一直使用某一对象的锁来保护某状态。
- 线程安全委托。 将类的线程安全性委托给某个或多个线程安全的状态变量。(注意多个时,这些变量必须是彼此独立,且不存在相关联的不变性条件。
synchronized和volatile的区别?
- synchronized叫做同步锁,是Lock的一个简化版本
- volatile是一个类型修饰符,用来修饰被不同线程访问和修改的变量,当值被一个线程更改后,该值会在缓存中更新,保持一致
- 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比例还是比较大的。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。
- 关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键在解决的是多个线程之间访问资源的同步性。
synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?
- 锁住的是对象。通过执行顺序判断出来
sleep()和wait()的区别?
- 最主要是sleep方法没有释放锁,而wait方法释放了锁
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
- sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程
- wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
《集合篇》
Java集合框架中有哪些类?都有什么特点
- Collection:代表一组对象,每一个对象都是它的子元素
- Set:不包括重复元素的Collection
- List:有顺序的Collection,并且可以包含重复元素
- Map:可以把键(key)映射到值(value)的对象,键不能重复
集合、数组、泛型的关系,并比较
- 集合(ArrayList):通俗的说,集合就是一个放数据的容器,准确的说是放数据对象引用的容器。
- 数组(Array):数组是一组相关数据的集合,一个数组实际上就是一连串的变量,数组按照使用可以分为一维数组、二维数组、多维数组
- 泛型:即参数化类型
- 集合里所有的元素都是Object,如果元素是值类型,会自动装箱。
- 泛型集合可以定义元素类型,相对于集合,泛型集合可以避免装箱拆箱,提高性能,同时程序具有更好的可读性。
- 数组本身可以认为是一种泛型集合结构体
ArrayList和LinkList的区别?
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
- 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
- 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。
- 在ArrayList集合中添加或者删除一个元素时,当前的列表所所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。
- LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。 4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
ArrayList和Vector的区别?
- Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
- 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
HashSet和TreeSet的区别?
- 1、TreeSet 是二差树实现的,Treeset中的数据是自动排好序的,不允许放入null值。
- 2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
- 3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
HashMap和Hashtable的区别?
比较 | HashMap | HashTable |
存储结构 | 数组 + 链表/红黑树 | 数组 + 链表 |
扩容方式 | oldCap * 2 | oldCap * 2 + 1 |
K,V能否为null | key, value 均可以为 null | key, value 均不可以为 null |
线程是否安全 | 线程不安全 | 线程安全 |
HashMap在put、get元素的过程?体现了什么数据结构?
在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构。它之所以有相当 快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap通过key的hashCode来计算hash值,不同的hash值就存在数组中不同的位置,当多个元素的hash值相同时(所谓hash冲突),就采用链表将它们串联起来(链表解决冲突),放置在该hash值所对应的数组位置上。在JDK1.8中,HashMap的存储结构已经发生变化,它采用数组+链表+红黑树这种组合型数据结构。当hash值发生冲突时,会采用链表或者红黑树解决冲突。当同一hash值的结点数小于8时,则采用链表,否则,采用红黑树。
如何解决Hash冲突?
- hash冲突:就是根据key即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有人先来了。就是说这个地方被抢了啦。这就是所谓的hash冲突啦。
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法(Java hashmap就是这么做的)
- 建立一个公共溢出区
如何保证HashMap线程安全?什么原理?
- HashMap(不是线程安全的,但是它的效率会比Hashtable要好很多)在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环,一旦成环,Entry的next节点永远不为空,产生死循环.
- 方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的。 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现。
- 方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
HashMap是有序的吗?如何实现有序?
- HashMap 的一个功能缺点是它的无序性,被存入到 HashMap 中的元素,在遍历 HashMap 时,其输出是无序的。如果希望元素保持输入的顺序,可以使用 LinkedHashMap 替代。还可用TreeMap 则根据元素的 Key 进行排序。
HashMap是如何扩容的?如何避免扩容?
- 扩容:就是重新计算容量,
- HashMap的自动扩容,当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
hashcode()的作用,与equal()有什么区别?
- hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致,那么equal()既然已经能实现对比的功能了,为什么还要hashCode()呢?
- 因为重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?
- 因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
- 1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
- 2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
CurrentHashMap原理?
- ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap
- CocurrentHashMap是由Segment数组和HashEntry数组组成。
- Segment是重入锁(ReentrantLock),作为一个数据段竞争锁,每个HashEntry一个链表结构的元素,利用Hash算法得到索引确定归属的数据段,也就是对应到在修改时需要竞争获取的锁。
List、Set、Map的区别?
- List、Set是实现了Collection接口的子接口;而Map是另一个集合接口。
- List中的元素,有序、可重复、可为空;
- Set中的元素,无序、不重复、只有一个空元素;
- Map中的元素,无序、键不重,值可重、可一个空键、多可空值;