面试题整理

数组实现队列

public class ArrayQueue {

    private String [] items;  //定义数组
    private int n = 0;           //数组大小
    private int head = 0;     //表示队头下标
    private int tail = 0;        //表示队尾下标

    //申请一个大小为capacity的数组
    public ArrayQueue(int capacity) {
        this.n = capacity;
        this.items = new String[capacity];  //初始化数组
    }

    public boolean enQueue(String item) {
        if (tail == n) {  //队列已经满了
            return false;
        }
        items[tail] = item;
        tail++;
        return true;
    }

    public String deQueue() {
        if (head == tail) {   //队列为空
            return null;
        }
        String item = items[head];
        head++;
        return item;
    }

}
public class ArrayStack {
    String[] arrays;
    int i = 0;

    public ArrayStack(int n) {
        arrays = new String[n];
    }

    public void push(String item) {
        if (i > arrays.length - 1) {
            return;
        }
        arrays[i] = item;
        i++;
    }

    public String pop() {
        if (i > 0) {
            i--;
        }
        String temp = arrays[i];
        arrays[i] = null;
        return temp;
    }

}

java软引用与弱引用区别

参考了一些资料

  • 强引用
    • 我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。
    • 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
    • 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应强引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
  • 软引用(SoftReference)
    • 只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
    • 当JVM认为内存充足的时候,不会去回收软引用
    • 软引用什么时候会被回收
  • 弱引用(WeakReference)
    • 当发生GC时,如果扫描到一个对象只有弱引用,不管当前内存是否足够,都会对它进行回收。
        String str = new String("abc");
        //创建一个弱引用,让这个弱引用引用到str字符串
        WeakReference weakReference = new WeakReference(str);
        //切断str引用和 str 字符串之间的引用,此时str 只有一个弱引用weakReference指向它
        str = null;
        // 没有进行垃圾回收,我们还可以通过弱引用来访问他
        System.out.println(weakReference.get()); // abc
        //强制进行垃圾回收
        System.gc();
        //再次取出弱引用所引用的对象
        System.out.println(weakReference.get()); // null
    
  • 虚引用(PhantomReference)
    • 垃圾回收时回收,无法通过引用取到对象值
    • 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
    • 虚引用主要用于检测对象是否已经从内存中删除。程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用对象是否即将被回收。
    • 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

final变量用反射修改

  • 当final修饰的成员变量在定义的时候就初始化了值,那么java反射机制就已经不能动态修改它的值了。

    • 原因:编译期间final类型的数据自动被优化了,即:所有用到该变量的地方都被替换成了常量。
    public final String name = "abc";
    public String getName() {
        return "abc";
    }
    
  • 当final修饰的成员变量在定义的时候并没有初始化值的话,那么就还能通过java反射机制来动态修改它的值。

  1. HashMap的内部结构,给定一个key,如何找到对应的value,使用equal
  2. volatile
  3. Java线程池有什么作用
  4. Java动态代理
  5. handler机制
  6. android跨进程通信的方式
  7. 自定义控件方式
  8. Canvas绘制过什么 手写功能

断点续传的实现

  • 从字面上理解,所谓断点续传就是从停止的地方重新下载。 断点:线程停止的位置。 续传:从停止的位置重新下载。
  • 用代码解析就是:断点: 当前线程已经下载完成的数据长度。续传: 向服务器请求上次线程停止位置之后的数据。原理知道了,功能实现起来也简单。每当线程停止时就把已下载的数据长度写入记录文件,当重新下载时,从记录文件读取已经下载了的长度。而这个长度就是所需要的断点。
  • 总结来说就是下载过程中要使用数据库实时存储到底存储到文件的哪个位置了,这样点击开始继续传递时,才能通过HTTP的GET请求中的 urlConnection.setRequestProperty("Range","bytes=" + start + "-" + info.getLength()); 方法可以告诉服务器,数据从哪里开始,到哪里结束。同时在本地的文件写入时,RandomAccessFile的seek()方法也支持在文件中的任意位置进行写入操作。同时通过广播将子线程的进度告诉Activity的ProcessBar。

