Java面试题八股文(进阶)--完结

TOC](Java面试题持续整理ing)

一、性能优化面试专题

tomcat性能优化

1、怎么给你的tomcat调优

1、JVM的调优参数:-Xms表示初始化堆的大小,-Xmx表示JVM堆的最大值。这两个值的大小一般是根据需要设置的,当程序需要的内存超出堆内存的最大值的时候就会提示内存溢出(OOM)。并且导致服务崩溃,应用宕机。因此一般建议堆的最大值设置为可用内存的最大值的80%,在tomcat中的catalina.bat 里,设置 JAVA_OPTS=’-Xms256m-Xmx512m’,表示初始化内存为256MB,可以使用的最大内存为512MB。
2、禁用DNS查询:当web应用程序像要记录客户端的信息时候,他会记录客户端的ip地址或者通过域名服务器查找机器名转化为ip地址。DNS查询需要占用网络,并且包括可能从很远的服务器或者不起作用的服务器上去获取对应的ip的过程,这样会消耗一定的时间。为了消除DNS查询对应能的影响我们可以关闭DNS查询,方式是修改tomcat中的server.xml文件中的enableLookups这个参数的值:

Tomcat4
<Connector
className="org.apache.coyote.tomcat4.CoyoteConnector"port="80"
minProcessors="5"maxProcessors="75"enableLookups="false"redire
ctPort="8443"
acceptCount="100"debug="0"connectionTimeout="20000"
useURIValidationHack="false"disableUploadTimeout="true"/>
Tomcat5
<Connectorport="80"maxThreads="150"minSpareThreads="25"
maxSpareThreads="75"enableLookups="false"redirectPort="8443"
acceptCount="100"debug="0"connectionTimeout="20000"
disableUploadTimeout="true"/>

3、调整线程数量:通过应用程序的连接器(Connector)进行性能控制的参数是创建处理请求的线程数。Tomcat是使用的线程池技术加速响应速度来处理请求的。在java中线程是程序运行时的路径,是在一个程序中与其他控制线程无关的、能够独立运行的代码段,他们共享相同的地址空间。多线程帮助程序员写出最大利用cpu的高效程序,使空闲时间保持最低,从而接收更多的请求。

Tomcat4 中可以通过修改minProcessors和maxProcessors的值来控制线程数。这些值在安装后就已经设定为默认的并且是足够使用的了。但是随着站点的扩容而改大这些值。minProcessors服务器启动时创建的处理请求的线程数应该足够处理的一个小量的负载。也就是说,如果一天内每秒仅发生5次单击事件,并且每个请求处理任务需要1秒钟,那么预先设置线程数为5就足够了。但是在你的站点访问量较大的时候就需要设置更大的线程数,指定为参数maxProcessors的值。maxProcessors的值也是有上限的。应该防止流量不可控制(或者是恶意的服务攻击),从而导致超出了虚拟机使用内存的大小。如果要加大并发连接数,应该同事加大这两个参数。Web Server允许的最大连接数还受制于操作系统的内核参数设置。通常windows系统是2000左右,Linux系统是1000左右。
Tomcat5 中对这些参数进行了调整,请看下面的属性:
maxThreads Tomcat :使用线程来处理接收每个请求。这个值表示Tomcat可创建的最大的线程数。
acceptCount :指定当所有可以使用的处理请求的线程数都被使用时,可以放到队列中的请请求数,超过这个数的请求将不会处理。
connnection Timeout :网络连接超时:单位是ms。设置为0表示永不超时,这样设置是不好的有很大隐患。通常我们会把它设置为30000毫秒。
minSpareThreadsTomcat :初始化时创建的线程数。
maxSpareThreads :一旦创建的线程超过这个数值,tomcat就会关闭不再需要的socket线程。
最好的方式是多设置几次并进行测试,观察响应时间和内存使用情况。在不同的机器、操作系统或虚拟机组合的情况下可能会不同,而且并不是所有人的web站点的流量都是一样的,因此没有一刀切的方案来确定线程数的值!

2、如何加大tomcat连接数

在 tomcat配置文件 server.xml 中的 配置中,和连接数相关的参数有:

minProcessors:最小的空闲链接线程数,用于提高系统处理性能,默认值为10;
maxProcessors:最大的线程数,即:并发处理的最大请求数,默认是75;
acceptCount:允许最大的连接数,应该大于maxProcessors,默认为100
enableLookups: 是否反查域名,取值为true或者false。为了提高处理性能,应该设置为false;
connectionTimeout:网络链接超时时间,单位:毫秒。设置为0表示用不超时,通常设置为30000毫秒。其中和最大连接数相关的参数为maxProcessors和acceptCount,如果要加大并发连接数,应同时加大这两个参数。
tomcat5 中的配置示例:

