Java互联网科技面试题

20000字Java面试题解析(事务+缓存+数据库+多线程+JV

2019-09-29  本文已影响0人  程序员追风

前言

题目答案总结并非标准,仅供参考,如果有错误或者更好的见解,欢迎留言讨论!

image

事务

1、什么是事务?事务的特性(ACID)

什么是事务:事务是程序中一系列严密的操作,所有操作执行必须成功完成,否则在每个操作所做的更改将会被撤销,这也是事务的原子性(要么成功,要么失败)。

事务特性分为四个:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持续性(Durability)简称ACID。

1、原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。

2、一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。

3、隔离性:一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

4、持久性:也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

2、事务的隔离级别有几种,最常用的隔离级别是哪两种?

并发过程中会出现的问题:

事务的隔离级别有4种:

1、未提交读(Read uncommitted)

2、提交读(Read committed)

3、可重复读(Repeatable read)

4、串行化(Serializable)

image

缓存

3、分布式缓存的典型应用场景?

数据库

4、MongoDB与Mysql的区别?

两种数据库的区别:

Mongodb的鲜明特征:

Mongodb的优势:

Mongodb的缺陷:

Mongodb的应用场景:

不适用的场景:

关系型数据库和非关系型数据库的应用场景对比:

关系型数据库适合存储结构化数据,如用户的帐号、地址:

NoSQL适合存储非结构化数据,如文章、评论:

5、Mysql索引相关问题。

1)什么是索引?

2)索引具体采用的哪种数据结构呢?

3)InnoDb内存使用机制?

Innodb体系结构如图所示:

image

Innodb关于查询效率有影响的两个比较重要的参数分别是innodb_buffer_pool_size,innodb_read_ahead_threshold:

可以看出来,Mysql的缓冲池机制是能充分利用内存且有预加载机制,在某些条件下目标数据完全在内存中,也能够具备非常好的查询性能。

4)B+ Tree索引和Hash索引区别?

5)B+ Tree的叶子节点都可以存哪些东西吗?

6)这两者有什么区别吗?

7)聚簇索引和非聚簇索引,在查询数据的时候有区别吗?

8)主键索引查询只会查一次,而非主键索引需要回表查询多次(这个过程叫做回表)。是所有情况都是这样的吗?非主键索引一定会查询多次吗?

覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。当一条查询语句符合覆盖索引条件时,MySQL只需要通过索引就可以返回查询所需要的数据,这样避免了查到索引后再返回表操作,减少I/O提高效率。

如,表covering_index_sample中有一个普通索引 idx_key1_key2(key1,key2)。当我们通过SQL语句:select key2 from covering_index_sample where key1 = 'keytest';的时候,就可以通过覆盖索引查询,无需回表。

9)在创建索引的时候都会考虑哪些因素呢?

一般对于查询概率比较高,经常作为where条件的字段设置索引。

10)在创建联合索引的时候,需要做联合索引多个字段之间顺序,这是如何选择的呢?

在创建多列索引时,我们根据业务需求,where子句中使用最频繁的一列放在最左边,因为MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。

所以当我们创建一个联合索引的时候,如(key1,key2,key3),相当于创建了(key1)、(key1,key2)和(key1,key2,key3)三个索引,这就是最左匹配原则。

11)你知道在MySQL 5.6中,对索引做了哪些优化吗?

12)如何知道索引是否生效?

explain显示了MySQL如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。使用方法,在select语句前加上explain就可以了。

13)那什么情况下会发生明明创建了索引,但是执行的时候并没有通过索引呢?

在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案。这个成本最低的方案就是所谓的执行计划。优化过程大致如下:

14)为什么索引结构默认使用B+Tree,而不是Hash,二叉树,红黑树?

6、如何优化MySQL?

MySQL优化大致可以分为三部分:索引的优化、SQL语句优化和表的优化

索引优化可以遵循以下几个原则:

SQL语句优化,可以通过explain查看SQL的执行计划,优化语句原则可以有:

数据库表优化

7、为什么任何查询都不要使用SELECT ?

多线程

Java实现多线程有几种方式?

有三种方式:

线程的生命周期

image

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

start()方法和run()方法的区别?

Runnable接口和Callable接口的区别?

volatile关键字

volatile基本介绍:volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。另外,使用volatile还能确保变量不能被重排序,保证了有序性。

