Java面试题 java高级

八:java高级

1:代码优化

(1)代码优化的目标是:
1 、减小代码的体积
2 、提高代码运行的效率
(2)代码结构层次的优化(目的:更加方便代码的维护–可维护性,可读性)
1.代码注释(代码规范)
2.工具类的封装(方便代码的维护,使代码结构更加清晰不臃肿,保证团队里代码质量一致性)
3.公共部分的提取
(3)代码性能的优化(目的:使程序的性能最优化)
(1)尽量指定类、方法的 final 修饰符
(2)尽量重用对象,比如通常使用stringbuffer替代string来进行字符串的拼接。
(3)尽可能使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
(4)及时关闭流
Java 编程过程中,进行数据库连接、 I/O 流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。
(5)尽量减少对变量的重复计算
明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:
for (int i = 0; i < list.size(); i++)
{…}
建议替换为:
for (int i = 0, length = list.size(); i < length; i++)
{…}
这样,在 list.size() 很大的时候,就减少了很多的消耗
(6)尽量采用懒加载的策略,即在需要的时候才创建。
(7)慎用异常。——异常对性能不利。只要有异常被抛出, Java 虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
(8)不要在循环中使用 try…catch… ,应该把其放在最外层
(9)如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度比如 ArrayList 、 LinkedLlist 、 StringBuilder 、 StringBuffer 、 HashMap 、 HashSet 等等
(10)当复制大量数据时,使用 System.arraycopy() 命令
(11)循环体内不要不断创建对象,可以在循环体外创建空对象——对象引用,在循环体每次new 对象的时候,对象引用指向的是不同的对象,这时候内存中只有一份,大大节省内存空间。
(12)基于效率和类型检查的考虑,应该尽可能使用 array ,无法确定数组大小时才使用 ArrayList

2:Jvm机制

https://blog.csdn.net/u011546655/article/details/52175550
(1)JVM的基础概念
JVM的中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。
JVM也充当着一个翻译官的角色,我们编写出的Java程序,是不能够被操作系统所直接识别的,这时候JVM的作用就体现出来了,它负责把我们的程序翻译给系统“听”,告诉它我们的程序需要做什么操作。
我们都知道Java的程序需要经过编译后,产生.Class文件,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。
(2)JVM的生命周期
JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。
一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
权限足够的话,可以调用exit()方法终止程序。
(3)JVM结构体系
Java面试题 java高级_第1张图片
(4)JVM内存空间
JVM内存空间包含:方法区、java堆、java栈、本地方法栈。
1.方法区是各个线程共享的区域,存放类信息、常量、静态变量。
2.java堆也是线程共享的区域,我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。
3.java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈,这里面有很多细节,我们以后再讲。如果java栈空间不足了,程序会抛出StackOverflowError异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。
4.本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。
5.PC寄存器,说到这里我们的类已经加载了,实例对象、方法、静态变量都去了自己改去的地方,那么问题来了,程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是PC寄存器在管,它的作用就是控制程序指令的执行顺序。
6.执行引擎当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。

(5)类加载子系统
类加载子系统也可以称之为类加载器,JVM默认提供三个类加载器:
1、BootStrap ClassLoader :称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等。
2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。
类加载器子系统不仅仅负责定位并加载类文件,它还严格按照以下步骤做了很多事情:
1、加载:寻找并导入Class文件的二进制信息
2、连接:进行验证、准备和解析
1)验证:确保导入类型的正确性
2)准备:为类型分配内存并初始化为默认值
3)解析:将字符引用解析为直接引用
3、初始化:调用Java代码,初始化类变量为指定初始值

