01、java 基础整理

125 阅读40分钟

面试复习——Android工程师之Java面试题

大纲

Android面试题集 & 答案

LearningNotes

23

一、原文:www.jianshu.com/p/c93965357…

1、HashMap 和 HashTable 以及 CurrentHashMap 的区别。 一般来说,这三个东西基本在面试中 70% 会被问到,而问的方向也不太一样。比如初级的问法是讲讲它们之前的区别,这个我想没什么难度,大多数人还是知道主要核心区别是并发上的处理。此外,内部数据结构的实现、扩容、存取操作这些问题应该是很老生常谈了,这并没有什么好说的,大多数人也都知道。稍微问的深一点的可能会在下面这些点上出问题。哈希碰撞,哈希计算,哈希映射,为什么是头插法,扩容为什么是 2 的幂次等这样的问题。

2、synchronized 和 volatile 、ReentrantLock 、CAS 的区别。 这个问题被问频率不在 HashMap 之下,因为并发编程,真的很重要。能问到这几个点的方式真的是太多了,我们能发挥的空间也同样很大。CAS 的 ABA 问题?上面几个东西的特性?使用场景?大概我不用再例举了吧?对了,我多次被问到的一个问题是:synchronized 修饰实例方法和修饰静态方法有啥不一样。

3、JVM 类加载机制、垃圾回收算法对比、Java 虚拟机结构等。 这三个问题大概出现概率 40%,基本只需要看我每日一问系列的推文就差不多了吧,希望更清楚明白的可以直接看《深入理解 Java 虚拟机》。当你讲到分代回收算法的时候,不免会被追问到新生对象是怎么从年轻代到老年代的,以及可以作为 root 结点的对象有哪些两个问题。

4、Java 的四大引用 四大引用面试出现概率比我想象中要高,我原本以为就强引用、软引用、弱引用、虚引用这四个玩意儿没啥可讲的。实际上也确实没啥好讲的,稍微问的深一些的面试官会和内存泄漏检测原理以及垃圾回收糅杂在一起。

5、Java 的泛型,<? super T> 和 <? extends T> 的区别。 Java 泛型还是会在面试中出现的,不过几率不是很高,大概是因为我简历中有提到泛型擦除相关的东西。所以会被问到泛型、泛型擦除、通配符相关的东西。不过这个东西,不应该是为了应付面试,实际开发中真的很重要。

6、Java 线程有哪些状态,有哪些锁,各种锁的区别。 这个问题讲真,我也只懂一点皮毛,并且当时回答不是很全面,出现概率的话,不是很高吧。

7、final 、finally、finalize 区别。 老生常谈的问题,没啥好说的,实际上这次社招面试也只遇到了两次。比较喜欢追根溯源的面试官可能会对这个 finalize 有点执念,一定希望搞清楚,这玩意儿我们是不是可以真的搞点黑科技骚操作。

8、接口和抽象类的区别。 没想到还被问了一次这个,这玩意儿给我的感觉就是随时都在用,但真要较真,还真不能一口气把所有区别都信手拈来。

9、sleep 、wait、yield 的区别,wait 的线程如何唤醒它? 大多数 Android 应用开发并接触不到很多并发相关的东西,不过这玩意儿还是在面试中挺容易出现的。

二、原文:www.jianshu.com/p/c70989bd5…

(一) java基础面试知识点
  • java中==和equals和hashCode的区别
  • int、char、long各占多少字节数
  • int与integer的区别
  • 谈谈对java多态的理解
  • String、StringBuffer、StringBuilder区别
  • 什么是内部类?内部类的作用
  • 抽象类和接口区别
  • 抽象类的意义
  • 抽象类与接口的应用场景
  • 抽象类是否可以没有方法和属性?
  • 接口的意义
  • 泛型中extends和super的区别
  • 父类的静态方法能否被子类重写
  • 进程和线程的区别
  • final,finally,finalize的区别
  • 序列化的方式
  • Serializable 和Parcelable 的区别
  • 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?
  • 静态内部类的设计意图
  • 成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用
  • 谈谈对kotlin的理解
  • 闭包和局部内部类的区别
  • string 转换成 integer的方式及原理