当一个变量定义为volatile之后,它将具备两种特性:

    //假设以下代码在线程A执行
    A.init();
    isOK=true;
    //假设以下代码在线程B执行
    while(!isOK){
       sleep();
     }
     B.init(); 

A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才能够进行自己的初始化。这里的先后关系依赖于isOK这个变量。如果没有volatile修饰isOK这个变量,那么isOK的赋值就可能出现在A.init()之前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们之前形成的那种依赖关系,可能就会出错。

volatile使用场景:

如果正确使用volatile的话,必须依赖下以下种条件:

在以下两种情况下都必须使用volatile:

什么是线程安全?

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

线程安全的级别:

sleep方法和wait方法有什么区别?

写一个会导致死锁的程序。

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (lock1){
                System.out.println("thread1 get lock1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("thread1 get lock2");
                }
                System.out.println("thread1 end");
            }
        }).start();
        new Thread(()->{
            synchronized (lock2){
                System.out.println("thread2 get lock2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("thread2 get lock1");
                }
                System.out.println("thread2 end");
            }
        }).start();
      }
    } 

类加载过程

1、类加载过程:加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

image

具体过程如下:

1)加载:首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。

2)链接:

验证:确保被加载类的正确性。

准备:为类的静态变量分配内存,并将其初始化为默认值。

解析:把类中的符号引用转换为直接引用。

image

直接引用可以是:

为什么要使用符号引用?

符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高。那为什么要用符号引用呢?这是因为类加载之前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,所以只能用符号引用来表示,当然,符号引用是要遵循java虚拟机规范的。

还有一种情况需要用符号引用,就例如前文举得变量的符号引用的例子,是为了逻辑清晰和代码的可读性。

3)为类的静态变量赋予正确的初始值。

2、类的初始化

1)类什么时候才被初始化:

2)类的初始化顺序

3、类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:

image

类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种:

image

4、加载器

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

image

加载器介绍:

1)BootstrapClassLoader(启动类加载器):

负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。

2)ExtensionClassLoader(标准扩展类加载器):

负责加载java平台中扩展功能的一些jar包,包括JAVAHOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。2)ExtensionClassLoader(标准扩展类加载器):负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。

3)AppClassLoader(系统类加载器):

负责加载classpath中指定的jar包及目录中class。

4)CustomClassLoader(自定义加载器):

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

类加载器的顺序

5、类加载器之双亲委派模型

image

垃圾回收机制

Java内存区域划分*

我们先来看看Java的内存区域划分情况,如下图所示:

image

私有内存区的区域名和相应的特性如下表所示:

image

虚拟机栈中的局部变量表里面存放了三个信息:

这个returnAddress和程序计数器有什么区别?前者是指示JVM的指令执行到了哪一行,后者是指你的代码执行到哪一行。

共享内存区(接下来主要讲jdk1.7)的区域名和相应的特性如下表所示:

image

哪些内存需要回收?

私有内存区伴随着线程的产生而产生,一旦线程中止,私有内存区也会自动消除,因此我们在本文中讨论的内存回收主要是针对共享内存区。

Java堆

