三大特性:封装、继承和多态
面试集锦
https://www.cnblogs.com/peke/p/7894685.html
1.jvm类加载
类加载器:“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。
双亲委派机制得工作过程:
1-类加载器收到类加载的请求;
2-把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
3-启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
采取双亲委托模式主要有两点好处:
避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改
类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。
java跨平台
首先开发好的程序 HellowordApp.java经过编译器Compiler的编译变为HellowordApp.class文件,然而这个.class文件并不是真正的本地可以执行的指令 我们可以把这个.class文件称之为“中间码”
2.不同的计算机操作系统有着相应的JVM 比如win32位的 win64位的 linux系统的,.class文件经过Interpreter(解释器,也就是JVM)的解释(或者称之为翻译),变为真正的本地可执行指令(“00101001001…”)
总结:一处边写到处运行是因为程序的中间码.class文件是标准的,一致的,在各个系统对应的JVM上都可以被识别解释然后运行,所以可以实现跨平台
4.java内存分区
https://blog.csdn.net/weixin_41205419/article/details/84931429
程序计数器记录的是当前线程正在执行的字节码指令的地址
虚拟机栈可以看做是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息
5.abstract class抽象类和interface接口有什么区别
抽象类不能创建实际对象,含有抽象方法的抽象类必须定义为 abstract class。
接口可以说成是一种特殊的抽象类,接口中的所有方法都必须是抽象的,接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final。
两者的区别:
a. 抽象类可以有构造方法,接口中不能有构造方法。
b. 抽象类中可以有普通成员变量,接口中没有普通成员变量。
c. 抽象类中可以包含非抽象普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的方法。
d. 抽象类中的抽象方法的访问权限可以是 public、protected 和(默认类型,虽然 eclipse 不报错,但也不能用,默认类型子类不能继承),接口中的抽象方法只能是 public 类型的,并且默认即为 public abstract 类型。
e. 抽象类中可以包含静态方法,在 JDK1.8 之前接口中不能不包含静态方法,JDK1.8 以后可以包含。
f. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问权限可以是任意的,但接口中定义的变量只能是 public static final 类型的,并且默认即为 public static final 类型。
g. 一个类可以实现多个接口,用逗号隔开,但只能继承一个抽象类,接口不可以实现接口,但可以继承接口,并且可以继承多个接口,用逗号隔开。
6.谈谈你对MVC、MVP和MVVM的理解
MVC分三个层:
视图层(View):对应于xml布局文件和java代码动态view部分。
控制层(Controller):MVC中Android的控制层是由Activity来承担的,Activity作为初始化页面,展示数据的操作。但是因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。
模型层(Model):针对业务模型,建立数据结构和相关的类,它主要负责网络请求,数据库处理,I/O的操作。
总结:
具有一定的分层,model彻底解耦,controller和view并没有解耦。Controller既要承担页面的初始化又要处理控制逻辑,承担的功能过多,其代码量也会过多。Model和View要直接交互,它们耦合度依然更高。
MVP
MVP在MVC的基础上,引入了中间层Present把Model和View层彻底解耦,由Present来控制逻辑,解决了MVC中Controller和View分不清的问题。但是随着业务逻辑的增加,一个页面可能会非常复杂,UI的改变是非常多,会有非常多的case,这样就会造成View的接口会很庞大。
MVVM
MVP中我们说过随着业务逻辑的增加,UI的改变多的情况下,会有非常多的跟UI相关的case,这样就会造成View的接口会很庞大。而MVVM就解决了这个问题,通过双向绑定的机制,实现数据和UI内容,只要想改其中一方,另一方都能够及时更新的一种设计理念,这样就省去了很多在View层中写很多case的情况,只需要改变数据就行。
如果项目简单,没什么复杂性,未来改动也不大的话,那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用。
对于偏向展示型的app,绝大多数业务逻辑都在后端,app主要功能就是展示数据,交互等,建议使用mvvm。
对于工具类或者需要写很多业务逻辑app,使用mvp或者mvvm都可。
7.java同步的方法
volatile和synchronize
重入锁 ReentrantLock 重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞
RenntrantLock是一个排它重入锁,重入次数为Integer.MAX_VALUE,其中通过构造实现两大核心(公平锁,非公平锁)。在默认情况下是非公平锁
Lock与synchronized有以下区别:
Lock是一个接口,而synchronized是关键字。
synchronized会自动释放锁,而Lock必须手动释放锁。
Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
通过Lock可以知道线程有没有拿到锁,而synchronized不能。
Lock能提高多个线程读操作的效率。
synchronized能锁住类、方法和代码块,而Lock是块范围内的
synchronized 是非公平锁,可以重入。
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁。
在释放锁的时候总是没有新的兔子来打扰,则非公平锁等于公平锁;
2、若释放锁的时候,正好一个兔子来喝水,而此时位于队列头的兔子还没有被唤醒(因为线程上下文切换是需要不少开销的),此时后来的兔子则优先获得锁,成功打破公平,成为非公平锁;
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
synchronized是用java的monitor机制来实现的,就是synchronized代码块或者方法进入及退出的时候会生成monitorenter跟monitorexit两条命令。线程执行到monitorenter时会尝试获取对象所对应的monitor所有权,即尝试获取的对象的锁;monitorexit即为释放锁。
https://www.cnblogs.com/nevermorewang/p/9864797.html
偏向锁的特征是:
简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个字段如果是空的,
第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.
这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
但是偏向锁也有一个问题,就是当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态。
也就是说一旦偏向锁冲突,双方都会升级为轻量级锁。
重量级锁:
这个就是我们平常说的synchronized锁,如果抢占不到,则线程阻塞,等待正在执行的线程结束后唤醒自己,然后重新开始竞争。之所以说是重量级,是因为线程阻塞会让出cpu资源,从内核态转换为用户态,然后执行的时候再次转换为内核态,这个过程中,cpu要切换线程,看这个线程上次执行到哪儿了,这次应该从哪儿开始,相关的变量有哪些之类的,这就是所谓的执行上下文,这个上下文的切换对宝贵的cpu资源来说是“无用功”,因为这是在为执行做准备条件,如果cpu大量的时间用在这些“无用功”上,当然也就出活儿少,也就影响执行效率了。
执行流程
每一个线程在准备获取共享资源时:
第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” ;
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord;
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己;
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。
另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。
如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。
同时需要注意锁可以升级,但是不能降级。
synchronized就是这样,默认开始是偏向锁,有竞争就逐渐升级,最终可能是重量级锁的一个过程。锁定的区域就是对象的Mark Word的内容。
8.Java 内存模型中的可见性、原子性和有序性。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义
9.volatile
可见性 有序性
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
我们知道:如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。这个就是所谓的“可见性”,就是一个线程修改了,其他线程能知道这个操作,这就是可见性。如何实现的呢?volatile修饰的变量在生成汇编代码的时候,会产生一条lock指令,lock前缀的指令在多核处理器下会引发两件事情:
1、将当前处理器缓存好的数据写回到系统内存;
2、这个写回内存的操作会使得在其它cpu里缓存了该内存地址的数据无效;
10锁
乐观锁 VS 悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
11 GC
基本思想是通过一系列称为“GC roots”的对象作为起始点,可以作为根节点的是:
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
12常见GC算法
引用计数法
简单但是速度很慢,缺陷是不能处理循环引用的情况。
原理:此对象有一个引用,既增加一个计数器,删除一个引用减少一个计数器,垃圾回收时,只回收计数器为0的对象,此算法最致命的是无法处理循环引用的情况。
标记-清除算法
标记清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:
1. 一个是效率问题,标记和清除两个过程的效率都不高
2. 另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续的内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用的内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
只是这种算法的代价是将内存缩小为原来的一半
标记-整理算法
分代收集算法 年轻带 老年代
年轻带:存活率低,复制清除算法
老年代:存活率高,标记清除/整理算法
年轻带:eden区,先在eden,两个suvivor区,from和to区 老年代
当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。
一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。
这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,
经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收
当老年代存活对象多时,每次minor gc查询老年代所有对象影响gc效率(因为gc stop-the-world),所以在老年代有一个write barrier(写屏障)来管理的card table(卡表),card table存放了所有老年代对象对新生代对象的引用。
13 java注解
元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
14hashmap的put和get过程
当调用put(),首先会根据key生成一个 hash值
拿到了hash值后,调用 putVal(),做了如下操作
将对象table赋值给tab,并以tab是否为空作为是否第一次调用此方法的判断,是则resize()并给tab,n赋值;
获取tab的第i个元素:根据 (n - 1) & hash 算法 ,计算出i找到,如果为空,调用newNode() ,赋值给tab第i个;
如果不为空,可能存在2种情况:hash值重复了,也就是put过程中,发现之前已经有了此key对应的value,则暂时e = p;
至于另外一种情况就是位置冲突了,即根据(n - 1) & hash算法发生了碰撞,再次分情况讨论;
1.以链表的形式存入;
2.如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
最后,如果e不为空,将e添加到table中(e.value 被赋值为 putVal()中的参数 value);
15Java的线程生命周期
有六种状态:
New(初始化状态)
Runnable(可运行/运行状态)
Blocked(阻塞状态)
Waiting(无时间限制的等待状态)
Timed_Waiting(有时间限制的等待状态)
Terminated(终止状态)
16ConcurrentHashMap
在ConcurrentHashMap中有个重要的概念就是Segment。我们知道HashMap的结构是数组+链表形式,从图中我们可以看出其实每个segment就类似于一个HashMap。Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。在ConcurrentHashMap中有2的N次方个Segment,共同保存在一个名为segments的数组当中。可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。
为什么说ConcurrentHashMap的性能要比HashTable好,HashTables是用全局同步锁,而CconurrentHashMap采用的是锁分段,每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不干扰。
简单说一下数据库的三范式?
第一范式:数据库表的每一个字段都是不可分割的
第二范式:数据库表中的非主属性只依赖于主键
第三范式:不存在非主属性对关键字的传递函数依赖关系
事务的四个基本特征
原子性,一致性,隔离性,持久性
什么时候会使用HashMap?他有什么特点?
是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
你知道HashMap的工作原理吗?
通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
你知道hash的实现吗?为什么要这样实现?
在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。
hashcode
https://www.jianshu.com/p/b9558ad35f70
JNI
入口
JNI_OnLoad里gClasses.add(env, new SurfaceJni(env));
jnihelper帮助封装,包括堆jni类型和注册方法等进行封装
子模块initialize方法里addNativeMethod,然后registerNativeMethods
https://www.jianshu.com/p/87ce6f565d37
第1步:在Java中先声明一个native方法
第2步:编译Java源文件javac得到.class文件
第3步:通过javah -jni命令导出JNI的.h头文件
第4步:使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。)
第5步:将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib)
第6步:通过Java命令执行Java程序,最终实现Java调用本地代码。
_JavaVM是C++中JavaVM结构体,调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能够获取JNIEnv结构体
用JavaVM结构体_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以释放 本线程的JNIEnv
JNIEnv结构
JNIEnv是一个指针,指向一个线程相关的结构,线程相关结构,线程相关结构指向JNI函数指针数组,这个数组中存放了大量的JNI函数指针,这些指针指向了详细的JNI函数。
JNIEnv的作用:
调用Java 函数:JNIEnv代表了Java执行环境,能够使用JNIEnv调用Java中的代码
操作Java代码:Java对象传入JNI层就是jobject对象,需要使用JNIEnv来操作这个Java对象
调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能够获取JNIEnv结构体
静态注册:
先由Java得到本地方法的声明,然后再通过JNI实现该声明方法
动态注册:
先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法。
动态debug
gradle-experimental插件
so 的加载流程是怎样的,生命周期是怎样的
这个要从 java 层去看源码分析,是从 ClassLoader 的 PathList 中去找到目标路径加载的,同时 so 是通过 mmap 加载映射到虚拟空间的。生命周期加载库和卸载库时分别调用 JNI_OnLoad 和 JNI_OnUnload() 方法
Arraylist和linkedist
ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率低。
Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)
ArrayList 最坏的一种情况,时间复杂度是 O(n) ,而 LinkedList 中插入或删除的时间复杂度仅为 O(1)