(6)方法区(Method Area)
在JVM中,类型信息和类静态变量都保存在方法区中,类型信息是由类加载器在类加载的过程中从类文件中提取出来的信息。
需要注意的一点是,常量池也存放于方法区中。
程序中所有的线程共享一个方法区,所以访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待。
在程序运行时,方法区的大小是可以改变的,程序在运行时可以扩展。
(7)类型信息包括什么?
1、类型的全名(The fully qualified name of the type)
2、类型的父类型全名(除非没有父类型,或者父类型是java.lang.Object)(The fully qualified name of the typeís direct superclass)
3、该类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
4、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
5、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)
6、类型的字段信息(Field information)
7、类型的方法信息(Method information)
8、所有静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)
9、一个指向类加载器的引用(A reference to class ClassLoader)
10、一个指向Class类的引用(A reference to class Class)
11、基本类型的常量池(The constant pool for the type)
(8)方法列表(Method Tables)
为了更高效的访问所有保存在方法区中的数据,在方法区中,除了保存上边的这些类型信息之外,还有一个为了加快存取速度而设计的数据结构:方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例方法的引用,保存那些父类中调用的方法。

(9)Java堆(JVM堆、Heap)
当Java创建一个类的实例对象或者数组时,都在堆中为新的对象分配内存。
虚拟机中只有一个堆,程序中所有的线程都共享它。
堆占用的内存空间是最多的。
堆的存取类型为管道类型,先进先出。
在程序运行中,可以动态的分配堆的内存大小。
堆的内存资源回收是交给JVM GC进行管理的。
(10)Java栈(JVM栈、Stack)
在Java栈中只保存基础数据类型和自定义对象的引用,注意只是对象的引用而不是对象本身哦,对象是保存在堆区中的。
拓展知识:像String、Integer、Byte、Short、Long、Character、Boolean这六个属于包装类型,它们是存放于堆中的。
栈的存取类型为类似于水杯,先进后出。
栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
每个线程都会建立一个操作栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,每个栈帧包含了三部分:
局部变量区(方法内基本类型变量、变量对象指针)
操作数栈区(存放方法执行过程中产生的中间结果)
运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)

(11)本地方法栈
本地方法栈的功能和JVM栈非常类似,用于存储本地方法的局部变量表,本地方法的操作数栈等信息。
栈的存取类型为类似于水杯,先进后出。
栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用。
本地方法都不是使用Java语言编写的,比如使用C语言编写的本地方法,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。
HotSpot VM将本地方法栈和JVM栈合并了。