(二) java深入源码级的面试题(有难度)
  • 哪些情况下的对象会被垃圾回收机制处理掉?
  • 讲一下常见编码方式?
  • utf-8编码中的中文占几个字节;int型几个字节?
  • 静态代理和动态代理的区别,什么场景使用?
  • Java的异常体系
  • 谈谈你对解析与分派的认识。
  • 修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?
  • Java中实现多态的机制是什么?
  • 如何将一个Java对象序列化到文件里?
  • 说说你对Java反射的理解
  • 说说你对Java注解的理解
  • 说说你对依赖注入的理解
  • 说一下泛型原理,并举例说明
  • Java中String的了解
  • String为什么要设计成不可变的?
  • Object类的equal和hashCode方法重写,为什么?
(三) 数据结构
  • 常用数据结构简介
  • 并发集合了解哪些?
  • 列举java的集合以及集合之间的继承关系
  • 集合类以及集合框架
  • 容器类介绍以及之间的区别(容器类估计很多人没听这个词,Java容器主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections),具体的可以看看这篇博文 Java容器类
  • List,Set,Map的区别
  • List和Map的实现方式以及存储方式
  • HashMap的实现原理
  • HashMap数据结构?
  • HashMap源码理解
  • HashMap如何put数据(从HashMap源码角度讲解)?
  • HashMap怎么手写实现?
  • ConcurrentHashMap的实现原理
  • ArrayMap和HashMap的对比
  • HashTable实现原理
  • TreeMap具体实现
  • HashMap和HashTable的区别
  • HashMap与HashSet的区别
  • HashSet与HashMap怎么判断集合元素重复?
  • 集合Set实现Hash怎么防止碰撞
  • ArrayList和LinkedList的区别,以及应用场景
  • 数组和链表的区别
  • 二叉树的深度优先遍历和广度优先遍历的具体实现
  • 堆的结构
  • 堆和树的区别
  • 堆和栈在内存中的区别是什么(解答提示:可以从数据结构方面以及实际实现方面两个方面去回答)?
  • 什么是深拷贝和浅拷贝
  • 手写链表逆序代码
  • 讲一下对树,B+树的理解
  • 讲一下对图的理解
  • 判断单链表成环与否?
  • 链表翻转(即:翻转一个单项链表)
  • 合并多个单有序链表(假设都是递增的)
(四) 线程、多线程和线程池
  • 开启线程的三种方式?
  • 线程和进程的区别?
  • 为什么要有线程,而不是仅仅用进程?
  • run()和start()方法区别
  • 如何控制某个方法允许并发访问线程的个数?
  • 在Java中wait和seelp方法的不同;
  • 谈谈wait/notify关键字的理解
  • 什么导致线程阻塞?
  • 线程如何关闭?
  • 讲一下java中的同步的方法
  • 数据一致性如何保证?
  • 如何保证线程安全?
  • 如何实现线程同步?
  • 两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
  • 线程间操作List
  • Java中对象的生命周期
  • Synchronized用法
  • synchronize的原理
  • 谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
  • static synchronized 方法的多线程访问和作用
  • 同一个类里面两个synchronized方法,两个线程同时访问的问题
  • volatile的原理
  • 谈谈volatile关键字的用法
  • 谈谈volatile关键字的作用
  • 谈谈NIO的理解
  • synchronized 和volatile 关键字的区别
  • synchronized与Lock的区别
  • ReentrantLock 、synchronized和volatile比较
  • ReentrantLock的内部实现
  • lock原理
  • 死锁的四个必要条件?
  • 怎么避免死锁?
  • 对象锁和类锁是否会互相影响?
  • 什么是线程池,如何使用?
  • Java的并发、多线程、线程模型
  • 谈谈对多线程的理解
  • 多线程有什么要注意的问题?
  • 谈谈你对并发编程的理解并举例说明
  • 谈谈你对多线程同步机制的理解?
  • 如何保证多线程读写文件的安全?
  • 多线程断点续传原理
  • 断点续传的实现