app 启动速度的优化

影响启动的因素
  • 高耗时任务
  • 复杂的View层级
  • 类过于复杂
  • 主题及Activity配置
启动耗时检测
  • 查看Logcat
  • adb shell
  • 代码打点(函数插桩)
  • 启动速度分析工具 — TraceView
  • 启动速度分析工具 — Systrace
解决方案
  • 异步初始化 : 充分利用CPU多核,自动梳理任务顺序。
  • 延迟初始化 :第三方库懒加载,按需初始化; 利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化
  • Multidex预加载优化 启动时单独开一个进程去异步进行Multidex的第一次加载,即Dex提取和Dexopt操作。
  • 复杂的View层级 减少层级嵌套
  • 主题切换:使用Activity的windowBackground主题属性预先设置一个启动图片
  • 保活
  • WebView启动优化

线程优化

线程调度模型
  • 分时调度模型 轮流获取、均分CPU
  • 抢占式调度模型 优先级高的获取
如何干预线程调度?
  • 设置线程优先级。
Android异步方式
  • Thread 直接创建,缺点很多,比如说不容易被复用,导致频繁创建和销毁线程的开销大,不建议使用
  • HandlerThread 本质上也是一个 Thread,自带了消息循环,串行执行
  • IntentService 是 Service 组件的子类,它的内部有一个 HandlerThread,所以它具备了 HandlerThread 的特性。优点:使用了 Service,会提高应用的优先级;异步,不占用主线程
  • AsyncTask 内部实现使用了线程池
  • 线程池 易复用,减少频繁创建、销毁的时间;功能强大,如定时、任务队列、并发数控制等
  • RxJava 功能强大,提供了不同的线程池:IO Computation
Android线程优化实战
  • 严禁使用new Thread方式。
  • 提供基础线程池供各个业务线使用,避免各个业务线各自维护一套线程池,导致线程数过多。
  • 根据任务类型选择合适的异步方式:优先级低,长时间执行,HandlerThread;定时执行耗时任务,线程池。
  • 创建线程必须命名,以方便定位线程归属,在运行期Thread.currentThread().setName修改名字。
  • 关键异步任务监控,注意异步不等于不耗时,建议使用AOP的方式来做监控。
  • 重视优先级设置(根据任务具体情况),Process.setThreadPriority();可以设置多次。
线程收敛优雅实践初步
  • 基础库内部暴露API:setExecutor。
  • 初始化的时候注入统一的线程库。

布局优化

  • 减少层级
    • 合理使用RelativeLayout和LinearLayout,RelativeLayout会对子View做两次测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高。
    • 合理使用Merge
  • 提供显示速度
    • ViewStub
  • 布局复用
    • 通过标签来实现
  • 其他
    • 使用标签加载一些不常用的布局。
    • 尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。
    • 使用TextView替换RL、LL。
    • 使用低端机进行优化,以发现性能瓶颈。
    • 使用TextView的行间距替换多行文本:lineSpacingExtra/lineSpacingMultiplier。
    • 使用Spannable/Html.fromHtml替换多种不同规格文字。
    • 尽可能使用LinearLayout自带的分割线。
    • 使用Space添加间距。
    • 多利用lint + alibaba规约修复问题点。
    • 嵌套层级过多可以考虑使用约束布局

减少过度绘制

导致过度绘制的主要原因是:
  • XML布局:控件有重叠且都有设置背景。
  • View自绘:View.OnDraw里面同一个区域被绘制多次。
如何避免过度绘制
  • 布局上的优化

    • 移除XML中非必需的背景
    • 有选择性地移除窗口背景:getWindow().setBackgroundDrawable(null)
    • 按需显示占位背景图片
  • 自定义View优化

    • 通过canvas.clipRect()来帮助系统识别那些可见的区域
    • 绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回