(12)程序计数器
在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。
程序计数器仅占很小的一块内存空间。
当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。
程序计数器这个内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存不足错误)的区域。
(13)JVM执行引擎
Java虚拟机相当于一台虚拟的“物理机”,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的。而JVM的执行引擎是自己实现的,因此程序员可以自行制定指令集和执行引擎的结构体系,因此能够执行那些不被硬件直接支持的指令集格式。
在JVM规范中制定了虚拟机字节码执行引擎的概念模型,这个模型称之为JVM执行引擎的统一外观。JVM实现中,可能会有两种的执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。有些虚拟机只采用一种执行方式,有些则可能同时采用两种,甚至有可能包含几个不同级别的编译器执行引擎。
输入的是字节码文件、处理过程是等效字节码解析过程、输出的是执行结果。在这三点上每个JVM执行引擎都是一致的。
(14)本地方法接口(JNI)
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C和C++)。
JNI的适用场景
当我们有一些旧的库,已经使用C语言编写好了,如果要移植到Java上来,非常浪费时间,而JNI可以支持Java程序与C语言编写的库进行交互,这样就不必要进行移植了。或者是与硬件、操作系统进行交互、提高程序的性能等,都可以使用JNI。需要注意的一点是需要保证本地代码能工作在任何Java虚拟机环境。
JNI的副作用
一旦使用JNI,Java程序将丢失了Java平台的两个优点:
1、程序不再跨平台,要想跨平台,必须在不同的系统环境下程序编译配置本地语言部分。
2、程序不再是绝对安全的,本地代码的使用不当可能会导致整个程序崩溃。一个通用规则是,调用本地方法应该集中在少数的几个类当中,这样就降低了Java和其他语言之间的耦合。
(15)JVM 常量池
JVM常量池也称之为运行时常量池,它是方法区(Method Area)的一部分。用于存放编译期间生成的各种字面量和符号引用。运行时常量池不要求一定只有在编译器产生的才能进入,运行期间也可以将新的常量放入池中,这种特性被开发人员利用比较多的就是String.intern()方法。
由“用于存放编译期间生成的各种字面量和符号引用”这句话可见,常量池中存储的是对象的引用而不是对象的本身。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,它也实现了对象的共享。
(16)JVM优化
JVM 优化主要是解决java的 GC (垃圾回收)问题。
JVM 的使用过程中各代有,年轻带主要存放,新创建对象。 年老代,年老代存放从年轻代存活的 对象。Perm(持久代)用 于存放静态文件,如今Java类、方法等。一般持久代可以设置大一点。
GC优化的目的有两个:
1、将转移到老年代的对象数量降低到最小;
2、减少full GC的执行时间;
为了达到上面的目的,一般地,你需要做的事情有:
1、减少使用全局变量和大对象;
2、调整新生代的大小到最合适;
3、设置老年代的大小为最合适;
4、选择合适的GC收集器;
【垃圾回收(GC收集器):串行收集器、并行收集器、并发收集器。
o 串行处理器:
–适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
–缺点:只能用于小型应用
o 并行处理器:
–适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。(例如 ERP 银行系统)
–缺点:应用响应时间可能较长
o 并发处理器:
–适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。(例如互联网网站)】
5、设置jvm堆大小 ,32bit 1.5-2G ,64bit 可以超过 2G ,新版的JDK 每个线程的堆大小在1M改变这个线程所占用的堆大小,可以生成更多的线程,一般项目里线程数不能超过5000个。
GC 和 Full GC 有什么区别?
GC(或Minor GC):收集 生命周期短的区域(Young area)。
Full GC (或Major GC):收集生命周期短的区域(Young area)和生命周期比较长的区域(Old area)对整个堆进行垃圾收集。
他们的收集算法不同,所以使用的时间也不同。
Minor GC后,Eden是空的吗?
是的,Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation 中。
GC 效率也会比较高,我们要尽量减少 Full GC 的次数。 当显示调用System.gc() 时,gc does a full collection(both young generation and tenured generation).
显式调用system.gc()会触发full gc,对象在Eden出生每经历一次MInor GC后仍然存活,并且能被Survivor容纳,就会被移动到Survivor,
并设定年龄为1.以后在Survivor每"熬过"一次Minor GC,年龄就增加一岁,当年龄超过一定值就被移动到老年代,MaxTenuringThreshold用于设置年龄阈值,但是如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,
年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
大对象直接进入老年代,比如长数组或长字符串,-XX:PretenureSizeThreshold用于设置大于这个值直接在老年代分配。
对象何时进入老年代
1.(1)当对象首次创建时, 会放在新生代的eden区, 若没有GC的介入,会一直在eden区, GC后,是可能进入survivor区或者年老代
2.(2)当对象年龄达到一定的大小 ,就会离开年轻代, 进入老年代, 对象进入老年代的事件称为晋升, 而对象的年龄是由GC的次数决定的, 每一次GC,若对象没有被回收, 则对象的年龄就会加1, 可以使用以下参数来控制新生代对象的最大年龄:
3.  -XX:MaxTenuringThreshold=n  假设值为n , 则新生代的对象最多经历n次GC, 就能晋升到老年代, 但这个必不是晋升的必要条件
4.  -XX:TargetSurvivorRatio=n  用于设置Survivor区的目标使用率,即当survivor区GC后使用率超过这个值, 就可能会使用较小的年龄作为晋升年龄
5.(3)除年龄外, 对象体积也会影响对象的晋升的, 若对象体积太大, 新生代无法容纳这个对象, 则这个对象可能就会直接晋升至老年代, 可通过以下参数使用对象直接晋升至老年代的阈值, 单位是byte
6.  -XX:PretenureSizeThreshold  即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对串行回收器以及ParNew回收有效, 而对ParallelGC回收器无效
在JVM里的内存空间,从大的层面划分,主要有新生代空间(Young)和老年代空间(Old),其中Young空间,又被分为2个部分和3个板块,分别是1个Egen区,和2个Survivor区,
https://www.tuicool.com/articles/juiIbiA
优化虚拟机堆的空间大小,根据实际物理内存的大小进行比例分配,并且,堆不进行自动扩展。然后使用ParNew+CMS进行垃圾回收,在多线程高并发的情况下,表现很好
(17)垃圾回收算法
引用计数法
引用计数法顾名思义,就是对一个对象被引用的次数进行计数,当增加一个引用计数就加1,减少一个引用计数就减1。
引用计数算法原理非常简单,是最原始的回收算法,但是java中没有使用这种算法,原因有2。1是频繁的计数影响性能,2是它无法处理循环引用的问题。
例如Teacher对象中引用了Student对象,Student对象中又引用了Teacher对象,这种情况下,对象将永远无法被回收。
标记清除
标记清除算法,它是很多垃圾回收算法的基础,简单来说有两个步骤:标记、清除。
标记:遍历所有的GC Roots,并将从GC Roots可达的对象设置为存活对象;
清除:遍历堆中的所有对象,将没有被标记可达的对象清除
这里需要注意的是标记清除算法执行过程中,会产生“stop the world”,让java程序暂停等待以保证在标记清除的过程中,不会有新的对象产生。为什么必须暂停java程序呢?举个例子,如果在标记过程完成后,又新产生了一个对象,而该对象已经错过了标记期,那么在接下来的清除流程中,这个新产生的对象因为未被标记,所以将被视为不可达对象而被清除,这样程序就会出错,因此标记清除算法在执行时,java程序将被暂停,产生“stop the world”。
接下来我们总结一下标记清除算法:
1、因为涉及大量的内存遍历工作,所以执行性能较低,这也会导致“stop the world”时间较长,java程序吞吐量降低;
2、我们注意到对象被清除之后,被清除的对象留下内存的空缺位置,造成内存不连续,空间浪费。