<Connectorport="8080"
maxThreads="150"minSpareThreads="25"maxSpareThreads="75"
enableLookups="false"redirectPort="8443"acceptCount="100"
debug="0"connectionTimeout="20000"
disableUploadTimeout="true"/>
以此类推

3、怎样加大tomcat的内存

首先检查程序有没有陷入死循环,这个问题主要还是由于 java.lang.OutOfMemoryError:Java heap space(OOM)引起的。第一次出现这样的问题以后,引发了其他的问题。可能是堆栈设置的太小的原因。
根据网上的答案大致有两种解决方法:
1、设置环境变量
手动设置Heap size,修改TOMCAT_HOME/bin/catalina.sh set JAVA_OPTS=-Xms32m-Xmx512m,可以根据自己机器内存进行更改。
2、java-Xms32m-Xmx800m className
就是在执行java类文件时候加上这个参数,其中className是需要执行的全类名。而且执行的速度比没有设置的时候快很多。如果在测试的时候可能会用idea,就在启动的参数设置VM arguments中输入-Xms32m-Xmx800m这个参数就可以了。
一、java.lang.OutOfMemoryError:PermGen space PermGen space 的全称是 Permanent Generation space,是指内存的永久保存区域,这块内存中主要是被JVM存放CLass和Meta信息的,Class被Loader时就会放到PermGen space中,他和存放类实例(Instance)的Heap不同,GC(Garbage Collection)不会在主程序运行期间对PermGen space进行清理,所以如果你的应用中有很多class就很可能出现 PermGen space 错误。这种错误常见在web服务器对JSP进行precompile(预编译)的时候,如果你的web app下使用了大量的第三方的jar,其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。解决方法: 手动设置MaxPermSize大小,修改TOMCAT_HOME/bin/catalina.sh,在“echo"Using CATALINA_BASE:$CATALINA_BASE"” 上面加一行:JAVA_OPTS="-server-XX:PermSize=64M-XX:MaxPermSize=128m 建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以达到减少jar文档重复占用内存的目的。
二、 java.lang.OutOfMemoryError:Java heap space Heap size :JVM堆的设置是指java程序在运行过程中jvm可以调配使用的内存空间设置。JVM启动时会自动设置堆内存的大小(Heap Size的值),其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用jvm提供的 -Xmn-Xms-Xmx等选项可进行设置。Heap size的大小是 Young Generation和TenuredGeneraion只和
提示:在JVM中如果98%的时间是用于GC且可用的Heap Size不足2%的时候将抛出此异常信息。HeapSize 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx设置为相同,而-Xmn(年轻代大小)设置为-Xmx的1/4。

4、tomcat如何禁止列目录下的文件

在{tomcat_home}/conf/web.xml中,把listing参数设置为false即可,如下:

<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>

5、tomcat有几种部署方式

tomct中四种部署项目的方法
第一种:在tomcat中conf目录中,在server.xml中的host节点添加

<Context path="/hello"
docBase="D:/eclipse3.2.2/forwebtoolsworkspacehello/WebRoot"deb
ug="0"
privileged="true">
</Context>

至于Context节点属性,可以查阅 相关文档

第二种:将web项目文件拷贝到webapps目录中。
第三种:在conf目录中新建Catalina(注意大小写)\localhost目录,在该目录下新建一个xml文件,名字随意,只要不重复就行。该xml文件内容为:

<Context path="/hello"docBase="D:eclipse3.2.2forwebtoolsworksp
aceheloWebRoot"
debug="0"privileged="true">
</Context>

这种方法有个优点,可以定义别名。服务器端运行的项目名称为path,外部访问的URL则使用xml的文件名。这个方法很方便的隐藏了项目的名称,对一些项目名称被固定不能更换,但外部访问时又想换个路径的情况非常有效。第二、三种方法还有个优点,可以定义一些个性化配置,比如数据源配置等。

第四种:可以利用tomcat在线后台管理器,一般tomcat都打开了,直接上传war包就可以了。

6、tomcat的优化经验

Tomcat作为web服务器,他的处理性直接关系到用户的体验,下面是常见的tomcat的优化常见措施:

去掉对web.xml的监视,把jsp提前编译成servlet。有富余物理内存的情况,加大tomcat使用jvm的内存。