webview的优化

  • WebView首次创建比较耗时,需要预先创建WebView提前将其内核初始化。
  • 使用WebView缓存池,用到WebView的时候都从缓存池中拿,注意内存泄漏问题。
  • 本地离线包,即预置静态页面资源
  • 本地接口请求,缓存
  • DNS解析优化
  • 另外开启webView进程
  • 首屏静态html
  • 骨架屏

fresco加载图片原理 优势是什么

缓存怎么处理的

a、根据Uri在已解码的(Bitmap缓存)内存缓存中查找,找到了则返回Bitmap对象;如果没找到,则开启后台线程开始后续的工作。
b、根据Uri在未解码的内存缓存中查找,若找到了则解码,然后缓存到已解码的内存缓存中,并且返回Bitmap对象。
d、如果在未解码的内存缓存中没找到,则根据Uri在磁盘缓存中查找,若找到了则读取数据(byte数组),并缓存到未解码的内存缓存中,解码、然后缓存到已解码的内存缓存中,并且返回Bitmap对象。
e、如果在磁盘缓存中没找到,则从网络或者本地加载数据。加载完成后,依次缓存到磁盘缓存、未解码的内存缓存中。解码、然后缓存到已解码的内存缓存中,并且返回Bitmap对象。

bitmap 内存分配
  • 在4.x及以下的系统上,Fresco的bitmap decode会把bitmap的pixel data(像素数据)放到一个“特殊的内存区域’ ”,这个特殊的内存区域其实就是ashmem
  • 为什么在4.x及以下系统需要这样做?原因是如果Bitmap数量很多时会占用大量的内存(这里内存特指Java Heap),必然就会更加频繁的触发虚拟机进行GC,GC 会导致stop the world
  • 怎么实现的?
    • BitmapFactory.Options 的参数 inPurgeable能使bitmap的内存分配到ashmem上。inPurgeable的作用是,在KITKAT及以下,使用该参数的话,当系统需要回收内存的时候,bitmap的pixels可以被清除。好在的是,当pixels需要被重新访问的时候(例如bitmap draw或者调用getPixels()的时候),它们又可以重新被decode出来
    • isPurgeable为true的图片,内存分配的时机和流程,只有当bitmap进行draw或者getPixels(这个大抵一致)的时候,bitmap 的pixels才进行实际的内存分配
    • bitmap draw的整个流程看,操作都处于UI线程,由于存在耗时的操作,会导致drop frames
  • ART模式下,BitmapOptions的inBitmap和inTempStorage去优化内存使用。inBitmap是由上层的BitmapPool 去分配内存,inTempStorage是由 SynchronizedPool分配内存,都是用缓存池的方式分配和回收内存,做到对这些区域的内存可管理,减少各个不同地方自行分配内存 。
  • 为啥使用 Ashmem
    • Java Heap:大小受系统限制,内存自动回收。
    • Native Heap:大小不受系统限制,仅受物理内存限制,内存需要手动释放。
    • Ashmem:大小不受系统限制,仅受物理内存限制,系统在需要的时候可以自动回收,也可以手动阻止回收。

多进程通讯的方式

  • bundle 四大组件可以通过这种方式传递数据
  • 文件共享 简单的文件读写,序列化文件,sp
  • AIDL 客户端,服务端,接口
  • Messenger 底层是binder,客户端和服务端两端,主要是对消息的处理,串行的方式处理消息
  • ContentProvider 底层是binder,和数据库操作息息相关
  • Socket serverSocket

Handler 如何规避内存泄漏

  • 原因:非静态内部类会隐性地持有外部类的引用
  • 监控内存泄漏
    • LeakCanary
    • 原理,利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收
  • 解决方法
    • 在onDestory中移除 handler.removeCallbacksAndMessages(null)
    • 声明一个静态的Handler内部类,并持有外部类的弱引用