3:Hash算法

https://blog.csdn.net/kai46385076/article/details/96373331

4:深入理解 Spring 事务原理

https://mp.csdn.net/mdeditor/96366346#

5:Spring AOP详解

(1)Aop核心概念
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
2、切面(aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象
3、连接点(joinpoint)
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
4、切入点(pointcut)
对连接点进行拦截的定义
5、通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
6、目标对象
代理的目标对象
7、织入(weave)
将切面应用到目标对象并导致代理对象创建的过程
8、引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
(2)Spring对AOP的支持
Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:
1、默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
2、当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB
AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:
1、定义普通业务组件
2、定义切入点,一个切入点可能横切多个业务组件
3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作
所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

6:jdk1.7、jdk1.8、jdk1.9新特性?

注意,你用1.8版本开发的程序如果换到其余的1.7版本下可能会报错,即无法运行,而1.7版本下开发的程序,在1.8版本下应该可以正常的运行。1.9换到1.8同理。
因为版本是自上而下兼容,而自下而上,可能会出问题。
(1)Jdk1.7新特性?
1, Switch可以支持字符串(String类型)。
2, 泛型实例的创建可以通过类型推断来简化,可以去掉后面new部分的泛型类型,只用<>就可以了。(List strList = new ArrayList();)但是在实际使用时最好还是养成加上的习惯,因为SVN检出时如果后方泛型为空可能会导致项目无法启动。
3, 俩个char之间可以使用equals方法
4, 数值可加下划线int x= 1_2_3;(相当于int x = 123;)
5, map支持花括号传值
6, 二进制变量的表示,所有整数int、short、long、byte都可以用二进制表示,以0b开头。(byte b = (byte) 0b00100001;short s = (short) 0b1010000101000101;)
7, Try-with-resource语句(减少finally的使用)
8, Catch多个异常 (catch (IOException | SQLException ex))
9, ThreadLocalRandon 并发下随机数生成类,保证并发下的随机数生成的线程安全,实际上就是使用threadlocal
10,将字符串转换为数字int x=Integer.parseInt("+1");//JDK 1.7之前不行

(2)Jdk1.8新特性
1,允许给接口本身添加一个默认的实现。用“default”进行修饰。(接口允许有一个默认的已实现方法,该特征又叫扩展方法)
2,lambda表达式(
(int x, int y) -> x + y接收x和y这两个整形参数并返回它们的和;
() -> 42不接收参数,返回整数42;
(String s) -> { System.out.println(s); 接收一个字符串并把它打印到控制台,不返回值。
)
4,允许::调用方法(“Main::myMethod”表示 Main 类中的 myMethod() 方法)
5,加入全新的日期API(这些功能都放在java.time包下)提出了时间的加减乘除,可以直接与date直接转换。
6,允许在类和接口上写注解
7,接口中同时也也可以有static修饰的方法。
(3)JDK1.9新特性
1, 引入了一个新的package:java.net.http,里面提供了对Http访问很好的支持,支持Http1.1、HTTP2、WebSocket,据说性能超过Apache HttpClient,Netty,Jetty。
2, 对断言机制增加了一些增强。
3, 接口可以私有化方法并写方法体。
4,提供了消息发布订阅的框架,该框架主要是由Flow这个类提供的,他同样会在java.util.concurrent中出现,并且提供了Reactive编程模式。
5,轻量级的Json文本处理API。
6,改善锁争用机制(通讯服务器开房了海量的进程,链接客户端申请同一个资源)

8:Tomcat优化

1.使用64位的tomcat和jdk
2.开启server模式
3.通过-Xms和-Xmx设置初始堆大小和最大堆大小,通常将两个值设置为一样,避免堆空间不断 增大和缩小所带来的性能损耗
4.启用gzip压缩
5.通过maxThreads增加tomcat的最大并发数,将其设置为500
6.去掉对web.xml的监视,把jsp提前编辑成Servlet。
7.有富余物理内存的情况,加大tomcat使用的jvm的内存

9:nginx+tomcat负载均衡

反向代理的解释:反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
所谓tomcat集群,就是可以向外提供并行服务的多台机器,任何一台服务器宕机,其它服务器可以替代它向外提供服务,而不影响用户访问。
nginx是一个常用的反向代理服务,可自定义模块,实现请求转发及负载均衡(根具体采用策略有关)。为了tomcat集群的高可用性,还需要实现nginx的双机热备。

如果仅是对外提供一个页面访问,不用区分单一用户(不区分每个访问session,不涉及用户权限,用户资料等内容),仅仅配置nginx负载均衡策略即可。
nginx负载均衡策略主要分一下四种:
1)、轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器宕机,能自动剔除。
2)、ip_hash 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器。
3)、fair 按后端服务器的响应时间来分配请求,响应时间短的优先分配。
4)、url_hash 按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