(五)并发编程有关知识点(这个是一般Android开发用的少的,所以建议多去看看):

平时Android开发中对并发编程可以做得比较少,Thread这个类经常会用到,但是我们想提升自己的话,一定不能停留在表面,,我们也应该去了解一下java的关于线程相关的源码级别的东西。

学习的参考资料如下:

Java 内存模型

线程状态:

锁:

并发编程:

3、原文:www.jianshu.com/p/73935e04a…

泛型中 extends 和 super 的区别?

上界通配符:Plate<? extends Fruit> = new Plate(new Apple()); 下界通配符:Plate<? super Fruit> = new Plate(new Food()); 频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。

HashMap的实现原理

数组和链表 www.cnblogs.com/chengxiao/p… HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。 哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。   我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。 比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

Parcelable比Serializable:

Parcelable比Serializable性能高,所以应用内传递数据推荐使用Parcelable,但是Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,但此时还是建议使用Serializable Parcel类是一种最快的序列化/反序列化机制,专为Android中的进程间通信而设计。该类提供了一些方法来将成员容纳到容器中,以及从容器展开成员。 现在我们知道了如何传递自定义的对象,那么在两个Activity之前传递对象还要注意什么呢? 一定要要注意对象的大小,Intent中的Bundle是在使用Binder机制进行数据传递的,能使用的Binder的缓冲区是有大小限制的(有些手机是2M),而一个进程默认有16个binder线程,所以一个线程能占用的缓冲区就更小了(以前做过测试,大约一个线程可以占用128KB)。所以当你看到“The Binder transaction failed because it was too large.”这类TransactionTooLargeException异常时,你应该知道怎么解决了。因此,使用Intent在Activity之间传递List和Bitmap对象是有风险的。

多态

多态的作用:消除类型之间的耦合关系。 多态存在的三个必要条件 一、要有继承; 二、要有重写; 三、父类引用指向子类对象。

classLoader