data binding

  • 原理
    • 将xml文件拆分成两部分,一部分是数据文件,一部分是布局文件,在布局文件中,为每个View设置一个tag,数据文件中,标示了相对应的tag的View所需要使用的数据
    • 根据上面的两个xml在编译时生成 ActivityDataBindBinding和BR类
    • 在 ActivityDataBindBinding 当中,会更加tag对每一个View进行赋值
    • 根据上面的xml对数据进行绑定操作 (executeBindings方法)
  • 数据的实时刷新
    • 在进行set的方法时候,最终会调用到 executeBindings方法

Android中动画的原理

  • 帧动画

    • animation-list --> AnimationDrawable
    • 容易oom
  • view动画

    • a.getTransformation方法会返回一个布尔值,表示是否需要继续动画,一旦为true,则父控件会重新计算需要动画需要刷新的区域并且更新该区域。在动画结束之前会不断重绘,从而形成连续的动画效果。
    • getTransformation 在这个方法中,根据流逝的时间计算当前动画时间百分比,然后通过插值器(Interpolator)重新计算这个百分比,并且以此来计算当前动画属性值
  • 属性动画

    • 属性动画是通过VSYNC信号来持续改变属性值进行动画的
    • 相比于补间动画,属性动画重绘明显会少很多
    • 流程 ObjectAnimator.ofFloat(button,"translationX",0,200);
      • 首先会把我们传入的View保存起来
      • 然后会初始化一个Holder对象,这个Holder对象是用来干啥的呢?我们传入了一个translationX值,Holder对象中提供一个方法,把translationX拼接成一个set方法,setTranslationX,然后通过反射找到View中的此方法。当数值计算出来之后,执行获取的这个方法。
      • KeyframeSet是一个关键帧的集合,最后我们传入0-200就是关键帧
      • 插值器,用来计算某一时间中动画长度播放的百分比
      • 估值器,根据插值器计算的百分比,来计算某个时间,动画所要更新的值。然后交给Holder执行动画
      • 属性动画会监听系统发出的VSYNC信号,每收到一次信号就执行一次
  • 插值器和估值器

    • 插值器:设置 属性值 从初始值过渡到结束值 的变化规律
    • 估值器:设置 属性值 从初始值过渡到结束值 的变化具体数值
    • 插值器只是根据时间百分比计算出一个属性值百分比,而把属性值百分比转换为真正属性值则交给估值器来做。例如:插值器返回的是一个百分比,估值器就可以将这个百分比转换成色值(0-255)或者一个坐标

字符串hash函数

  • 源码
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
  • 为什么是31
    • 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一,为啥选择质数,质数可以降低哈希算法的冲突率
    • 31可以被 JVM 优化,31 * i = (i << 5) - i
    • Effective Java : 选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。

ThreadLocal

  • ThreadLocal是并发场景下用来解决变量共享问题的类,它能使原本线程间共享的对象进行线程隔离,即一个对象只对一个线程可见
  • 当设置value时,变量的值保存在一个与线程相关的map中(ThreadLocalMap),这样做是为了避免多线程竞争,因为放在Thread对象中就相当于线程私有了,处理的时候不需要加锁
  • ThreadLocalMap里面的 Entry extends WeakReference>
  • 存在的潜在问题
    • 如果任务对象结束而线程实例仍然存在(常见于线程池的使用中,需要复用线程实例),那么仍然会发生内存泄露。
    • 线程复用会产生脏数据
    • ThreadLocalMap在它的getEntry、set、remove、rehash等方法中都会主动清除ThreadLocalMap中key为null的Entry,但如果我们声明ThreadLocal变量后,再也没有调用过上述方法,依然会发生内存泄露
  • 参考