二,如果涉及到用户session,做一些鉴权缓存、存放临时信息时,就必须做tomcat的session共享。
目前可参考到的session共享方式主要分为两种。
1)利用tomcat自带的组播机制,实现session复制。
对tomcat及应用的若干配置文件进行配置即可实现,网上有很多资料可参考。但这种方式些弊端,看过一些资料,不建议用session复制的方式。在实际使用过程中,也发现有存在session莫名失踪的现象。
2)利用第三方机制存储session。
比较常见的是tomcat集成memcached服务器来存储session。实际项目中,我们采用过利用redis实现session存储,redis高效的存取性能为高效的访问提供了保障,但是目前redis的集群功能似乎没有发布,如何解决redis的单点故障需要研究。
小结:是否实现session共享与nginx的负载策略有很大关系。比如采用轮询策略,就必须实现session共享,因为客户端会访问到每台服务器;而如果采用ip_hash策略,就可以不用考虑session共享的问题了,但是ip_hash有些缺陷使它不能随便使用(如多台pc使用同一个外网ip)。

在linux系统安装nginx+tomcat集群,
Tomcat集群
1、复制两个tomcat,在tomcat中将webapps中的ROOT文件夹都删掉(注意:使用在server.xml文件中配置,启动tomcat时,将在webapps文件夹中生成ROOT文件夹,访问时不加项目名)
2、修改server.xml文件中的端口号,第一个tomcat中修改端口号为8081
3、启动tomcat
4、修改nginx-1.9.14文件夹下的conf文件夹下的nginx.conf文件(http标签、本地服务器连接端口、请求拦截)
Nginx负载均衡安装
nginx需要编译一下,所以linux系统上需要提前安装gcc编辑软件 在安装nginx之前一定要先安装依赖库,解压nginx 命令:tar -zxvf nginx-1.8.1.tar.gz 切换到解压后的文件夹进行初始化nginx,命令:./configure ,编译nginx源码: 命令为: make 在安装nginx ,命令为:make install,进入到安装目录启动Sbin目录下的nginx命令(./nginx)启动查看进程是否启动: ps -ef | grep nginx
下面整合nginx和tomcat,修改 /usr/local/nginx/conf文件夹下的 nginx.conf文件 ,修改内容为使用的是Ip_hash策略

