@TOC
语言特性
string,stringgbuffer,stringbuilder区别:
共同之处:
- 三者共同之处:都是final类,不允许被继承
不同之处:
- String:不可变字符串,初始化时可以赋空值,每次对String的操作都会生成一个新的String对象,效率较低且浪费大量内存空间。
- StringBuffer:可变字符串、效率低、线程安全;StringBuffer对象默认生成16个字节的缓冲容量,当字符串大小超过容量时,会自动增加容量。
- StringBuilder:可变字符序列、效率高、线程不安全;与StringBuffer一样都继承和实现了同样的接口和类,方法除了没使用synch修饰以外基本一致
深入阅读: String,StringBuffer与StringBuilder的区别??
== 和equals区别
对于理解==和equals的区别,我们首先需要了解Java中的基本数据类型与引用类型
基本数据类型: Byte,short,int,long,double,folat,boolean,char,这八种数据变量中直接存储值,无equals方法。
引用类型: 除以上基本类型之外的都是引用类型,像String类型属于引用类型,变量中存储的是引用地址,对应的地址中存储数据。
equals
- 只有引用类型含有此方法,比较的是两个引用类型中的引用地址的值是否相同。
==
- 对于基本类型:比较的就是基本类型的值是否相同。
- 对于引用类型:比较的就是引用地址是否相同。
深入阅读: 浅谈Java中equals()和==的区别
ArrayList和LinkedList的区别
1.ArrayList是实现了基于动态数组的数据结构,采用下表索引对数据直接进行访问。每次对于新增或者删除非末尾的数据,都需要将后方的数据进行前移或后移操作。
LinkedList基于双链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
深入阅读 : ArrayList和linkedList的区别
HashTable和HashMap区别
- HashTable:线程安全,不允许键值为null,效率稍低。继承自Dictonary类。
- HashMap:线程不安全,允许键值为null,效率稍高。实现了Serializable接口支持序列化,实现了Cloneable接口能被克隆。继承自abstractMap。
深入阅读: HashMap和Hashtable的区别
并发与多线程
JAVA多线程实现的四种方式
- 继承Thread类
- 实现Runnable接口
- 使用ExecutorService、Callable、Future实现有返回结果的多线程。
- 通过线程池创建线程
前两种无返回值,通过重写run方法实现,run方式的返回值是void,所以没有办法返回结果。
后两种有返回值,通过Callable接口实现call方法,这个方法的返回值是Object,返回的结果可以放在Object对象中。
Thread 类中的start() 和 run() 方法有什么区别?
- 通过start()方法来启动一个线程,此时线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止,所以通过start()方法可以达到多线程的目的。
- 如果直接调用线程类的run()方法,会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,start()方法呢能够异步的调用run()方法,但是直接调用run()方法缺是同步的,无法达到多线程的目的。
volatile 和synchronized ?
volatile
- volatile不会造成线程的阻塞,是Java提供的一种轻量级锁,相比使用synchronized所带来的庞大开销,volatile效率更高。
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取
- volatile仅能实现变量的修改可见性,不能保证原子性;
- volatile仅能使用在变量级别
synchronized
- 通常称为重量级锁,锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- synchronized则可以使用在变量、方法、和类级别的
- 而synchronized则可以保证变量的修改可见性和原子性
深入阅读: Java并发编程:volatile关键字解析
什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
而且一个进程能创建的线程数有限。为了避免这些问题,可以采用线程池的方式多多线程进行管理。并对线程进行复用,而不是执行完一个任务就进行销毁。
创建线程池一般是分为自动创建和手动创建,自动创建线程池的几种方式都封装在Executors工具类中,他是根据应用场景封装好了一些创建参数,但是实际使用中不推荐采用自动创建的方式,推荐使用ThreadPoolExecutor手动创建线程池:通过手动创建的方式确保使用者更加明确线程池的运行规则,避免线程池的资源耗尽。
ThreadPoolExecutor类是线程池中最核心的一个类,有7个构造参数
- corePoolSize:核心线程池的大小。指的是线程池中常驻的线程数,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。也可以调用prestartCoreThread()方法预创建线程。
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。
但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有七种属性,天、小时、分钟、秒、毫秒、微妙、纳秒
- workQueue:一个阻塞队列,用来存储等待执行的任务。
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,
在ThreadPoolExecutor类中有几个非常重要的方法:
- execute:通过这个方法可以向线程池提交一个任务,交由线程池去执行。
- submit:也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,因为它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
- shutdown()和shutdownNow()是用来关闭线程池的。shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。
- 而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
深入理解: Java并发编程:线程池的使用
JVM内存模型
JVM 运行时内存共分为程序计数器,Java虚拟机栈,本地方法栈,堆,方法区五个部分。
其中 Java虚拟机栈、本地方法栈、程序计数器是线程私有的。
1、程序计数器(线程私有): 一个指向方法区中的方法字节码指针,由执行引擎读取下一个将要执行的指令代码,是一个非常小的内存空间,几乎可以忽略不记,是唯一一个在Java虚拟机规范中没有OutOfMemoryError情况的区域。
jvm的多线程是通过线程轮流切换并分配处理器执行时间来实现的,在一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此每条线程都需要有一个独立的程序计数器标记当前处理的位置,各条线程之间计数器互不影响,是“线程私有”的内存。
如果正在执行的是Native方法,这个计数器则为空(undefined)。
2、Java虚拟机栈(线程私有): 生命周期和线程相同,在线程创建时创建,线程结束栈内存也就释放,所以对于栈来说不存在垃圾回收问题。基本类型的变量和对象的引用变量都是在当前内存中分配。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。方法从调用到完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表所需的内存空间在编译期完成分配,在方法运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机允许的深度,将会抛出Stack Overflow栈溢出异常;如果动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3、本地方法栈(线程私有): 本地方法栈与虚拟机栈作用类似,虚拟机栈执行Java方法,本地方法栈执行native方法服务。native方法指的是在java中调用的其他编程语言的方法,融合不同的编程语言为Java所用
与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。
4、Java堆(线程共享): JVM中最大的区域,几乎所有的实例对象和数据都是存在这个区域,被所有线程共享的,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。也是 gc 主要的回收区。
堆内存分为3大部分:新生代、老年代和永久代,其中新生代又进一步划分为Eden、S0、S1(Survivor)三个区,在JDK1.8之后,堆的永久代取消了,由元空间取代。从内存回收角度看,现在收集器基本一般分代收集算法,可以更好的回收内存并且更快的分配内存。
我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,也可以手动设置晋升年龄。
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代
如果在堆中没有内存用来分配实例,并且堆也无法扩展,将会抛出OutOfMemoryError异常。
5、方法区(线程共享): 和堆一样所有线程共享,主要用于存储已经加载的类信息、常量、静态变量、和编译后的代码等数据。包括运行时常量池。
运行时常量池:作为方法区的一部分。用于存放编译期生成的各种字面量和符号引用,动态性,不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。
简述GC内存回收
大白话说就是垃圾回收机制,内存空间是有限的,你创建的每个对象和变量都会占据内存,java提供自动清除无用对象的功能,清除对象将内存释放出来,这就是GC要做的事。
堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区。
垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC
- Minor GC(年轻代GC):
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。
- Full GC(老年代GC):
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。
判断一个对象是否应该被回收,主要是看其是否还有引用。方法包括引用计数法以及可达性分析。
- 引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
- 可达性分析:
可达性分析的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。
常见的回收算法有标记-清除算法,复制算法和标记整理算法。
类加载顺序
类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
加载分为五个步骤:
加载 -> 验证 -> 准备 -> 解析 -> 初始化。
加载: 把class字节码文件从各个来源通过类加载器装载入内存中,类加载器:一般包括引导类加载器,扩展类加载器,系统类加载器,以及用户的自定义类加载器。
验证: 确保加载进来的 calss 文件包含的额信息符合 Java 虚拟机的要求;
准备: 为类变量分配内存,设置类变量的初始值;
解析: 将常量池内的符号引用转为直接引用;
初始化: 初始化类变量和静态代码块。
new一个对象的具体过程
双亲委派机制
在当前类加载器接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;如果父加载器无法完成此加载任务时,才会自己去加载。
其中类加载器分为以下四种:
- 引导类加载器:负责加载Java基础类
- 扩展类加载器:它负责加载JRE的扩展目录
- 系统类加载器:负责加载CLASSPATH变量所指定的目录下所有jar和class。
- 自定义类加载器。由Java实现。用户可以自定义类加载器,加载指定路径下的class文件。
优点:
- 避免重复加载。当双亲委派的过程中,如果父亲已经加载了该类,则当前类就不需要重新加载。
- 保证核心类不被篡改。通过委托方式,如果发现该类已经加载过之后,会直接返回已加载过的该类。确保核心类不会被篡改。即使类被篡改,因为不同的加载器加载同一个.class也不是同一个对象。这样也能保证核心类的执行安全。
还有哪些想看的面试题,读者可以在评论区补充,博主会在一天内进行更新。