编码格式

  • 文字到0、1的映射称为编码,反过来从0、1到文字叫解码。
  • 最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),0 - 255被用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码
  • Unicode编码定义了这个世界上几乎所有字符的数字表示,已经扩展到了 21 位
    • Unicode给这串数字ID起了个名字叫[码点]。[码点]经过映射后得到的二进制串的转换格式单位称之为[码元]
    • [码点]就是一串二进制数,【码元】就是切分这个二进制数的方法。
    • 编码空间被分成 17 个平面(plane),每个平面有 65,536 个字符(2个字节,16位)。0 号平面叫做「基本多文种平面」(BMP),涵盖了几乎所有你能遇到的字符,除了 emoji(emoji位于1号平面 - -)。其它平面叫做补充平面,大多是空的
  • UTF-32 UTF-32也就是说它的码元是32位,每32位去读一下码点
  • UTF-16 它的码元是16位的,也就是说每16位去读一下码点,获取码点的前16位数字,直到读取完成。
    • BMP平面(plane0)中的每一个码点都直接与一个UTF-16 的码元一一映射。
    • 其它平面里很少使用的码点都是用两个 16 位的码元来编码的
  • UTF-8 使用一到四个字节来编码一个码点
    • 从 0 到 127 的这些码点直接映射成 1 个字节,西文,都位于此段,该编码方式非常节约空间
    • 接下来的 1,920 个码点映射成 2 个字节
    • 在 BMP 里所有剩下的码点需要 3 个字节 对于中文,就位于此段,3个字节
    • 参考

volatile

用于保持内存可见性(随时见到的都是最新值)和防止指令重排序 参考

java内存模型
  • 主内存,工作内存
    • 主内存:虚拟机中的一块内存,对应java堆中的对象实例数据
    • 工作内存:每条线程自己的内存空间,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作,都必须在工作内存中进行,不能直接读取主内存中的变量,对应虚拟机栈中的部分区域
  • 内存间的交互
    • lock:作用于主内存,把变量标识为线程独占状态。
    • unlock:作用于主内存,解除独占状态。
    • read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
    • load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
    • use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
    • assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
    • store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
    • write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。
  • 可见性:当一条线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的
    • 普通变量的值是线程间传递需要通过主内存来完成的,一个值在一个线程被修改,需要向主内存进行回写,另一条线程在去从主内存中进行读取操作,新值才会对另外一条线程可见
volatile保持可见性
  • 关键字修饰的变量看到的随时是自己的最新值
  • volatile的特殊规则就是:
    • read、load、use动作必须连续出现。
    • assign、store、write动作必须连续出现。
  • 使用volatile变量能够保证:
    • 每次读取前必须先从主内存刷新最新的值。
    • 每次写入后必须立即同步回主内存当中。
  • 注意:volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作,即:
    • 基本类型的自增(如count++)等操作不是原子的。
    • 对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的。
防止指令重排
  • 指令重排序是cpu采用了允许将多条指令不按照程序规定的顺序分开发送给各相应电路单元来处理

  • volatile关键字通过“内存屏障”来防止指令被重排序 (只有在Happens-Before内存模型中才会出现指令重排)

  • volatile 读操作的性能消耗与普通变量几乎没有什么差别,写操作可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不返生乱序执行

  • 来看一个单利模式 DCL(Double Check Lock,双重检查锁)

  class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

它可以”抽象“为下面几条JVM指令:

memory = allocate();    //1:分配对象的内存空间
initInstance(memory);   //2:初始化对象
instance = memory;      //3:设置instance指向刚分配的内存地址

JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();    //1:分配对象的内存空间
instance = memory;      //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);   //2:初始化对象

引用instance指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。

解决这个该问题,只需要将instance声明为volatile变量:

private static volatile Singleton instance;

Thread类的sleep() yield() 和 wait()的区别?

  • sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复
  • wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会

多线程如何保证线程安全

  • 同步 Synchronized 参考
    • 普通同步方法,锁是当前实例对象.JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法
    • 静态同步方法,锁时当前类的Class对象。
    • 代码块同步:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。使用monitorenter和monitorexit指令实现。
  • 使用原子类(atomic concurrent classes) 如AtomicInteger等,AtomicInteger通过声明一个volatile value(内存锁定,同一时刻只有一个线程可以修改内存值)类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。
  • 实现并发锁
  • 使用volatile关键字
  • 使用不变类和线程安全类