引导类加载器(bootstrap class loader)   --用来加载java 的核心库(String 、Integer、List。。。)在jre/lib/rt.jar路径下的内容,是用C代码来实现的,并不继承自java.lang.ClassLoader。   --加载扩展类和应用程序类加载器。并指定他们的父类加载器。 扩展类加载器(extensions class loader)   --用来加载java的扩展库(jre/ext/*.jar路径下的内容)java虚拟机的实现会自动提供一个扩展目录。该类加载器在此目录里面查找并加载java类。 应用程序类加载器(application class loader)   --它根据java应用的类路径(classpath路径),一般来说,java应用的类都是由它来完成加载的。 自定义类加载器   --开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。 Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。 双亲委派过程:当一个类加载器收到类加载任务时,立即将任务委派给它的父类加载器去执行,直至委派给最顶层的启动类加载器为止。如果父类加载器无法加载委派给它的类时,将类加载任务退回给它的下一级加载器去执行;除了启动类加载器以外,每个类加载器拥有一个父类加载器,用户的自定义类加载器的父类加载器是AppClassLoader;双亲委派模型可以保证全限名指定的类,只被加载一次;双亲委派模型不具有强制性约束,是Java设计者推荐的类加载器实现方式; segmentfault.com/a/119000000…

java JVM基本机构 内存分配 垃圾回收

www.cnblogs.com/LeonJ-Java/… 经过javac编译器的编译,我们需要将.java后缀的源码编译为.class后缀的字节码,JVM作用就是将这些字节码通过类加载器加载到内存当中,从而实现我们的业务逻辑需求. JVM的内存区域分为5大块: 1.虚拟机栈(Stack):一般俗称栈区,是线程私有的.栈区一般与线程紧密相联,一旦有新的线程被创建,JVM就会为该线程分配一个对应的java栈区,在这个栈区中会有许多栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量,方法返回值等.栈帧中存储的局部变量随着线程的结束而结束,其生命周期取决于线程的生命周期,所以讲java栈中的变量都是线程私有的. 2.堆(Heap):真正存储对象的区域,当进行Object obj = new Object()这样一个操作时,真正的obj对象实例就会在heap中. 3.方法区(Method Area):包含常量池,静态变量等,有人说常量池也属于heap的一部分,但是严格上讲方法区只是堆的逻辑部分,方法区还有个别名叫做非堆(non-heap),所以方法区和堆还是有不同的. 4.程序计数器(Program Couter Register):用于保存当前线程的执行的内存地址.因为JVM是支持多线程的,多线程同时执行的时候可能会轮流切换,为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的位置,由此可以看出程序计数器也是线程私有的. 5.本地方法栈(Native Method Stack):性质与虚拟机栈类似,是为了方便JVM去调用本地方法接口的栈区,此处开发者很少去关存储机制: www.yanwushu.com/post/38.htm… www.cnblogs.com/zyj-bozhou/… 注,我也是了解有限,因此不深入探究其作用. 每个方法都会建立自己的内存栈,在这个方法定义的变量将会放到这块栈内存里,随着方法的结束,这个方法的内存栈也将自动销毁。(不需要GC回收) 当我们在程序中创建一个对象时,这个对象会被保存到运行时数据区中,以便反复利用(复用,因为创建对象的成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随着方法的结束而销毁,即使方法结束后,这个对象还可能被另外一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量去引用它时,系统的垃圾回收器(GC)才会在合适的时候回收它。 方法区: 1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class文件和static变量,方法。 虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。 2.方法区中包含的都是在整个程序中永远唯一的元素,如class文件,static变量,方法。 Java虚拟机运行时的数据区,包括方法区、虚拟机栈、堆、程序计数器、本地方法栈。 无论是虚拟机栈,程序计数器以及本地方法栈均属于线程私有,其生命周期与线程的生命周期一致,当线程执行完毕,其所占用的内存空间也就随之释放,因此这三部分是不存在垃圾回收问题的.Java开发者平常所说的垃圾回收,主要针对的是堆区和方法区而言的。 JVM的内存分配一般是先一次性分配出一个较大的空间,内存申请一般分为静态内存和动态内存.静态内存比较容易理解,编译时就能确定的内存就是静态内存,即内存是固定的,系统可以直接进行分配,比如short,int类型的变量,其占用的内存大小是固定的.而动态内存分配是只有当程序运行时,才能知道所要分配的内存空间大小,在运行之前是不确定的,因此属于动态内存. 垃圾回收:(如果一个对象,没有一个引用指向它,那么它就被认为是一个垃圾。) 年轻代(Young): 在年轻代中,又划分为伊甸园区(Eden),幸存0区(Survivor0)和幸存1区(Survivor1).所有对象最初被创建的时候都会在Eden区,当Eden区被填满时会进行Minor GC,如果还有对象存活,那么就会被JVM转移到幸存区0区或幸存1区.一般地,幸存0区和幸存1区中总有一个是空的,当其中一个区被填满时就会再进行Minor GC,就这样还能存活的对象就会在幸存0区和幸存1区之间来回切换.在幸存区经过很多次GC后依然还能存活的对象会被转移到年老代(一般需要设定一个存活阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置),当超过这个临界值时,就将还依旧存活的对象转移到年老代当中. 年老代(Old): 处于该代的java对象都是在年轻代久经考验而存活的对象,一般都是生命周期较长的对象.当年老代的空间被占满时,则不会进行Minor GC,而会触发Major GC/Full GC,回收整个堆内存.

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。 结论: 局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。 成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。 了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。 Java内存分配中的栈   在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 Java内存分配中的堆   堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

java方法区在对内存吗?

三种情况: java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变; java7中,static变量从永久代移到堆中; java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中

4、原文:blog.csdn.net/qq_30379689…

5、反射juejin.cn/post/684490…

6、原文:juejin.cn/post/684490…

1、Java 相关

  1. 容器(HashMap、HashSet、LinkedList、ArrayList、数组等)

需要了解其实现原理,还要灵活运用,如:自己实现 LinkedList、两个栈实现一个队列,数组实现栈,队列实现栈等。

HashMap、HashTable 和 CurrentHashMap 的核心区别(并发),其次内部数据结构的实现、扩容、存取操作,再深一点 哈希碰撞,哈希计算,哈希映射,为什么是头插法,扩容为什么是 2 的幂次等。

参考链接

JAVA容器-自问自答学HashMap 什么是HashMap? 从源码角度认识ArrayList,LinkedList与HashMap

  1. 内存模型

参考链接

理解Java内存模型 你了解Java内存模型么(Java7、8、9内存模型的区别)

  1. 垃圾回收算法(JVM)
JVM 类加载机制、垃圾回收算法对比、Java 虚拟机结构

当你讲到分代回收算法的时候,不免会被追问到新生对象是怎么从年轻代到老年代的,以及可以作为 root 结点的对象有哪些两个问题。
复制代码

1、谈谈对 JVM 的理解? 2、JVM 内存区域,开线程影响哪块区域内存? 3、对 Dalvik、ART 虚拟机有什么了解?对比?

ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,极大的提高了程序的运行效率,同时减少了手机的耗电量,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。

优点:

  • 系统性能的显著提升。
  • 应用启动更快、运行更快、体验更流畅、触感反馈更及时。
  • 更长的电池续航能力。
  • 支持更低的硬件。

缺点:

  • 机器码占用的存储空间更大,字节码变为机器码之后,可能会增加10%-20%(不过在应用包中,可执行的代码常常只是一部分。比如最新的 Google+ APK 是 28.3 MB,但是代码只有 6.9 MB。)
  • 应用的安装时间会变长。

4、垃圾回收机制和调用 System.gc()的区别?

参考链接

Java虚拟机(JVM)你只要看这一篇就够了!

  1. 类加载过程(需要多看看,重在理解,对于热修复和插件化比较重要)
  2. 反射
  3. 多线程和线程池
线程有哪些状态,哪些锁,各种锁的区别

并发编程:
synchronizedvolatile 、ReentrantLock 、CAS 的区别

synchronized 修饰实例方法和修饰静态方法有啥不一样。
复制代码

sleep 、wait、yield 的区别,wait 的线程如何唤醒它

  1. HTTP、HTTPS、TCP/IP、Socket通信、三次握手四次挥手过程
计算机网络部分:
1、TCP 有哪些状态
2、三次握手、四次挥手。为啥不是三次不是两次
3、HTTPS 和 HTTP 的区别,HTTPS 2.0 3.0?
4、浏览器输入一个 URL 按下回车网络传输的流程?
5、问的深一点的可能涉及到网络架构,每层有什么协议,FTP 相关原理,例:TCP 建立连接后,发包频率是怎么样的?

复制代码
  1. 设计模式(六大基本原则、项目中常用的设计模式、手写单例等)
1、生产者模式和消费者模式的区别?
2、单例模式双重加锁,为什么这样做?
3、知道的设计模式有哪些?
4、项目中常用的设计模式有哪些?
5、手写生产者、消费者模式。
6、手写观察者模式代码。
7、适配器模式、装饰者模式、外观模式的异同?
8、谈谈对 java 状态机的理解。
9、谈谈应用更新(灰度、强制更新、分区更新?)
复制代码
  1. 断点续传
  2. Java 四大引用
强引用、软引用、弱引用、虚引用的区别以及使用场景。

强引用置为 null,会不会被回收?

稍微问的深一些的面试官会和内存泄漏检测原理以及垃圾回收糅杂在一起。

复制代码
  1. Java 的泛型,<? super T> 和 <? extends T> 的区别
问到泛型、泛型擦除、通配符相关的东西
复制代码
  1. final、finally、finalize 的区别
  2. 接口、抽象类的区别

7、原文:juejin.cn/post/684490…

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,采用的是标记整理算法。 JVM内存区域的划分,哪些区域会发生 OOM JVM 的内存区域可以分为两类:线程私有和区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。 JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代。 方法区。方法区主要存放类的结构信息,比如静态属性和方法等等。 运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息。

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

堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。 JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。 方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。

类加载过程 Java 中类加载分为 3 个步骤:加载、链接、初始化。

加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。 链接。 链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。

验证。 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。 准备。 这一步会创建静态变量,并为静态变量开辟内存空间。 解析。 这一步会将符号引用替换为直接引用。

初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。

双亲委派模型 类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。

启动类加载器主要加载 jre/lib下的jar文件。 扩展类加载器主要加载 jre/lib/ext 下的jar文件。 应用程序类加载器主要加载 classpath 下的文件。

所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。 Java 中的集合类 HashMap 的原理 HashMap 的内部可以看做数组+链表的复合结构。数组被分为一个个的桶(bucket)。哈希值决定了键值对在数组中的寻址。具有相同哈希值的键值对会组成链表。需要注意的是当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构。 把握HashMap的原理需要关注4个方法:hash、put、get、resize。

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

put 方法。 put 方法主要有以下几个步骤:

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

get方法。 get 方法主要有以下几个步骤:

通过 hash 方法获取 hash 值,根据 hash 值寻址。 如果与寻址到桶的 key 相等,直接返回对应的 value。 如果发生冲突,分两种情况。如果是树,则调用 getTreeNode 获取 value;如果是链表则通过循环遍历查找对应的 value。

resize 方法。 resize 做了两件事:

将原数组扩展为原来的 2 倍 重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。

什么情况下 Java 会产生死锁,如何定位、修复,手写死锁 sleep 和 wait 的区别

sleep 方法是 Thread 类中的静态方法,wait 是 Object 类中的方法 sleep 并不会释放同步锁,而 wait 会释放同步锁 sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步代码块中使用 sleep 中必须传入时间,而 wait 可以传,也可以不传,不传时间的话只有 notify 或者 notifyAll 才能唤醒,传时间的话在时间之后会自动唤醒

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;
        }
    }
}

复制代码 volatile和synchronize的区别 Java中的线程池 线程通信 Java中的并发集合 Java中生产者与消费者模式 生产者消费者模式要保证的是当缓冲区满的时候生产者不再生产对象,当缓冲区空时,消费者不再消费对象。实现机制就是当缓冲区满时让生产者处于等待状态,当缓冲区为空时让消费者处于等待状态。当生产者生产了一个对象后会唤醒消费者,当消费者消费一个对象后会唤醒生产者。 三种种实现方式:wait 和 notify、await 和 signal、BlockingQueue。

wait 和 notify

//wait和notify import java.util.LinkedList;

public class StorageWithWaitAndNotify { private final int MAX_SIZE = 10; private LinkedList list = new LinkedList();

public void produce() {
    synchronized (list) {
        while (list.size() == MAX_SIZE) {
            System.out.println("仓库已满:生产暂停");
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        list.add(new Object());
        System.out.println("生产了一个新产品,现库存为:" + list.size());
        list.notifyAll();
    }
}

public void consume() {
    synchronized (list) {
        while (list.size() == 0) {
            System.out.println("库存为0:消费暂停");
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        list.remove();
        System.out.println("消费了一个产品,现库存为:" + list.size());
        list.notifyAll();
    }
}

}

复制代码 await 和 signal

import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock;

class StorageWithAwaitAndSignal { private final int MAX_SIZE = 10; private ReentrantLock mLock = new ReentrantLock(); private Condition mEmpty = mLock.newCondition(); private Condition mFull = mLock.newCondition(); private LinkedList mList = new LinkedList();

public void produce() {
    mLock.lock();
    while (mList.size() == MAX_SIZE) {
        System.out.println("缓冲区满,暂停生产");
        try {
            mFull.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    mList.add(new Object());
    System.out.println("生产了一个新产品,现容量为:" + mList.size());
    mEmpty.signalAll();

    mLock.unlock();
}

public void consume() {
    mLock.lock();
    while (mList.size() == 0) {
        System.out.println("缓冲区为空,暂停消费");
        try {
            mEmpty.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    mList.remove();
    System.out.println("消费了一个产品,现容量为:" + mList.size());
    mFull.signalAll();

    mLock.unlock();
}

}

复制代码 BlockingQueue

import java.util.concurrent.LinkedBlockingQueue;

public class StorageWithBlockingQueue { private final int MAX_SIZE = 10; private LinkedBlockingQueue list = new LinkedBlockingQueue(MAX_SIZE);

public void produce() {
    if (list.size() == MAX_SIZE) {
        System.out.println("缓冲区已满,暂停生产");
    }

    try {
        list.put(new Object());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("生产了一个产品,现容量为:" + list.size());
}

public void consume() {
    if (list.size() == 0) {
        System.out.println("缓冲区为空,暂停消费");
    }

    try {
        list.take();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("消费了一个产品,现容量为:" + list.size());
}

}

复制代码final、finally、finalize区别 final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。 finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。 finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。 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 的时候才会加载内部类,所以实现了延迟初始化。由于类只会被加载一次,并且类加载也是线程安全的,所以满足我们所有的需求。静态内部类实现的单例也是最为推荐的一种方式。 Java中引用类型的区别,具体的使用场景 Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。

强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。

软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。

弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。

虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。

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 也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误,具体根据需求来判断是否需要捕获,并不会在编译器强制要求。

volatile 一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主存的读写速度,所以直接从主存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主存中读取数据,在 CPU 运算之后在合适的时候刷新到主存中。 这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1 ,在两个线程中执行这句代码,假设i的初始值为0。我们期望两个线程运行后得到2,那么有这样的一种情况,两个线程都从主存中读取i到各自的高速缓存中,这时候两个线程中的i都为0。在线程1执行完毕得到i=1,将之刷新到主存后,线程2开始执行,由于线程2中的i是高速缓存中的0,所以在执行完线程2之后刷新到主存的i仍旧是1。 所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。 在Java的多线程开发中,有三个重要概念:原子性、可见性、有序性。

**原子性:**一个或多个操作要么都不执行,要么都执行。 可见性: 一个线程中对共享变量(类中的成员变量或静态变量)的修改,在其他线程立即可见。 有序性: 程序执行的顺序按照代码的顺序执行。 把一个变量声明为volatile,其实就是保证了可见性和有序性。 可见性我上面已经说过了,在多线程开发中是很有必要的。这个有序性还是得说一下,为了执行的效率,有时候会发生指令重排,这在单线程中指令重排之后的输出与我们的代码逻辑输出还是一致的。但在多线程中就可能发生问题,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的汇编代码中多了一个lock前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有3个作用:

确保指令重排的时候不会把屏障后的指令排在屏障前,确保不会把屏障前的指令排在屏障后。 修改缓存中的共享变量后立即刷新到主存中。 当执行写操作时会导致其他CPU中的缓存无效。

8、原文: 基础

并发

虚拟机

垃圾回收

9、十个不可忽视的Java基础知识