Java 面试题

7 篇文章 0 订阅
2 篇文章 0 订阅

基础知识

实例方法和静态方法的区别

  • 实例方法:
    • 当类的字节码文件加载到内存中时,类的实例方法并没有被分配入口地址,只有当该类的对象创建以后,实例方法才被分配了入口地址,所以,实例方法并不能被类名. 调用。
    • 当我们创建第一个类的对象时,实例方法的入口地址就会完成分配,在后续创建的对象时,不会再重新分配新的入口地址,也就是说,该类的实例对象共享该实例方法的入口地址,当该类的所有实例对象全部被销毁时,入口地址才会消失。
  • 静态方法:
    • 当类的字节码文件加载到内存,类方法的入口地址就会分配完成,所以类方法不仅可以被该类的对象调用,也可以直接通过类名完成调用。类方法的入口地址只有程序退出时才会消失。
  • 静态方法可以通过类名.调用,实例方法又称非静态方法,必须先初始化类的实例,通过实例才能调用。
  • 因为静态方法的入口地址的分配要早于实例方法的入口地址分配时间,所有在定义类方法和实例方法是有以下规则需要遵循:
    • 在静态方法中不能引用实例变量
    • 在类方法中不能使用superthis关键字
    • 静态方法中不能调用非静态方法

String、StringBuffer和StringBuilder区别

  • String: String类是不可变的类,因此每次对String类型对象进行修改时,都会重新生成一个String类型对象,然后将引用指向新的String对象,所以经常修改的字符串最好不要使用String类,会影响性能。查看源码可知String类型底层使用的是字符数组,而同String类一样,该数组被final修饰。
  • StringBuffer:线程安全的可变字符序列,底层采用的是字符数组,但并没有被final修饰。可通过某些方法调用可以改变该序列的长度和内容。
  • StringBuilder:线程不安全的可变字符序列,是JDK1.5新增的,与StringBuffer不同之处在于其不保证同步,适用于单线程下。由于是线程不安全的,所以效率要比StringBuffer高。

hashCode方法的作用

  • hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSetHashMap以及Hashtable
  • 在为了保证集合存储唯一的情况下,当数据量又特别大的情况下,逐一使用equals方法进行比较显然效率低下。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
  • hashCodeequals方法对于判定对象是否相同的作用:
    • 对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
    • 如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
    • 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false
    • 如果两个对象的hashcode值相等,则equals方法得到的结果未知。

下面这段话摘自Effective Java一书:

  • 在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数。
    • 设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果在将一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码。
  • 如果两个对象根据equals方法比较是相等的,那么调用两个对象的hashCode方法必须返回相同的整数结果。
  • 如果两个对象根据equals方法比较是不等的,则hashCode方法不一定得返回不同的整数。

Object类中有的方法

  • clone():创建并返回一个该对象的副本;
  • equals():指示其它对象是否与此对象相等;
  • finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法;
  • getClass():返回此Object运行时的类;
  • hashCode():返回该对象的哈希码值;
  • notify():唤醒在此对象监视器上等待的单个线程;
  • notifyAll():唤醒在此对象监视器上等待的所有线程
  • toString():返回该对象的字符串表示;
  • wait():在其它线程调用该对象的notify()方法获notifyAll()方法前,导致当前线程等待;
  • wait(long timeout):在其它线程调用该对象的notify()方法获notifyAll()方法前,或者超过指定时间量前,导致当前线程等待;
  • wait(long timeout, int nanos):在其它线程调用该对象的notify()方法获notifyAll()方法前,或者其它某个线程中断当前线程,或者超过指定时间量前,导致当前线程等待;

Java中的异常分类

异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。

Throwable类是Java异常类型的顶层父类,一个对象只有是Throwable类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。
在这里插入图片描述

  • Error: Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
  • Exception: Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
    • 非检查异常(unchecked exception): ErrorRuntimeException以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。
    • 检查异常(checked exception): 除了ErrorRuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try...catch...finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。