10:乐观锁和悲观锁

首先我们理解下两种不同思路的锁,乐观锁和悲观锁。
这两种锁机制,是在多用户环境并发控制的两种所机制。下面看百度百科对乐观锁和悲观锁两种锁机制的定义:
(1)悲观锁
悲观锁(Pessimistic Lock),正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
(2)乐观锁
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

11:单例模式

(1)饿汉式 static final field
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
(2)懒汉式,线程安全
当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

public class Singleton {
private static Singleton instance;
private Singleton (){}
public static  synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

(3)双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了。

public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) { 
synchronized (Singleton.class) {
if (instance == null) { 
instance = new Singleton();
}
}
}
return instance;
}
}

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。
(4)静态内部类 static nested class

public class Singleton { 
private static class SingletonHolder { 
private static final Singleton INSTANCE = new Singleton(); 
} 
private Singleton (){} 
public static final Singleton getInstance() { 
return SingletonHolder.INSTANCE; 
} 
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
(5)枚举 Enum
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{
INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。
一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现。就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

12:Maven的生命周期

在Maven中有三套独立的生命周期:
Clean Lifecycle:在进行真正的构建之前进行一些清理工作
Default Lifecycle:构建的核心部分,编译、测试、打包、部署
Site Lifecycle:生成项目报告、生成站点、发布站点
例如,默认(default)的生命周期包括以下阶段(注意:这里是简化的阶段,用于生命周期阶段的完整列表,请参阅下方生命周期参考):
验证(validate) - 验证项目是否正确,所有必要的信息可用
编译(compile) - 编译项目的源代码
测试(test) - 使用合适的单元测试框架测试编译的源代码。这些测试不应该要求代码被打包或部署
打包(package) - 采用编译的代码,并以其可分配格式(如JAR)进行打包。
验证(verify) - 对集成测试的结果执行任何检查,以确保满足质量标准
安装(install) - 将软件包安装到本地存储库中,用作本地其他项目的依赖项
部署(deploy) - 在构建环境中完成,将最终的包复制到远程存储库以与其他开发人员和项目共享。
这些生命周期阶段(以及此处未显示的其他生命周期阶段)依次执行,以完成默认生命周期。给定上述生命周期阶段,这意味着当使用默认生命周期时,Maven将首先验证项目,然后尝试编译源代码,运行这些源代码,打包二进制文件(例如jar),运行集成测试软件包,验证集成测试,将验证的软件包安装到本地存储库,然后将安装的软件包部署到远程存储库。
换句话说,在生命周期里面阶段是连续的,在不出错的前提下,比如执行打包(package)时就一定是执行了测试(test)之后再执行。
清洁(clean)生命周期
预清洁(pre-clean) 执行实际项目清理之前所需的流程
清洁(clean) 删除以前构建生成的所有文件
后清洁(post-clean) 执行完成项目清理所需的流程
站点(site)生命周期
预网站(pre-site) 在实际的项目现场生成之前执行所需的进程
网站(site) 生成项目的站点文档
后网站(post-site) 执行完成站点生成所需的进程,并准备站点部署
网站部署(site-deploy) 将生成的站点文档部署到指定的Web服务器

13:linux常用的一些命令:

1.df -h:查看磁盘空间大小
2.cat /proc/meminfo :查看内存信息
3.cat /proc/cpuinfo :查看cpu信息
4.top:相当于widows下的任务管理器
5.history:查看历史命令
6.tar -zcvf 文件名.tar.gz 要压缩的文件 :将指定的文件打包压缩成tar.gz
7.tar -zxvf 文件名.tar.gz :解压缩后缀名为tar.gz文件
8.unzip 文件名.zip :解压后缀名为zip的压缩文件
9.ifconfig:查看ip地址
10.java -version:查看jdk的版本
11.rpm -qa | grep 软件的名称:查找和指定名称相关的软件
12.rpm -e --nodeps 软件名称:卸载指定的软件
13.rpm -ivh 软件名称: 安装指定的软件
14.uname -a :查看linux系统的基本信息(计算机名,操作的位数,版本号)
15.ll/ls:用来查看当前目录下的所有文件资源。
16.mkdir 目录名:创建文件夹
17.vi 文件名:对指定的文件名进行编辑。
18. :wq! 强制保存并退出,:q! 强制退出
19. dd 在命令模式下删除行。
20.pwd : 查看当前目录的完整路径
21.mv 源文件名 目标文件名:移动并重命名的作用
22.rm -rf 文件夹名 :递归强制删除文件夹及其下面的所有子文件
23.service iptables stop/start/status:禁用/启动/查看防火墙状态
24.chmod +x *.sh:使所有后缀名为sh的文件,拥有可执行权限
25.在bin目录下./startup.sh启动tomcat
26.在bin目录下通过tail -f …/logs/catalina.out来查看启动日志.
27.ps -ef | grep 进程名 :查看指定进程是否启动。
28. kill -9 进程号:强制杀死进程
29.touch 文件名称: 创建文件
30.cat 文件名称: 查看文件内容
31.ping 测试通讯链接
32.cp 源文件目录1 新文件目录 :复制文件
33.ping 测试通讯链接
34.clear 清屏
35.ctrl + c :退出进程 多用于 退出查看日志等

14:BS与CS的联系与区别。

C/S是Client/Server的缩写。服务器通常采用高性能的PC、工作站或小型机,并采用大型数据库系统,如Oracle、Sybase、InFORMix或 SQL Server。客户端需要安装专用的客户端软件。
B/S是Brower/Server的缩写,客户机上只要安装一个浏览器(Browser),如Netscape Navigator或Internet Explorer,服务器安装Oracle、Sybase、InFORMix或 SQL Server等数据库。在这种结构下,用户界面完全通过WWW浏览器实现,一部分事务逻辑在前端实现,但是主要事务逻辑在服务器端实现。浏览器通过Web Server 同数据库进行数据交互。
C/S 与 B/S 区别:
1.硬件环境不同:
  C/S 一般建立在专用的网络上, 小范围里的网络环境, 局域网之间再通过专门服务器提供连接和数据交换服务.
  B/S 建立在广域网之上的, 不必是专门的网络硬件环境,例与电话上网, 租用设备. 信息自己管理. 有比C/S更强的适应范围, 一般只要有操作系统和浏览器就行
2.对安全要求不同
  C/S 一般面向相对固定的用户群, 对信息安全的控制能力很强. 一般高度机密的信息系统采用C/S 结构适宜. 可以通过B/S发布部分可公开信息.
  B/S 建立在广域网之上, 对安全的控制能力相对弱, 可能面向不可知的用户。
3.对程序架构不同
  C/S 程序可以更加注重流程, 可以对权限多层次校验, 对系统运行速度可以较少考虑.
  B/S 对安全以及访问速度的多重的考虑, 建立在需要更加优化的基础之上. 比C/S有更高的要求 B/S结构的程序架构是发展的趋势, 从MS的.Net系列的BizTalk 2000 Exchange 2000等, 全面支持网络的构件搭建的系统. SUN 和IBM推的JavaBean 构件技术等,使 B/S更加成熟.
4.软件重用不同
  C/S 程序可以不可避免的整体性考虑, 构件的重用性不如在B/S要求下的构件的重用性好.
  B/S 对的多重结构,要求构件相对独立的功能. 能够相对较好的重用.就入买来的餐桌可以再利用,而不是做在墙上的石头桌子
5.系统维护不同
  C/S 程序由于整体性, 必须整体考察, 处理出现的问题以及系统升级. 升级难. 可能是再做一个全新的系统
  B/S 构件组成,方面构件个别的更换,实现系统的无缝升级. 系统维护开销减到最小.用户从网上自己下载安装就可以实现升级.
6.处理问题不同
  C/S 程序可以处理用户面固定, 并且在相同区域, 安全要求高需求, 与操作系统相关. 应该都是相同的系统
  B/S 建立在广域网上, 面向不同的用户群, 分散地域, 这是C/S无法作到的. 与操作系统平台关系最小.
7.用户接口不同
  C/S 多是建立的Window平台上,表现方法有限,对程序员普遍要求较高
  B/S 建立在浏览器上, 有更加丰富和生动的表现方式与用户交流. 并且大部分难度减低,减低开发成本.
8.信息流不同
  C/S 程序一般是典型的中央集权的机械式处理, 交互性相对低
  B/S 信息流向可变化, B-B B-C B-G等信息、流向的变化, 更像交易中心。

15:常见的六种设计模式以及应用场景

1) 单例模式。
单例模式是一种常用的软件设计模式。
在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
应用场景:如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
2) 工厂模式。
工厂模式主要是为创建对象提供了接口。
应用场景如下:
a、 在编码时不能预见需要创建哪种类的实例。
b、 系统不应依赖于产品类实例如何被创建、组合和表达的细节。
3) 策略模式。
策略模式:定义了算法族,分别封装起来,让它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。
应用场景如下。
a、 一件事情,有很多方案可以实现。
b、我可以在任何时候,决定采用哪一种实现。
c.、未来可能增加更多的方案。
d、 策略模式让方案的变化不会影响到使用方案的客户。
举例业务场景如下。
系统的操作都要有日志记录,通常会把日志记录在数据库里面,方便后续的管理,但是在记录日志到数据库的时候,可能会发生错误,比如暂时连不上数据库了,那就先记录在文件里面。日志写到数据库与文件中是两种算法,但调用方不关心,只负责写就是。
4) 观察者模式。
观察者模式又被称作发布/订阅模式,定义了对象间一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
应用场景如下:
a、对一个对象状态的更新,需要其他对象同步更新,而且其他对象的数量动态可变。
b、对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。
5) 迭代器模式。
迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
应用场景如下:
当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍 历的时候,就应该考虑用迭代器模式。其实stl容器就是很好的迭代器模式的例子。
6) 模板方法模式。
模板方法模式定义一个操作中的算法的骨架,将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。
应用场景如下:
对于一些功能,在不同的对象身上展示不同的作用,但是功能的框架是一样的。
总结:个人意见,对编程过程中较为显著的设计原则要遵循,对较为接近的设计模式要进行借鉴,不一定要完全按照套路,类似于将武林秘籍与自己所学加以印证,这样才能为我所用。

16:java中内存泄露有几种?如何分析泄露原因

什么是内存泄露?经常听人谈起内存泄露,但要问什么是内存泄露,没几个说得清楚。内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。那么,Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。具体主要有如下几大类:静态集合类引起内存泄露,当集合里面的对象属性被修改后,再调用remove()方法时不起作用。监听器,各种连接,内部类和外部模块等的引用,单例模式

九:多线程

在典型的Java面试中, 面试官会从线程的基本概念问起, 如:为什么你需要使用线程, 如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口),然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程常用的设计模式,经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的, 你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。掌握了这些技巧,你就可以轻松应对多线程和并发面试了。

程序(program):一段静态的代码,为完成特定任务、用某种语言编写的一组指令的集合。
进程(process):程序的一次执行过程,或是正在运行的一个程序。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。

你可能感兴趣的:(java,面试)