image

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC (但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

新生代:刚刚新建的对象在Eden中,经历一次Minor GC, Eden中的存活对象就被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC, Eden和S0中的存活对象会被复制送入第二块survivor space S1。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就被送到老年代中。

为什么新生代内存需要有两个Sruvivor区:

先不去想为什么有两个Survivor区,第一个问题是,设置Survivor区的意义在哪里?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。那我们来想想在没有Survivor的情况下,有没有什么解决办法,可以避免上述情况:

image

显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。我们可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

设置两个Survivor区最大的好处就是解决了碎片化,下面我们来分析一下。为什么一个Survivor区不行?

第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:

刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。

老年代:如果某个对象经历了几次垃圾回收之后还存活,就会被存放到老年代中。老年代的空间一般比新生代大。

这个流程如下图所示:

image

什么时候回收?

Java并没有给我们提供明确的代码来标注一块内存并将其回收。或许你会说,我们可以将相关对象设为null或者用System.gc()。然而,后者将会严重影响代码的性能,因为每一次显示调用system.gc()都会停止所有响应,去检查内存中是否有可回收的对象,这会对程序的正常运行造成极大威胁。

另外,调用该方法并不能保障JVM立即进行垃圾回收,仅仅是通知JVM要进行垃圾回收了,具体回收与否完全由JVM决定。

生存还是死亡

可达性算法:这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

image

二次标记:在可达性分析算法中被判断是对象不可达时不一定会被垃圾回收机制回收,因为要真正宣告一个对象的死亡,必须经历两次标记的过程。

如果发现对象不可达时,将会进行第一次标记,此时如果该对象调用了finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,如果在此队列中该对象没有成功拯救自己(拯救自己的方法是该对象有没有被重新引用),

那么GC就会对F-Queue队列中的对象进行小规模的第二次标记,一旦被第二次标记的对象,将会被移除队列并等待被GC回收,所以finalize()方法是对象逃脱死亡命运的最后一次机会。

在Java语言中,可作为GC Roots的对象包括下面几种:

GC的算法

引用计数法(Reference Counting):

给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,但是不能解决互相循环引用的问题。

优点:

缺点:

标记-清除(Mark-Sweep)算法:

分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(后续的垃圾回收算法都是基于此算法进行改进的)。

缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,会产生很多碎片。

image

复制算法:

将可用内存按容量划分为大小相等的两块,每次只用其中一块。当这一块用完了,就将还存活的对象复制到另外一块上面,然后把原始空间全部回收。高效、简单。

缺点:将内存缩小为原来的一半。

image

标记-整理(Mark-Compat)算法

标记过程与标记-清除算法过程一样,但后面不是简单的清除,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

image

分代收集(Generational Collection)算法

新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清除”算法进行回收。

增量回收GC和并行回收GC这里就不做具体介绍了,有兴趣的朋友可以自行了解一下。

垃圾收集器

Serial收集器:单线程收集器,表示在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。"Stop The World"。

ParNew收集器:实际就是Serial收集器的多线程版本。

Parallel Scavenge收集器:该收集器比较关注吞吐量(Throughout)(CPU用于用户代码的时间与CPU总消耗时间的比值),保证吞吐量在一个可控的范围内。

CMS(Concurrent Mark Sweep)收集器:CMS收集器是一种以获取最短回收停顿时间为目标的垃圾收集器,是基于“标记——清除”算法实现的。

其回收过程主要分为四个步骤:

由于初始标记和重新标记速度比较快,其它工作线程停顿的时间几乎可以忽略不计,所以CMS的内存回收过程是与用户线程一起并发执行的。初始标记和重新标记两个步骤需要Stop the world;并发标记和并发清除两个步骤可与用户线程并发执行。“Stop the world”意思是垃圾收集器在进行垃圾回收时,会暂停其它所有工作线程,直到垃圾收集结束为止。

CMS的缺点:

G1(Garbage First)收集器:G1收集器是一款成熟的商用的垃圾收集器,是基于“标记——整理”算法实现的。

其回收过程主要分为四个步骤:

G1收集器的特点:

CMS收集器与G1收集器的区别:

JDK 1.8 JVM的变化

1、为什么取消方法区

2、JDK 1.8里Perm区中的所有内容中字符串常量移至堆内存,其他内容如类元信息、字段、静态属性、方法、常量等都移动到元空间内。

3、元空间

元空间(MetaSpace)不在堆内存上,而是直接占用的本地内存。因此元空间的大小仅受本地内存限制

也可通过参数来设定元空间的大小:

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

元空间的特点:

性能优化:

java自动装箱拆箱总结

当基本类型包装类与基本类型值进行==运算时,包装类会自动拆箱。即比较的是基本类型值。

具体实现上,是调用了Integer.intValue()方法实现拆箱。

Integer b = 1;
Integer c = new Integer(1);
System.out.println(a == b); //true
System.out.println(a == c); //true
System.out.println(c == b); //false
Integer a = 1;
会调用这个 Integer a = Integer.valueOf(1);
Integer已经默认创建了数值【-128到127】的Integer常量池
Integer a = -128;
Integer b = -128;
System.out.println(a == b); //true
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
Java的数学计算是在内存栈里操作的
c1 + c2 会进行拆箱,比较还是基本类型
int a = 0;
Integer b1 = 1000;
Integer c1 = new Integer(1000);
Integer b2 = 0;
Integer c2 = new Integer(0);
System.out.println(b1 == b1 + b2); //true
System.out.println(c1 == c1 + c2); //true
System.out.println(b1 == b1 + a); //true
System.out.println(c1 == c1 + a); //true 

最后

欢迎大家一起交流,喜欢文章记得关注我点个赞哟,感谢支持!

上一篇下一篇

猜你喜欢

热点阅读