ArrayList和LinkedList的内部实现及各自应用场景

  • ArrayList:底层实现原理其实是一个动态数组,初始容量为10,每次扩容为之前容量的0.5倍;
    • 查询元素时,直接根据索引查询,即数组式的查询,底层调用的也就是数组的查询方法。
    • 增加元素时,如果当前容量不够大时,创建一个1.5倍大数组,将元素进行复制到新数组中。如果没有指定索引位置,则直接在元素的末尾添加该元素。如果指定索引位置,那么则先将该索引的位置及以后的元素向后移(复制)一位,然后再置该索引位置的元素为增加的值。
    • 修改元素时,则是找到指定的索引位置,将该值修改为要修改的值。
    • 删除元素时,则是找到该指定索引的位置,该索引之后的所有元素向前移(复制)以为,然后使集合长度的最后一位索引的元素为null
  • LinkedList:底层实现原理是基于双向链表实现的。Node是结点,每个结点上记录着上一个结点的地址和下一个结点的地址,所以其在内存中是不连续的。
    • 查询元素时,从头遍历或者从尾遍历,根据记录的结点地址,一个一个的寻找所以查询较慢。
    • 增加元素时, 只需要修改指向结点的引用,让引用指向添加的元素即可。
    • 修改元素时, 将该结点上数据修改好,返回旧数据对象。
    • 删除元素时, 只需要将指向该元素的引用修改为指向下一个结点和之前一个结点即可。
  • 总结:
    • ArrayListLinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
    • ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
    • LinkedList不支持高效的随机元素访问。
    • ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。
  • 注意:
    • LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的getremoveinsert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)
    • LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。
    • ArrayList实现了可变大小的数组。它允许所有元素,包括nullArrayList没有同步。

当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

ClassLoader的作用

  • Java类的类加载器
    • BootStrap ClassLoader: 最顶层的加载类,主要加载核心类库,rt.jarresources.jarcharset.jarclass等。
    • Extention ClassLoader: 扩展的类加载器,加载JAVA_HOME下的\lib\ext目录下的jar包和class文件
    • Appclass Loader又称SystemAppClass: 加载当前应用的classpath下的所有类。
  • 类加载器的加载流程
    • 每个加载器都有一个父加载器,并不一定是父类。App的父加载器是ExtExt的父加载器是Boot,但是在JAVA无法获取BootStrap加载器,因为它是有C/C++编写的,本身是虚拟机的一部分。
    • 双亲委托机制:如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点
  • 总结
    • ClassLoader用来加载class文件的
    • 系统内置的ClassLoader通过双亲委托来加载加载指定路径下的class和资源。
    • 可以自定义ClassLoader,一般覆盖findClass()方法
    • ContextClassLoader与线程相关,可以获取和设置,可以绕过双亲委托的机制。

Volatile关键字在Java中的作用

一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰后,就具备了如下的语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其它的线程来说是立即可见的。
  • 禁止进行指令的重排序,可一定程度上保证有序性。
  • 使用volatile关键字的话,当某一线程对被该关键字修饰的变量进行修改时,会导致其它线程的工作内存的缓存行无效,那么他们在读取该变量时就会重新从对应的主存地址中读取最新的值。
  • volatile关键字不能保证原子性。

volatile的原理和实现机制:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在他前面的操作已经全部完成。
  • 它会强制对缓存的修改操作立即写入主存。
  • 如果是写操作,它会导致其它CPU中对应的缓存行无效。

使用volatile关键字的场景

  • synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意 volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
    • 对变量的写操作不依赖与当前值
    • 该变量没有包含在具有其它变量的不变式中。

Executors类可以创建哪几种线程池

  • newFixedThreadPool:创建固定大小的线程池。 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池则会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。 如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newSingleThreadExecutor:创建一个单线程的线程池。 这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newScheduledThreadPool:创建一个大小无限的线程池。 此线程池支持定时以及周期性执行任务的需求。

Callable和Future

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

自从Java 1.5开始,就提供了CallableFuture,通过它们可以在任务执行完毕之后得到任务执行结果。Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

  • Callable: 位于Java.util.concurrent包下,是一个接口,只声明了一个方法,叫call()。该方法使用一个泛型接口,call()函数返回的类型就是传递进来的V类型。

  • Future: 也位于Java.util.concurrent包下,也是一个接口。Future就是对于具体的RunnableCallable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。该接口下有5个方法:

    • cancle:用来取消任务,如果取消任务成功则返回true,取消人物失败则返回false。该方法有一个boolean类型的参数mayInterruptIfRunning,表示是否允许取消正在执行却没有执行完成的任务。
      • 如果任务已经完成,则无论mayInterruptIfRunningtrue还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false
      • 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若为false,则返回false
      • 如果任务还没有执行,则无论mayInterruptIfRunningtrue还是false,肯定返回true
    • isCancelled:表示任务是否取消成功,如果在任务正常完成前被取消成功,则返回true
    • isDone:表示任务是否已经完成,若任务完成,则返回true
    • get:用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回。
    • get(long timeout, TimeUnit unit):用来获取执行结果,如果在指定时间内,未获取到结果,抛出异常。

    由上可以看出Future提供了三种功能:

    • 判断任务是否已经完成
    • 能够中断任务
    • 能够获取任务执行结果
  • FutureTask: 实现了RunnableFuture<V>接口,而该接口继承了RunnableFuture<V>接口。FutureTaskFuture接口的一个唯一实现类。

