Java部分
首先关于Java基础知识点,主要包含以下内容,接口和抽象类区别,string.stringbuilder.stringbuffer三者区别,Java集合框架底层实现原理,线程和进程的理解,线程池得作用,线程的各种状态,gc垃圾回收方面得知识点,具体如下:
-
Java线程和多线程
-
说说JVM,它的程序计数器是干嘛用的?GCRoots的对象可以有哪些。
-
集合list和linkedList 区别
-
hashmap底层数据结构
-
Java同步方式有哪些
-
volatile关键字和synchronized关键字原理
-
jvm的相关东西
-
Integer的内容
-
Java线程池相关的内容
知识点:
1、接口和抽象类区别
Java中接口和抽象类的定义语法分别为interface与abstract关键字。
抽象类:在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:
a、抽象类不能被实例化只能被继承;
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。
接口 : Java中接口使用interface关键字修饰,特点为:
a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);
b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
c、一个类可以实现多个接口;
d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。
面试题:接口与抽象类的区别
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认protected,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
2、String 、StringBuilder、 StringBuffer区别
3、Java集合框架底层实现原理
List、Set、Map、Queue
- ArrayList
ArrayList 是基于动态数组实现,容量能自动增长的集合。随机访问效率高,随机插入、随机删除效率低。线程不安全,多线程环境下可以使用 Collections.synchronizedList(list) 函数返回一个线程安全的 ArrayList 类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类
- LinkedList
LinkedList 是可以在任何位置进行插入和移除操作的有序集合,它是基于双向链表实现的,线程不安全。LinkedList 功能比较强大,可以实现栈、队列或双向队列。
- Vector
Vector 是矢量队列,也是基于动态数组实现,容量可以自动扩容。跟 ArrayList 实现原理一样,但是 Vector 是线程安全,使用 Synchronized 实现线程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector。
- Stack
Stack 是栈,先进后出原则,Stack 继承 Vector,也是通过数组实现,线程安全。因为效率比较低,不推荐使用,可以使用 LinkedList(线程不安全)或者 ConcurrentLinkedDeque(线程安全)来实现先进先出的效果。
- CopyOnWriteArrayList
CopyOnWriteArrayList 是线程安全的 ArrayList,写操作(add、set、remove 等等)时,把原数组拷贝一份出来,然后在新数组进行写操作,操作完后,再将原数组引用指向到新数组。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。
CopyOnWriteArrayList 写操作加了锁,不然多线程进行写操作时会复制多个副本;读操作没有加锁,所以可以实现并发读,但是可能读到旧的数据,比如正在执行读操作时,同时有多个写操作在进行,遇到这种场景时,就会都到旧数据。
- HashMap
HashMap 是以key-value 键值对形式存储数据,允许 key 为 null(多个则覆盖),也允许 value 为 null。底层结构是数组 + 双链表 + 红黑树。
如果是链表结构,则判断链表长度是否大于阈值 8,如果 >=8 并且数组长度 >=64 才转为红黑树,如果 >=8 并且数组长度 < 64 则进行扩容;
- HashTable
和HashMap一样,Hashtable 数据结构为数组+单向链表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
和 HashMap 一样,Hashtable 也是一个哈希散列表,Hashtable 继承于 Dictionary,使用重入锁 Synchronized 实现线程安全,key 和 value 都不允许为 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。HashTable 默认的初始大小为 11,之后每次扩充为原来的 2 倍,底层结构是数组 + 单链表 。
- ConcurrentHashMap
ConcurrentHashMap 在 Java 7 和 Java 8 中的区别:
ConcurrentHashMap在 Java 8 版本中丢弃了 Segment(分锁段)、ReentrantLock、HashEntry 数组这些概念,而是采用CAS + Synchronized 实现锁操作,Node 改名为 HashEntry,引入了红黑树保证查询效率,底层数据结构由数组 + 链表 + 红黑树组成,Node 数组默认为 16,如果是链表结构,则判断链表长度是否大于阈值 8,如果 >=8 并且数组长度 >=64 才转为红黑树,如果 >=8 并且数组长度 < 64 则进行扩容;
Java 7 使用 Segment 分段加锁机制,Segment 继承 ReentrantLock 实现锁操作,而 Java 8 使用 CAS + Synchronized 实现锁操作;
Java 7 查询时遍历链表效率低,而 Java 8 采用红黑树提高查询效率;
Java 7 使用 HashEntry 存放数据,而 Java 8 改名为 Node,作用都是一样,存放的都是 hashcode、key、value、next 等数据。
Java 7 是把数据插入到链表的表头(头插法),而 Java 8 是将数据插入到链表的最后面(尾插法);
Java 7 先扩容,再插值,而 Java 8 先插值,再扩容;
Java 7 的最大并发个数是 Segment 的个数默认值是 16,锁住整个段,不影响其他段;而 Java 8 去掉了分段锁,更细粒度,只锁住一个 Node 节点,不影响其他 Node 节点;
Java 7 在扩容时锁住一个段,当前段可读不能写,其他段可读写,只开启一个线程执行扩容操作;而 Java 8 锁住一个 Node 结点,当前结点可读不能写,其他结点可读写,1 个线程执行扩容 + 可能多个 put()/remove 线程帮助扩容。
HashMap、Hashtable、ConccurentHashMap 三者的区别(Java 8):
HashMap 线程不安全,没有锁机制,数组 + 链表 + 红黑树
Hashtable 线程安全,锁住整个对象,数组 + 链表
ConccurentHashMap 线程安全,CAS+Synchronized,数组 + 链表 + 红黑树
HashMap 的 key 和 value 都可为 null,其他两个都不可以。
- TreeMap
TreeMap 实现了 SotredMap 接口,意味着可以排序,是一个有序的集合。底层数据结构是红黑树结构,TreeMap 中的每个元素都存放在红黑树的节点上,默认使用自然排序,也可以自定排序,线程不安全。
- LinkedHashMap
LinkedHashMap是HashMap的子类,只是增加了双向链表内容;
LinkedHashMap是有序的,基于双向链表实现的,accessOrder属性分为两种:true-访问顺序,false-插入顺序,不设置默认是false。
- IdentityHashMap
只使用了数组结构,与HashMap的区别为hash值算法不一样使用System.identityHashCode(x)来计算原始hash值,比如String类型,每个对象都有原始的hash值。
- WeakHashMap
弱引用map,数据结构为数组+单链表,把key包装成了弱应用,当key对象被回收时,entry也将被移出。
- ThreadLocalMap
线程本地map,数据结构为数组,不要使用基础类型作为WeakHashMap的key,每个Thread里都有一个ThreadLocalMap,主要为Thread提供本地变量解决方案。
当线程存在多个ThreadLocal时,其中的一个或几个ThreadLocal的引用在线程运行中,在没有执行remove的情况下被设置为null,即释放了强引用,ThreadLocal对象只有Entry的弱引用了。下次gc被回收,key是弱引用被回收,value是强引用一直存储在内存中。线程还在运行,value的内存就不会释放,这样发生了内存泄漏。
单个线程的情况下,线程结束的时候,内存泄漏的value会被回收,不会有什么问题。不过使用的线程池呢,线程是常备线程,会一直运行下去,这样value内存会被一直占用,并且可能会不断增加,这样就严重了。那需要怎么来避免和解决呢。
- HashSet
HashSet 是用来存储没有重复元素的集合类,并且是无序的。实现了 Set 接口。底层使用 HashMap 机制实现,所以也是线程不安全。
- TreeSet
TreeSet 实现了 SortedSet 接口,意味着可以排序,它是一个有序并且没有重复的集合类,底层是通过 TreeMap 实现。TreeSet 并不是根据插入的顺序来排序,而是字典自然排序。线程不安全。
TreeSet 支持两种排序方式:自然升序排序和自定义排序。
- LinkedHashSet
是使用LinkedHashMap来实现的,在HashMap的基础上增加双向链表。
- HashSet、TreeSet、LinkedHashSet 的区别
HashSet,TreeSet,LinkedHashSet 之间的区别:HashSet 只去重,TreeSet 去重并排序,LinkedHashSet 去重并保证迭代顺序。
- 队列Queue
队列(Queue)
Queue 是一个**先入先出(FIFO)**的集合,它有 3 种实现方式:阻塞队列、非阻塞队列、双向队列。Queue 跟 List、Set 一样,也是继承了 Collection 接口。
阻塞队列
阻塞队列是一个可以阻塞的先进先出集合,比如某个线程在空队列获取元素时、或者在已存满队列存储元素时,都会被阻塞。BlockingQueue 接口常用的实现类如下:
ArrayBlockingQueue :基于数组的有界阻塞队列,必须指定大小,只有一个ReentrantLock,出队和入队是不能同时进行的。
LinkedBlockingQueue :基于单链表的无界阻塞队列,不需指定大小,有两个ReentrantLock,出队和入队是可以同时进行的。
DelayedWorkQueue:实现的是一个优先队列,这里底层则是通过二叉堆实现的,二叉堆本质上是一个完全二叉树或近似完全二叉树,分为最大堆和最小堆两种方式,通常由数组作为基础数据结构进行实现。
DelayQueue:基于延迟、优先级、无界阻塞队列。
SynchronousQueue :基于 CAS 的阻塞队列。
常用方法:
add():新增一个元索,假如队列已满,则抛异常。
offer():新增一个元素,假如队列没满则返回 true,假如队列已满,则返回 false。
put():新增一个元素,假如队列满,则阻塞。
element():获取队列头部一个元素,假如队列为空,则抛异常。
peek():获取队列头部一个元素,假如队列为空,则返回 null。
remove():执行删除操作,返回队列头部的元素,假如队列为空,则抛异常。
poll():执行删除操作,返回队列头部的元素,假如队列为空,则返回 null。
take():执行删除操作,返回队列头部的元素,假如队列为空,则阻塞。
4、线程和进程的理解
进程与线程的区别总结:
- 本质区别: 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 包含关系: 一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 资源开销: 每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 影响关系: 一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。
衍生问题:
- 内存如何寻址?
- 如何终止线程?有几种方式?
- android binder怎么实现只需要一次拷贝的
1. 如何通信(沟通)的内容
通信是人的基本需求,进程与进程之间是相互独立的,也有通信需求。根据这一问题就可以展开内容提问:
- 进程/线程如何通信
答:进程可以通过管道、套接字、信号交互、共享内存、消息队列等等进行通信;而线程本身就会共享内存,指针指向同一个内容,交互很容易。
- 通信方式的差异,比如进程间共享内存和消息队列有何异同?
2. 创建线程的方式
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
5、线程池得作用
1.FixedThreadPool 固定线程数的线程池,核心线程数和最大线程数是一样,LinkedBlockingQueue无界单列表队列,拒绝策略AbortPolicy(超过最大数量时抛出异常)
2.CachedThreadPool缓存线程池,核心线程数量为0,最大线程数量为Integer.MAX_VALUE,空闲存活时间为60秒,SynchronousQueue(非公平,不存储元素),拒绝策略AbortPolicy(超过最大数量时抛出异常)
3.ScheduledThreadPool周期执行线程池,必须主动设置核心数,最大线程数量为Integer.MAX_VALUE,空闲存活时间为10秒,DelayedWorkQueue无界队列,数组实现的二叉树,拒绝策略AbortPolicy(超过最大数量时抛出异常)
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);延迟delay后执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); 延迟initialDelay后执行任务,下次周期执行时间为initialDelay+period,超过就完成立即执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); 延迟initialDelay后执行任务,下次周期执行时间为now+period
4.SingleThreadExecutor 单线程线程池,核心线程为1,最大线程数为1,空闲存活时间为0秒,LinkedBlockingQueue无界单列表队列,拒绝策略AbortPolicy(超过最大数量时抛出异常)
5.SingleThreadScheduledExecutor周期执行线程池,核心线程数量为1,最大线程数量为Integer.MAX_VALUE,空闲存活时间为10秒,DelayedWorkQueue无界队列,数组实现的二叉树,拒绝策略AbortPolicy(超过最大数量时抛出异常)
6.ForkJoinPool分治任务线程池,设计用来解决父子任务有依赖的并行计算问题的。 类似于快速排序、二分查找、集合运算等有父子依赖的并行计算问题,都可以用 ForkJoinPool 来解决。
采用了Fork/Join 框架
private ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, int mode, String workerNamePrefix) ;
parallelism:并发度,默认情况下跟我们机器的cpu个数保持一致,使用 Runtime.getRuntime().availableProcessors()可以得到我们机器运行时可用的CPU个数。
factory:创建新线程的工厂
handler:线程异常情况下的处理器
asyncMode:这个参数要注意,在ForkJoinPool中,每一个工作线程都有一个独立的任务队列,asyncMode表示工作线程内的任务队列是采用何种方式进行调度,可以是先进先出FIFO,也可以是后进先出LIFO。如果为true,则线程池中的工作线程则使用先进先出方式进行任务调度,默认情况下是false。
任务ForkJoinTask类型默认实现:
- RecursiveAction:可用于没有返回结果的任务。
- RecursiveTask :用于有返回结果的任务。
- CountedCompleter: 在任务完成执行后,触发自定义的钩子函数。
- 整体上 ForkJoinPool 是对 ThreadPoolExecutor 的一种补充
- ForkJoinPool 提供了其独特的线程工作队列绑定方式、工作分离以及窃取方式
- ForkJoinPool + ForkJoinTask 配合实现了 Fork/Join 框架
- 适用于任务可拆分为更小的子任务的场景(有点类似递归),适用于计算密集型任务,可以充分发挥 CPU 多核的能力
7.ExecutorhreadPoolExecutor总结:
上面介绍了5种线程池,查看源码能知道5种常用的线程池最终调用的都是ThreadPoolExecutor方法。
csharp
复制代码public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
ThreadPoolExecutor方法对应的5个参数如下:
corePoolSize: 线程池核心线程个数
maximunPoolSize: 线程池最大线程数量
keeyAliveTime: 空闲线程存活时间
TimeUnit: 存活时间单位
workQueue: 用于保存等待执行任务的阻塞队列
在ThreadPoolExecutor类中定义了四种拒绝策略:
AbortPolicy丢弃任务,并抛出异常RejectedExecutionException.默认策略。
DiscardPolicy 默默丢弃、不抛异常。
DiscardOldestPolicy 尝试去竞争第一个,也就是丢弃任务队列前面的任务,重新提交执行,失败了也不抛异常。
CallerRunsPolicy 使用调用者所在线程执行-提交任务的线程,就是哪里来的回哪里去。
6、线程的各种状态
线程有哪些基本状态?这些状态是如何定义的?
- 新建(new): 新创建了一个线程对象。
- 可运行(runnable): 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
- 运行(running): 可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
- 阻塞(block): 阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:
-
- (一). 等待阻塞: 运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waiting queue)中。
- (二). 同步阻塞: 运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- (三). 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5.死亡(dead): 线程run()、main()方法执行结束,或者因异常退出了run() 方法,则该线程结束生命周期。死亡的线程不可再次复生。
7、gc垃圾回收方面得知识点
- 可以作为GCRoot的对象
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象。
- 垃圾回收算法
- 标记-清理算法:标记清除算法分为标记和清理两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 复制算法:将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了就将还存活的对象复制到另一块中,然后再把已经使用过的内存一次清理掉。
- 标记整理算法:先根据GCRoot可达性分析将对象进行标记,然后将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法:分代收集算法就是根据各个年代的特点采用最适当的算法来收集。
分为新生代回收(Minor GC | Young GC)、老年代回收(Major GC)、清理整个堆(Full GC)
新生代按8:1:1分为Eden:survivor(from):survivor(to),当对象比较大或者新生代空间不足时可能直接分配到老年代,正常按gc多少次后转入老年代,特例虚拟机有一个动态年龄的概念,如果Survior空间中所有相同年龄大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进老年代。垃圾回收的触发条件:Eden空间不足,就会进行Minor GC回收新生代、老年代空间不足、元空间不足、要晋升老年代的对象所占用的空间大于老年代的剩余空间、显式调用System.gc()。
- 死亡逃逸
1、全局级别逃逸:需要在堆内分配空间。
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
- 对象是一个静态变量
- 对象是一个已经发生逃逸的对象
- 对象作为当前方法的返回值
2、参数级别逃逸:需要在堆内分配空间,可能会优化去掉同步锁。如果一个对象被作为参数传递给一个方法,但是在这个方法之外无法访问或者对其他线程不可见,这个对象标记为参数级别逃逸。
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
3、无逃逸状态:一个对象不会产生逃逸,可能会在栈上分配,也可能会在堆上分配空间,甚至根本不会去创建一个对象,而直接使用该对象的标量值代替,在栈内分配空间自动去除了同步锁。
即方法中的对象没有发生逃逸。
- finalize
finalize()方法释放本地方法申请的内存;2.作为终结条件
finalize方法是Object提供的的实例方法,使用规则如下:
- 当对象不再被任何对象引用时,GC会调用该对象的finalize()方法
- finalize()是Object的方法,子类可以覆盖这个方法来做一些系统资源的释放或者数据的清理
- 可以在finalize()让这个对象再次被引用,避免被GC回收;但是最常用的目的还是做cleanup
- Java不保证这个finalize()一定被执行;但是保证调用finalize的线程没有持有任何user-visible同步锁。
- 在finalize里面抛出的异常会被忽略,同时方法终止。
- 当finalize被调用之后,JVM会再一次检测这个对象是否能被存活的线程访问得到,如果不是,则清除该对象。也就是finalize只能被调用一次;也就是说,覆盖了finalize方法的对象需要经过两个GC周期才能被清除。
- Dalvik虚拟机与Hotspot虚拟机的区别
Dalvik
- Image Space: 是用于存放系统预加载类,这些系统预加载类是通过 AOT 预先编译好并存放在 oat 文件中,每次开机启动时会把这些类映射到 Image Space。类比 JVM 堆区可以理解为是方法区。这块内存不进行垃圾回收。
- Zygote Space :是存放管理 Zygote 进程在启动过程中预加载和创建的各种对象和资源,会进行垃圾回收。
- Allocation Space: 是我们主要关注的一块内存空间,它是 Java 或 C/C++ 代码的对象内存分配。类比 JVM 堆区就是年青代和老年代两个内存区域:
- LargeObject Space: 是用于分配大对象的内存,类比 JVM 堆区就是老年代。
当满足以下一个条件时对象内存会在 LargeObject Space 分配,否则在 Allocation Space 上分配:
对象需要分配的内存大小 >= 12KB(内存一页 page 是 4 KB,12 KB 也就是三页 page)
对象是基本数据类型数组,即 byte[]、int[]、boolean[] 等;图片也是 byte[] 所以也是会在这里分配
Dalvik虚拟机最常用的算法便是MarkSweep算法,该算法一般分Mark阶段(标记出活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。Dalvik虚拟机实现不进行可选的Compact阶段(减少堆中的碎片)。 Dalvik GC只对后台的App进行内存整理(压缩)。
Dalvik 有三种 GC 策略:
Sticky GC:只回收上一次 GC 到本地 GC 之间申请的内存。其实就是 CMS 回收浮游垃圾
Partial GC:局部垃圾回收,会回收 Allocation Space 和 LargeObject Space 的内存垃
Full GC:全局垃圾回收,回收除了 Image Space 之外的内存垃圾
Dalvik 对象分配流程如下:
- 首先会进行一次 Sticky GC,GC 完成后尝试分配;如果分配失败,则选择 Partial GC,GC 完成后再尝试分配;还是分配失败,则选择 Full GC,GC 完成后再尝试分配;如果还是不能分配,进入下一阶段
- 允许堆进行增长的情况下进行对象分配(可能一开始是 128 MB,最大是 256 MB,堆内存增长后再尝试分配)
- 经过两个阶段还不能分配,进行一次允许回收软引用的 GC 进行对象分配;如果还不能分配,OOM
将上面的三个阶段可以简单理解为:
- 内存是否足够,不够,GC 一次回收浮游垃圾
- 还不够,GC 一次局部回收(Allocation Space 和 LargeObject Space 的内存回收)
- 还不够,Full GC 一次(除 Image Space 之外的内存回收)
- 还不够,堆扩容
- 还不够,软引用回收
- 还不够,OOM
可以发现 Android 相比 JVM,在对象分配流程上会最大限度的保证不出现 OOM 的情况下,充分利用每一份空间,但是 当堆内存只要达到 85% 左右就会出现频繁 GC。
ART
使用的垃圾回收算法有多种:
主要包含并发标记-清除算法,每个版本各种优化
Hotspot
JVM 会有 Minor GC、Major GC 和 Full GC 三种策略:
Eden 区满了,触发 Minor GC
old 区空间不足了,先触发 Minor GC,如果内存还不够则触发 Major GC
System.gc()、方法区空间不足、old 区空间不足、从年青代推到老年代的对象 > old 区可用内存,触发 Full GC
JVM 对象分配流程如下:
- new 出来的对象进 Eden(大对象直接进老年代)
- Eden 区放满了,再放就会开启一个 GC 线程(Minor GC)来回收垃圾
- 把 Eden 区中非垃圾对象复制到 Survivor 的 From 区,剩下的直接全部清除
- 继续 new 对象时,如果 Eden 区又满了,GC 来了就把 Eden 和 From 区存活的对象复制到 To 区,对象的分代年龄 +1
- 每次 Eden 满的时候,就在 Eden+From 和 Eden+To 中来回复制
- 对象年龄到老年代阈值,就进入老年
- 老年代放满了,就会发生 Full GC,对堆进行全面 GC