提高服务器资源。服务器所能提供cpu,内存,硬盘的性能对处理能力有决定性影响。

对于高并发情况下会有大量的计算,那么cpu的速度会直接影响处理的速度。

内存在大量数据处理的情况下,将会有较大的内存容量需求,可以用-Xmx,-Xms-,XX:MaxPermSize等参数对内存不同的模块进行划分。我们之前就遇到过内存不足的情况,导致虚拟机一直处于full gc,从而导致处理能力严重下降。

硬盘主要的问题就是读写性能,当大量的文件进行读写的时候,磁盘极容易成为性能的瓶颈。最好的办法是利用下面提到的缓存。

利用缓存和压缩。对于静态页面最好的办法是能够缓存起来,这样就不必每次从磁盘上读取。这里我们采用Nginx作为缓存服务器,将图片,css,js文件都进行了缓存,有效减少了后端tomcat的访问。另外,为了加快网络的传输速度,开启gzip压缩也是必不可少的。但是考虑到tomcat已经需要处理很多东西了,所以把这个处理工作就交给前端Nginx来完成。除了文本可以用gzip压缩,其实很多图片也可以用图像处理工具预先进行压缩,找到一个平衡点可以让画质损失很小而文件可以减少很多。曾经我就见过一个图片从300多k压缩到几十kb,自己几乎看不出来区别。(你是一个认真的人,只要坚持努力将来一定会成功的!)

采用集群。单个服务器的性能总是有限的,最好的办法自然是横向扩展。我们还是采用Nginx来作为请求分流的服务器,后端多个tomcat共享session来协同工作。

优化tomcat参数。这里我用tomcat7的参数配置为例,需要修改server.xml,主要优化连接配置,关闭客户端dns查询。
<Connector port="8080" 
	protocol="org.apache.coyote.http11.Http11NioProtocol"
	connectionTimeout="20000"
	redirectPort="8443"
	maxThreads="500"
	minSpareThreads="20"
	acceptCount="100"
	disableUploadTimeout="true"
	enableLookups="false"
	URIEncoding="UTF-8"/>

二、JVM性能优化专题

1、Java类加载过程

Java类加载需要经历一下7个过程:

1 加载 加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:

  • 通过一个类的全限定名获取该类的二进制流。
  • 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
  • 在内存中生成该类的Class对象,作为该类的数据访问入口。