NIO及其使用场景

NIO:New IOIO又称非阻塞式IO(Non Block IO)java.NIO包里包括三个基本的组件:

  • buffer: 因为是面向缓冲的,所以buffer是最底层的必要类,这也是IONIO的根本不同,虽然stream等有buffer开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而NIO却是直接读到buffer中进行操作。因为读取的是字节,所以在操作文字时,要用charset类进行编解码操作。
  • channel: 类似于IOstream,但是不同的是除了FileChannel,其他的channel都能以非阻塞状态运行。FileChannel执行的是文件的操作,可以直接DMA操作内存而不依赖于CPU。其他比如socketchannel就可以在数据准备好时才进行调用。
  • selector: 用于分发请求到不同的channel,这样才能确保channel不处于阻塞状态就可以收发消息。

与传统IO的区别:面向流和面向缓冲、阻塞和非阻塞和有无选择器。

与传统IO的优势:

  • 在老的IO包中,serverSocketsocket都是阻塞式的,因此一旦有大规模的并发行为,而每一个访问都会开启一个新线程。这时会有大规模的线程上下文切换操作(因为都在等待,所以资源全都被已有的线程吃掉了),这时无论是等待的线程还是正在处理的线程,响应率都会下降,并且会影响新的线程。
  • NIO包中的serverSocketsocket就不是这样,只要注册到一个selector中,当有数据放入通道的时候,selector就会得知哪些channel就绪,这时就可以做响应的处理,这样服务端只有一个线程就可以处理大部分情况(当然有些持续性操作,比如上传下载一个大文件,用NIO的方式不会比IO好)。

LinkedBolckingQueue和ArrayBlockingQueue

  • 队列中的锁实现不同
    • ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一把锁;
    • LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock
  • 生产或消费是操作不同
    • ArrayBlockingQueue基于数组,在生产和消费的时候,直接将枚举对象插入或移除的,不会产生或销毁额外的对象实例。
    • LinkedBlockingQueue基于链表,在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据系统中,其对于GC的影响还是存在一定的区别。
  • 队列大小初始化方式不同
    • ArrayBlockingQueue是有界的,必须指定队列的大小。
    • LinkedBlockingQueue是无界的,可以不指定队列的大小,但是默认是Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的。
  • 从源码上看,有如下异同点
    • 相同点:都不允许元素为null;因为都有锁机制,所以都是线程安全的队列
    • 不同点:ArrayBlockingQueue内部维持一把锁和两个条件同一时刻只能有一个线程队列的一端操作。LinkedBlockingQueue内部维持两把锁和两个条件,同一时刻可以有两个线程在队列的两端操作,但同一时刻只能有一个线程在一端操作。

什么是死锁,如何避免

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。 当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

产生死锁的四个比必要条件:

  • 互斥条件: 一个资源每次只能被一个线程使用。

  • 请求和保持条件: 一个线程因请求资源而阻塞时,对已经获得的资源保持不放。

  • 不可抢占条件: 线程已获得的资源,在未使用完之前,不能强行剥夺,只能在进程使用完时自己释放。

  • 循环等待条件: 若干线程之间形成一种头尾相连的循环等待资源关系。

    这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

预防死锁的方法:

  • 破坏互斥条件:就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般互斥条件是无法破坏的。因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏互斥条件。
  • 破坏请求和保护条件: 在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
    • 方法1:所有进程在运行之前,必须一次性地申请在整个运行过程中所需的全部资源。这样,该进程在整个运行期间,便不会再提出资源请求,从而破坏了请求条件。系统在分配资源时,只要有一种资源不能满足进程的要求,即使其它所需的各资源都空闲也不分配给该进程,而让该进程等待。由于该进程在等待期间未占有任何资源,于是破坏了保持条件。
    • 方法2:要求每个进程提出新的资源申请前,释放它所占有的资源。
  • 破坏不可抢占条件: 允许对资源实行抢夺。
    • 如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
    • 如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。
  • 破坏循环等待条件: 将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
  • 死锁解除: 一旦检测出死锁,就应立即釆取相应的措施,以解除死锁。死锁解除的主要两种方法:
    • 抢占资源。 从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
    • 终止(或撤销)进程。 终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态解脱出来。