GC

参考

找到被回收的对象
  • 引用计数法
    • 堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
    • 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。
    • 缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销
    • 早期的JVM使用引用计数
  • 可达性分析法
    • 通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下继续寻找它们的引用节点,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
    • GC Roots对象:虚拟机栈中引用的对象;方法区中类静属性引用的对象;方法区中常量引用的对象;本地方法中JNI引用的对象
    • 真正宣告一个对象死亡,至少要经历两次标记过程:
      • 如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
      • 如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
回收算法
  • 标记清除算法

    • 首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象
    • 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
    • 缺点:
      • 标记和清除过程的效率都不高。这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。
      • 标记清除后会产生大量不连续的内存碎片
  • 复制算法

    • 它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
    • 优点:
      • (1)标记阶段和复制阶段可以同时进行。
      • (2)每次只对一块内存进行回收,运行高效。
      • (3)只需移动栈顶指针,按顺序分配内存即可,实现简单。
      • (4)内存回收时不用考虑内存碎片的出现
    • 缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
    • 复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法
  • 标记—整理算法

    • 标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存
    • 优点:
      • (1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。
      • (2)使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。
    • 缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
分代收集
  • Java的堆内存划分:新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成

  • 年轻代:

    • 几乎所有新生成的对象首先都是放在年轻代的。
    • 新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。
    • 大部分对象在Eden区中生成。 当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。
    • 回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。
    • 当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。
  • 年老代

    • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2)
    • 当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高
    • 一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组
  • 持久代

    • 用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
    • 对永久代的回收主要回收两部分内容:废弃常量和无用的类。
    • 永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。
  • 堆内存分配策略

    • 对象优先在Eden分配。
    • 大对象直接进入老年代。
    • 长期存活的对象将进入老年代。
  • 分代的回收算法

    • 新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
    • 老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。由于老年代中的对象生命周期比较长,因此Major GC并不频繁。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
    • 新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代
其他
  • 垃圾回收执行时间

    • GC分为Scavenge GC和Full GC。
      • Scavenge GC :发生在Eden区的垃圾回收。
      • Full GC :对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢
    • 有如下原因可能导致Full GC:
      • 1.年老代(Tenured)被写满;
      • 2.持久代(Perm)被写满;
      • 3.System.gc()被显示调用;
      • 4.上一次GC之后Heap的各域分配策略动态变化.
  • 相关函数

    • System.gc(),请求Java的垃圾回收。仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
    • finalize()
      • 在finalize()方法返回之后,对象消失,垃圾收集开始执行。
      • finalize()的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。其他做法开辟的内存空间,例如:1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法,(2)打开的文件资源等
      • 一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。
  • 触发主GC的条件

    • 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用
    • Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
    • 在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收
  • 减少GC开销的措施

    • 不要显式调用System.gc()。 这样会增加了间歇性停顿的次数。
    • 尽量减少临时对象的使用。 少用临时变量就相当于减少了垃圾的产生,从而延长了出现第二个垃圾回收的时间,减少了主GC的机会。
    • 对象不用时最好显式置为Null。 有利于GC收集器判定垃圾,从而提高了GC的效率。
    • 尽量使用StringBuffer,而不用String来累加字符串。 String是固定长的字符串对象,累加String对象时,本质上是重新创建新的String对象
    • 能用基本类型如Int,Long,就不用Integer,Long对象。 基本类型变量占用的内存资源比相应对象占用的少得多
    • 尽量少用静态对象变量。 静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
    • 分散对象创建或删除的时间。 集中在短时间内大量创建新对象,所需内存变多,会增加主GC的频率。集中删除对象会突然出现了大量的垃圾对象,会增加主GC的频率。

你可能感兴趣的:(面试题整理)