2 验证 :验证的目的是为了确保[Clas?文件的字节流中的信息不回危害到 虚拟机•在该阶段主要完成以下四钟验证:
•文件格式验证:验证字节流是否符合[Classj文件的规范,如主次版本号 是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
•元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类, 是否集成了不被继承的类等。
•字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和 控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
•符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解 析动作能正确执行。
3.准备: 准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都 将在方法区中进行分配。
准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。例如:public static int value=123;//在准备阶段value初始值为0。在初始化阶
段才会变为123。
4 解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初
始化动作完成之前,也有可能在初始化之后。
5 初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应
用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和
控制。到了初始化阶段,才真正开始执行类中的定义的java程序代码。
6 使用
7 卸载

2、java内存分配

•寄存器:无法控制。
•静态域:static定义的静态成员。
•常量池:编译时被确定并保存在.(class;文件中的| (final) |常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
•非RAM[存储:硬盘等永久存储空间。
•堆内存:new创建的对彖和数组,由Java虚拟机自动垃圾回收器管理,存取速度慢。
•栈内存:基本类型的变量和对象的引用变・(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。
Java堆的结构是什么样子的?什么是堆中的永久代| (Perm Gerispace) ?
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM「启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

3、描述一下JVM加载Class文件的原理机制?

JAVA语言是一种具有动态性的解释型语言,类(Class)只有被加载到JVM后才能运行。当运行指定程序时,JVM会将编译生成的点class文件按照需求和一定的规则加载到内存中,并组织成为一个完整的Java应用程序。这个加载过程是由类加载器完成,具体来说,就是由ClassLoader和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使用new等方式创建对象时,会隐式地调用类的加载器把对应的类加载到JV刚中。显示加载指的是通过直接调用〔class.fortteme"方法来把所需的类加载到JVM中。任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到JVM中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外在Java语言中,每个类或接口都对应一个[.class]文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。在Java语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到JVM中,至于其他类,则在需要的时候才加载。

类加载的主要步骤:
•装载。根据查找路径找到相应的class文件,然后导入。
•链接。链接又可分为3个小步:
•检查,检查待加载的class文件的正确性。
•准备,给类中的静态变量分配存储空间。
•解析,将符号引用转换为直接引用(这一步可选)
•初始化。对静态变量和静态代码块执行初始化工作。

4、GC是什么?为什么要有GC?

GC是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

5、简述Java垃圾回收机制

在Java中,程序员是不需要显示(像C++那样用代码实现)的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

6、如何判断一个对象是否存活?

判断一个对象是否存活有两种方法:

1、引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对彖时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对彖A,那么此时A、B对彖的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

2 、可达性算法(引用链法) 该算法的思想是:从一个被称为GC Roots的对象幵始向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明此对象不可用。在Java中可以作为GC Roots的对象有以下几种:
•虚拟机栈中引用的对象
•方法区类静态属性引用的对象
•方法区常量池引用的对象
•本地方法栈JNI引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达GCRoot时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记.如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第 一次标记并且逬行一次筛选,筛选的条件是是否有必要执行。finalize()方法。当对象没有覆盖finalize方法或者已被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行:finalize方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机触发一个[Finaliz()]线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finaliz()执行缓慢或者发主了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。

7、垃圾回收的优点和原理。并考虑2种回收机制。

Java语言中一个显著的特点就是引入了垃圾回收机制,使C++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有”作用域“。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。

8、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为“不可达”时,GC就有责任回收这些内存空间。马上回收内存。程序员可以手动执行|Systen
.gc(),通知GC运行,但是Java语言规范并不保证GC 一定会执行。

9、深拷贝和浅拷贝都是什么

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。简单来讲就是复制、克隆。

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

10 、简述Java内存分配与回收策略以及Minor GC和Major GC。

对象优先在堆的Eden区分配

  • 大对象直接进入老年代

  • 长期存活的对象将直接进入老年代

    当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生Gc的频率较高,回收速度比较快;Full GC/Major GC发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

11、JVM的永久代中会发生垃圾回收么?

可以说垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
可以这么理解,永久代是永久的,回收了还怎么永久。

注:Java 8中已经移除了永久代,新加了一个叫做元数据区的native内存区。元空间并不在虚拟机中,而是使用本地内存

12、Java中垃圾收集的方法有哪些?

标记-清除法:
这是垃圾收集算法中最基础的,根据名字就可以知道,它的 思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但
是会有两个主要问题:

  1. 效率不高,标记和清除的效率都很低;
  2. 会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

复制算法:
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)
标记-整理法:
该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
分代收集法:
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保。

13、什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:
•启动类加载器(BootstrapCIassLoader)用来加载Java核心类库,无法被Java程序直接引用。
•扩展类加载器(extensions class loader)陀用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载Java类。
•系统类加载器(system class loader):它根据Java应用的类路径(C3SSRATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoaderO 来获取它。
•用户自定义类加载器,通过继承javadang.ClassLoader类的方式实现。

14、类加载器双亲委派模型机制?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
在这里插入图片描述
这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
Bootstrap 类加载器是用 C++ 实现的,是虚拟机自身的一部分,如果获取它的对象,将会返回 null;扩展类加载器和应用类加载器是独立于虚拟机外部,为 Java 语言实现的,均继承自抽象类 java.lang.ClassLoader ,开发者可直接使用这两个类加载器。
Application 类加载器对象可以由 ClassLoader.getSystemClassLoader() 方法的返回,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型对于保证 Java 程序的稳定运作很重要,例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。

二、微服务架构面试专题

1、SpringBoot面试整理

2、SpringCloud面试整理

三、并发编程面试专题

1、你用过Synchronized吗?知道Synchronized原理吗?

这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或者没接触过Synchronized。
Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexi俩个字节码指令。这两个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexitf旨令时将锁计数器-1
;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。

2、你获取到的“锁”,它到底是什么?如何确定对象的锁?

“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以修饰不同的对象,因此,对应的对象锁可以这么确定。

  1. 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、Synchronized(this)等,说明加解锁对象为该对象。
  2. 如果没有明确指定:若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象;若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。

3、什么是锁的可重入性,为什么说Synchronized是可重入锁呢?

可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。比如下面的伪代码:一个类中的同步方法调用另一个同步方法,假如Synchronized不支持重入,进入method2方法时当前线程获得锁,
method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。对Synchronized来说,可重入性是显而易见的。刚才提到在执行
monitor enter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1,其实本质上就通过这种方式实现了可重入性。

4、JVM对Java中的原生锁做了哪些优化?

在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。
一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:
•偏向锁(Biased Locking)
•轻量级锁
•重量级锁
这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。
•当没有竞争出现时,默认会使用偏向锁。
JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所叹并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
•如果有另一线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切换到轻量级锁实现。
•轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

5、为什么说Synchronized是非公平的锁呢?

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

6、什么是锁消除和锁粗化?

•锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。
•锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化就是增大锁的作用域。

7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS? CAS有什么特性?

Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。
先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是CAS(CompareandSwap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令,JDK中提供了Unsafe类执行这些操作。

8、乐观锁一定就是好的吗?

乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

  1. 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小
  2. 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,给CPU带来很大的开销。
  3. ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了 A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号+1。

9、跟Synchronized相比,可重入锁ReentrantLock的实现原理有什么不同呢?

其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

10、请谈谈AQS框架是怎么回事儿?

AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架,各种Lock包中的锁(常用的有ReentrantLock. ReadWriteLock),以及其他如 Semaphorex、CountDownLatch,甚至是早期的 Futurelask 等,都是基于AQS来构建。

  1. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调用lock方法时,如果state=O,说明没有任何线程占有共享资源的锁,可以获得锁并将state";如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
  2. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
  3. Node类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。
  4. Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如 ReentranLock)。
  5. AQS通过内部类Conditionobject构建等待队列(可有多个),当Condition调用wait。方法后,线程将会加入等待队列中,而当Condition调用signalO方法后,线程将从等待队列转移动同步队列中进
    行锁竞争。
  6. AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的互相移动。

11、对比Synchronized和ReentrantLock的异同

ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度,ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一样使用), 甚至实现Synchronized没有的高级功能,如:

  1. ReentrantLock等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
  2. ReentrantLock带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
  3. ReentrantLock可以判断是否有线程在排队等待获取锁。
  4. ReentrantLock可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
  5. ReentrantLock可以实现公平锁。从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLockO放到finally{}中。

从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。但是在Java 6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

12、ReentrantLock是怎么实现可重入性的?

ReentrantLock内部自定义了同步器Sync(Sync既实现了AQS,又实现了AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。

13、除了ReentrantLock,你还接触过JUC中哪些并发工具?

通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了 Java并发的各种基础工具类,具体主要包括几个方面:

  • 提供了 CountDownLatchs CyclicBarrier.Semaphore等,比Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。
  • 提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayUst等各种线程安全的容器。
  • 提供了 ArrayBlockingQueuesSynchorousQueue 或针对特定场景的PriorityBlockingQueue等,各种并发队列实现。
  • 强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。

14、请谈谈ReadWriteLock和StampedLock(1.8)的区别有哪些?

虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势:
在这里插入图片描述
读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
在这里插入图片描述

15、如何让java的线程彼此同步?你了解过哪些同步器?请分别介绍下。

JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和Semaphore,通过它们可以方便地实现很多线程之间协作的功能。
CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看几个场景:

  • 跑步比赛,裁判需要等到所有的运动员(“其他线程”)都跑到终点(达到目标),才能去算排名和颁奖。
  • 模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们能同时并发,而不是一个一个的去执行。用法:CountDownLatch构造方法指明计数数量,被等待线程调用countDown将计数器减1,等待线程使用await进行线程等待。一个简单的例子:
  • 在这里插入图片描述

CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使用。CyclicBarrier的典型应用场景是用来等待并发线程结束。CyclicBarrier的主要方法是awaitO, await。每被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。在这之后,如果再次调用awaitO,计数就又会变成N-1,新一轮重新幵始,这便是Cyclic的含义所在。CyclicBarrier.awaitO带有返回值,用来表
示当前线程是第几个到达这个Barrier的线程。举例说明如下:
在这里插入图片描述
Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是通过acquireO获取一个许可,如果没有就等待,而release()释放一个许可。
在这里插入图片描述
如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?

它们的行为有一定相似度,区别主要在于:

  • CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这种限制,可以重用。
  • CountDownLatch的基本操作组合是countDown/await,调用await的线程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程里countDown,只要次数足够即可。
  • CyclicBarrier的基本操作组合就是await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。

CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后,自己再去做某个事(通过CyclicBarrier的第二个构造方法publicCyclicBarrier(int
parties, Runnable
barrierAction),在新线程里做事可以达到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有的都达到某个状态,然后这N个线程再继续执行各自后续(通过CountDownLatch在某些场合也能完成类似的效果)。

17、Java中的线程池是如何实现的?

  • 在Java中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类Worker,它基于AQS实现,存放在线程池的HashSetworkers成员变量中。
  • 而需要执行的任务则存放在成员变量workQueue(BlockingQueueworkQueue 冲。这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执行的任务,放在Workers中进行处理。
  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值