总结

  • 一般情况下,如果同一个线程先后两次调用lock。在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)
  • 另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调⽤用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调⽤用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。

Java里面的ThreadLocal是怎样实现的

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal类提供如下几个方法

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • get()方法是用来获取ThreadLocal在当前线程中保存的变量副本;
  • set()用来设置当前线程中变量的副本;
  • remove()用来移除当前线程中变量的副本;
  • initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法

ThreadLocal为每个线程创建副本的过程如下:

  • 首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
  • 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals

总结

  • 实际上通过ThreadLocal创建的副本是存储在每个线程自己的ThreadLocals中的;
  • 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量。
  • 在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
  • 应用场景:最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

Java为何还会存在内存泄漏

  • 内存泄漏: 简单来说就是一个东西放在内存里的时间太长了,当你的程序都跑完了,它还存在那里。这时它是白白的占用了你的内存,累积起来占用的内存越来越多……最后就会导致JVM报错:out of memory。他占用的是我们的物理内存。

  • 产生内存泄漏的根本原因: 内存对象明明已经不需要了,但仍然保留着这块内存和它的访问方式(引用)。

  • 原理:Java中对内存对象的访问,使用的是引用的方式。在Java代码中我们维护一个内存对象的引用变量,通过这个引用变量的值,我们可以访问到对应的内存地址中的内存对象空间。在Java程序中,这个引用变量本身既可以存放堆内存中,又可以放在代码栈的内存中(与基本数据类型相同)。GC线程会从代码栈中的引用变量开始跟踪,从而判定哪些内存是正在使用的。如果GC线程通过这种方式,无法跟踪到某一块堆内存,那么GC就认为这块内存将不再使用了(因为代码中已经无法访问这块内存了)。

    通过这种有向图的内存管理方式,当一个内存对象失去了所有的引用之后,GC就可以将其回收。反过来说,如果这个对象还存在引用,那么它将不会被GC回收,哪怕是Java虚拟机抛出OutOfMemoryError

  • 常见的内存泄漏: 栈溢出、堆溢出、代码栈溢出。

JVM堆的基本结构

  • JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。 它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
  • 堆内存是由存活和死亡的对象组成的。 存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
  • 堆内存中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
    • 年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured)。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
    • 年老代: 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    • 持久代: 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
  • JDK1.8中,永久代已经从java堆中移除,String直接存放在堆中,类的元数据存储在meta space中,meta space占用外部内存,不占用堆内存。可以说,在java8的新版本中,持久代已经更名为了元空间(meta space)

数据库性能的优化

优化法则可以归纳为一下5个层次

  • 减少数据访问(减少磁盘访问)
  • 返回更少数据(减少网络传输或磁盘访问)
  • 减少交互次数(减少网络传输)
  • 减少服务器CPU开销(减少CPU及内存开销)
  • 利用更多资源(增加资源)

优化方案

  • 对查询进行优化,要尽量避免全表扫描,首先应考虑在whereorder by涉及的列上建立索引
  • 尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而使用全表扫描。
  • 最好不要给数据库留NULL,尽可能使用NOT NULL填充数据库。NULL值也需要空间,如:char(100)型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用100个字符的空间的,如果是varchar这样的变长字段,null不占用空间。
  • 应尽量避免在where子句中使用!=< >操作符,否则数据库引擎会放弃使用索引而进行全表扫描。
  • 应尽量避免在where子句中使用or来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描。
  • innot in要慎用,否则会导致全表扫描。对于连续的数值,能用between就不要用in了。很多时候用exist代替in是一个好的选择。
select num from a where num in(select num from b);
select num from a where exists(select 1 from b where num = a.num);    
  • 尽量避免在where子句中对字段进行表达式操作,或者对字段进行函数操作。
  • 不要在where子句中的=左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
  • 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
  • Update语句,如果只更改1、2个字段,不要Update全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。
  • 对于多张大数据量(这里几百条就算大了)的表JOIN,要先分页再JOIN,否则逻辑读会很高,性能很差。
  • select count(*) from table;这样不带任何条件的count会引起全表扫描,并且没有任何业务意义,是一定要杜绝的。
  • 索引并不是越多越好,索引固然可以提高相应的select的效率,但同时也降低了insertupdate的效率,因为insertupdate时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个。
  • 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
  • 尽可能的使用varchar/nvarchar代替char/nchar,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些
  • 任何地方都不要使用select * from t,用具体的字段列表代替*****,不要返回用不到的任何字段。
  • 尽量使用表变量来代替临时表。 如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
  • 避免频繁创建和删除临时表,以减少系统表资源的消耗。 临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件, 最好使用导出表。
  • 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先truncate table,然后drop table,这样可以避免系统表的较长时间锁定。
  • 尽量避免使用游标,因为游标的效率较差。 如果游标操作的数据超过1万行,那么就应该考虑改写。
  • 在所有的存储过程和触发器的开始处设置SET NOCOUNT ON,在结束时设置SET NOCOUNT OFF。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC消息。
  • 尽量避免大事务操作,提高系统并发能力。
  • 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

行锁和表锁、乐观锁和悲观锁

锁的分类

  • 从对数据库操作的类型分:读锁和写锁

    • 读锁(共享锁): 针对同一张表数据,多个读操作可以同时进行而不会互相影响。
    • 写锁(排他锁): 当前写操作没有完成前,它会阻断其它写锁和读锁。
  • 从对数据操作的粒度分:表锁和行锁

    • 表锁: 开销小,加锁快,不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发效率低。
    • 行锁: 开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率最低,并发效率高。

    注意:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

锁的阻塞

  • 读锁会阻塞写,但是不会堵塞读。
  • 写锁则会把读和写都堵塞
  • 当一个线程获得对一个表的写锁后,只有持有锁线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。

加锁

  • 加读锁
    • 当前加读锁的session
      • 可以查询当前表的记录
      • 不能查询其它没有锁定的表
      • 向当前表更新或插入锁定表都会提示错误
    • 其它session
      • 可以查询该表的记录
      • 可以查询或更新未锁定的表
      • 插入或更新锁定表会进入阻塞,一直等待到获得锁。
  • 加写锁
    • 当前加写锁的session
      • 当前session对锁定表的查询、更新、插入操作都可以执行;
    • 其它session
      • 其它session对该表的查询、更新和插入操作都会阻塞,一直等待到获得锁。

乐观锁和悲观锁

  • 悲观锁(PCC): 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
    • 悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。
  • 乐观锁(OCC): 假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
    • 乐观锁不能解决脏读,加锁的时间要比悲观锁短(只是在执行sql时加了基本的锁保证隔离性级别),乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。
  • 乐观锁更适合解决冲突概率极小的情况;而悲观锁则适合解决并发竞争激烈的情况,尽量用行锁,缩小加锁粒度,以提高并发处理能力,即便加行锁的时间比加表锁的要长。

SQL什么情况下不会使用索引

  • 查询谓词没有使用索引的主要边界,换句话说就是使用了select * from table,可能会导致不走索引。
  • 单键值的b树索引列上存在null值,导致COUNT(*)不能走索引。
  • 索引列上有函数运算,导致不走索引。
  • 隐式转换导致不走索引。
  • 表的数据库小或者需要选择大部分数据,不走索引。
  • !=或者<>(不等于),可能导致不走索引,也可能走INDEX FAST FULL SCAN
  • 表字段的属性导致不走索引,字符型的索引列会导致优化器认为需要扫描索引大部分数据且聚簇因子很大,最终导致弃用索引扫描而改用全表扫描方式。
  • 建立组合索引,但查询谓词并未使用组合索引的第一列,此处有一个INDEX SKIP SCAN概念
  • like '%liu' 百分号在前。
  • not in ,not exist的使用

框架

Spring中常用的有哪些设计模式

  • 简单工厂:静态工厂方法(StaticFactory Method)模式,但不属于23GOF设计模式之一。简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
    • Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建要根据具体情况而定。
  • 工厂方法(Factory Method)模式: 通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
    • Spring框架中,应用程序将自己的工厂对象交给Spring管理。即Spring管理的就是工厂Bean
  • 单例模式(Singleton):
    • 当我们试图从Spring容器中取得某个类的实例时,默认情况下,Spring会采用单例模式进行创建。如果我不想使用默认的单例模式,只需将scope属性值设置为prototype(原型)就可以了。Spring框架对单例的支持是采用单例注册表的方式进行实现的。
  • 适配器(Adapter):
    • SpringAOP中,使用的Advice(通知)来增强被代理类的功能。Spring实现这一AOP功能的原理就是使用了代理模式(JDK动态代理,CGLib字节码生成技术代理)对类进行方法级别的切面增强。即,生成被代理类的代理类,并在代理类的方法前,设置拦截器,通过执行拦截器中的内容增强了代理方法的功能,实现面向切面编程。
  • 装饰者模式(Decorator):
    • spring中用到的装饰者模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。基本上都是动态地给一个对象添加一些额外的职责。
  • 代理模式(Proxy)
    • 为其他对象提供一种代理以控制对这个对象的访问。 从结构上来看和Decorator模式类似,Proxy是控制,更像是一种对功能的限制,而Decorator是增加职责。SpringProxy模式在aop中有体现,比如JdkDynamicAopProxyCglib2AopProxy
  • 观察者模式(Observer):
    • 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。SpringObserver模式常用的地方是listener的实现。如ApplicationListener
  • 策略模式(Strategy): 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
    • Spring在实例化对象的时候用到了Strategy模式。
  • 模板方法模式(Template Method): Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。Template Method模式一般是需要继承的。

Spring框架中IOC的原理是什么

  • IOC完成对象的创建和依赖的注入等等,最主要的是实现对象之间的解耦
    • 所谓控制反转,就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。那么必然需要创建一个容器,同时需要一种描述来让容器知道我们需要创建的对象与对象之间的关系。这个描述最具体表现就是我们可配置的文件。
      • 对象与对象之间的关系表示:可以选用xmlpropertiesyml文件等语义化配置文件表示。
    • 简单来说就是把复杂系统分解成相互作用合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
    • IOC理论提出的观点:借助于第三方实现具有依赖关系的对象之间的解耦。全部对象的控制权全部上缴给第三方IOC容器,所以IOC容器成了整个系统的关键核心,它起到了一种类似粘合剂的作用,把系统中的所有对象粘合在一起发挥作用。如果没有这个粘合剂,对象与对象之间便失去了联系。
  • DI:控制反转实质就是指获得依赖对象的过程被反转了,于是就有了一个更合适的名字:依赖注入。所谓依赖注入,就是IOC容器在运行期间,动态地将某种依赖关系注入到对象中。
  • 举例解析: 对象A依赖于对象B,当对象A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象AIOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
  • IOC好处
    • 解耦。相关的任何一方出现什么问题,都不会影响其它方的运行,这样程序或者说软件的可维护性比较好
    • 减轻开发人员的负担。 每个开发团队的成员都只需要关心实现自身的业务逻辑,完全不用去关心其它的人工作进展,因为你的任务跟别人没有任何关系,你的任务可以单独测试,你的任务也不用依赖于别人的组件,再也不用扯不清责任了。所以,在一个大中型项目中,团队成员分工明确、责任明晰,很容易将一个大的任务划分为细小的任务,开发效率和产品质量必将得到大幅度的提高。
      • 程序的复用性比较好。 我们可以把具有普遍性的常用组件独立出来,反复利用到项目中的其它部分,或者是其它项目,当然这也是面向对象的基本特征。显然,IOC不仅更好地贯彻了这个原则,提高了模块的可复用性。符合接口标准的实现,都可以插接到支持此标准的模块中。
    • 模块具有热插拔特性。IOC生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,这样,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。
  • IOC容器的技术剖析
    • IOC中最基本的技术就是反射(Reflection)编程。
    • IOC容器的工作模式就像是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程根据配置文件中给出的类名生成相应的对象。从实现上看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性
  • 注意问题
    • 由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
    • 由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
    • 需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
    • IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。

列举一个常用的消息中间件,如果消息要保序该如何实现

  • 一个重要的前提: 要保持多个消息之间的时间顺序,首先它们要有一个全局的时间顺序。因此,每个消息在被创建时,都将被赋予一个全局唯一的、单调递增的、连续的序列号(SerialNumber,SN)。可以通过一个全局计数器来实现这一点。通过比较两个消息的SN,确定其先后顺序。

  • rabbitMQ: 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实麻烦了一些;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。

更多的面试题,请点击:java面试题 02

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值