一、jvm题目
1、java的内存模型
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:堆,方法区
程序计数器: 当前线程所执行字节码的行号指示器。没有定义异常。
虚拟机栈: java方法执行的内存模型,每一个java方法在执行的时候都会创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表存放了编译期可知的基本类型和引用类型,局部变量表所需的内存空间在编译期间分配完成,运行期间不会改变大小。操作数栈中存放指令执行时所需要的操作数;动态链接:每个栈帧中都包含一个该栈帧所属方法的引用;方法出口:每一个方法结束无非是正常return或者抛出了Exception并且没有catch语句,在方法执行完之后需要返回到方法被调用的位置,返回时需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。可能抛出StackOverflowError或OutOfMemoryError
本地方法栈: 为native方法服务。可能抛出StackOverflowError或OutOfMemoryError
堆: 虚拟机所管理内存中最大的一块,在虚拟机启动时创建,用于存放对象实例和数组,从垃圾回收的角度看分为新生代、老年代;再细致可分为Eden、FromSurvivor、ToSurvivor空间、老年代。可能出现OutOfMemoryError
方法区: 用来存储虚拟机加载的类信息、常量、静态变量、即时编译后的代码数据。垃圾回收的对象为:常量池的回收和类型的卸载。可能出现
OutOfMemoryError。
运行时常量池(方法区的一部分):用来存放编译期生成的各种字面量(文本字符串、声明为final的常量)和符号引用(类的全限定名、字段和方法的名称和描述符)
还有一块不属于虚拟机运行时数据区的一部分为直接内存:例如nio中使用native函数库直接分配堆外内存,然后通过堆内的引用直接操作这块内存,避免了java堆和native堆的来回复制,可能抛出OutOfMemory异常
与垃圾回收相关的JVM参数:
-Xms/-Xmx堆的初始大小/堆的最大大小
-Xss栈的大小
-Xmn堆中年轻代的大小
-XX:-DisableExplicitGC让System.gc()不产生任何作用
-XX:+PrintGCDetails打印GC的细节
-XX:+PrintGCDateStamps打印GC操作的时间戳
-XX:NewSize/XX:MaxNewSize设置新生代大小/新生代最大大小
-XX:NewRatio可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold/-XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
2、类加载的过程
初始化的五种情况:
1.遇到new、getStatic、putStatic、invokeStatic这四条字节码指令时
2.对类进行反射调用时,该类未初始化
3.初始化一个类时,其父类若没有初始化则先初始化其父类(接口除外)
4.Main()函数所在的类
5.Jdk1.7中的动态语言的支持
以上称为对类的主动引用,被动引用不会引发初始化:
1.通过子类引用父类的静态字段
2.通过数组定义来引用类
3.引用类的常量(finalstatic的变量会在编译的时候存入常量池)
加载: 通过类的全限定名加载二进制字节流;将这个流所代表的静态存储结构转化为方法区的运行时数据结构;在方法区中生成一个代表这个类的Class对象
验证: 确保class文件中的字节流中的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全:文件格式验证、元数据验证、字节码验证、符号引用验证
准备: 为类变量分配内存并设置类变量初始零值
解析: 将常量池中的符号引用替换为直接引用。这一阶段会根据需要发生在初始化之前或之后,包含类或接口解析、字段解析、方法解析符号引用是无关虚拟机实现的内存布局。直接引用是和虚拟机实现的内存布局相关的。符号引用必须在运行期转换获得真正的内存入口地址
初始化: 执行\<clinit\>方法,该方法由编译器自动按书写顺序收集类中所有的类变量的赋值动作和静态语句块中的语句合并而成。
new一个类时:父类静态、子类静态、父类成员域、父类构造代码块、父类构造方法、子类成员域、子类构造代码块、子类构造方法
3、对象创建方法,对象的内存布局,对象的访问定位
对象的创建:
- 当虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到这个类的符号引用,并检查这个类是否已经被加载、解析、初始化过。如果没有必须进行类加载过程
- 为新生对象分配内存,通过指针碰撞或者空闲列表法。为了解决并发问题可以采用TLAB本地线程分配缓冲策略
- 初始化为对应类型的零值
- 在虚拟机对对象进行设置之后,执行\<init\>方法按照程序员的意图初始化
对象内存布局:
对象在内存中的存储布局可以分为:对象头、实例数据、对齐填充对象头中存放:对象自身运行时数据、指向类元数据的指针
对象的访问定位:
使用句柄和直接指针两种方式
4、双亲委派模型
类加载器通过组合的方式建立的父子关系,称为双亲委派模型。
类需要由加载它的类加载器和类本身一同确立其在虚拟机中的唯一性。
BootstrapClassLoader: 负责加载JAVAHOME/lib目录下,或者被-XbootclassPath参数所指定的路径中的类库
ExtensionClassLoader: 负责加载JAVAHOME/lib/ext目录下或者被java.ext.dirs系统变量所指定的路径中的所有类库
ApplicationClassLoader: 是ClassLoader.getSystemClassLoader()方法的返回值,负责加载用户类路径上所指定的类库
工作过程:
一个类加载器收到了类加载的请求,它首先不会尝试自己加载这个类而是把这个请求委派给父加载器去完成。只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去加载。
作用:
Java类随着它的类加载器一起具备了一种带有优先级的层级关系,比如Object这个类,无论哪个类加载它最终都是委派到bootstrapClassLoader去加载。保证了java程序的稳定运行。
5、静态分派与动态分派
静态分派:
依赖静态类型来定位方法执行版本的分派动作称为静态分派,典型应用是Overload。在方法接收者(执行方的所有者)已经确定的情况下,使用哪个重载版本在编译阶段就已经确定。实际类型的变化在运行期才能确定。
动态分派:
在运行期依靠实际类型决定确定发放执行版本的分派过程称为动态分派,典型的应用是Override。实现的关键是invokevirtual指令:找到引用所指向的实际类型记作C,如果在类型C中找到了与目标方法特征签名都相同的方法并通过访问权限校验直接调用,若不通过则一次查找其父类中的方法,如果始终没有找到则抛出AbstractMethodError异常
6、GC的两种判定方法
引用计数法:
在对象中添加一个引用计数器,每一个地方引用它值加1,当引用失效时值减1。很难解决对象间互相循环引用的问题。
可达性分析算法:
一个对象到GCRoots不可达就会判定为可回收对象
GCRoots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象。如果一个对象与GCRoots不可达时,将会被第一次标记并筛选判断是否有必要执行finalize()方法,有必要执行的进入F-QUEUE队列,稍后虚拟机将自动建立一个低优先级的线程去执行它。稍后虚拟机将进行二次标记:在finalize()方法中建立了与GCRoots的通路,仍然可以存活
7、java引用都有哪几类
强引用: 类似Obo=newOb();永远不会回收强引用引用的对象
软引用: 只有在内存不足时(将发生异常前),才会被回收。用来实现缓存
弱引用: 下一次垃圾回收就会被回收。Util包中的WeakHashMap就使用了弱引用。它的key都被封装成弱引用,一旦强引用被删除,这个弱引用就会在下一次被回收。用来解约内存空间。
虚引用: 不会对垃圾回收时间产生影响,这个对象被回收时,系统会受到一个通知。在创建虚引用的时候必须传入一个引用队列。
ReferenceQueue:我们可以声明虚引用来引用我们感兴趣的对象,在gc要回收的时候,gc收集器如果发现它还有虚引用会把这个虚引用添加到referenceQueue,这样我们如果检测到referenceQueue中有我们感兴趣的对象的时候,说明gc将要回收这个对象了。此时我们可以在gc回收之前做一些其他事情,比如记录些日志什么的。
ReferenceQueuerefQueue=newReferenceQueue();
DigitalCounterdigit=newDigitalCounter();
PhantomReference\<DigitalCounter\>phantom=newPhantomReference\<DigitalCounter\>(digit,refQueue);
8、堆里面的分区各自特点
分区:Eden、fromsurvival、tosurvival、老年代
Eden区
位于Java堆的新生代。新生代中的对象寿命较短。大多数情况下对象有现在Eden区分配,如果启动了TLAB(本地线程分配缓冲)则优先在TLAB上分配。如果Eden区内存也用完了,则会进行一次MinorGC。
Survival区
复制算法将内存分为Eden、fromsurival、tosurvival三个区域,使用的时候每次只使用Eden和fromsurvival区域,当回收时将Eden和fromsurvival区中的活着的对象复制到tosurvival区上。
老年代
里存放的都是存活时间较久的,大小较大的对象:
大量连续内存空间的大对象直接进入老年代、长期存活的对象将进入老年代。
当老年代容量满的时候,会触发一次FullGC,回收年老代和年轻代中不再被使用的对象资源。
9、常见垃圾收回算法
标记清扫法: 首先标记处所有需要回收的对象,然后统一回收,老年代
复制算法: 将内存划分为大小相等的两块,每次只使用其中的一块,当块用完了,就把还活着的对象复制到另一块上,然后把已使用的那块一次清理。新生代的收集都采用复制算法
标记整理法: 先标记需要回收的对象,然后让所有存活的对象都向一端移动,老年代
10、GC收集器有哪些?CMS收集器与G1收集器的特点?如果让你优化收集方法,有什么思路?
新生代收集器:Serial(单线程)、ParNew(多线程)
老年代:CMS(标记清扫)、SerialOld(标记整理)、ParallelOld
Jdk1.7:G1收集器(标记整理)
CMS收集器: 是一种以获取最短回收停顿时间为目标的收集器。基于标记清除算法实现,整个过程分为以下4个步骤:
(1)初始标记:仅仅只标记一下GCRoots能直接关联的对象,速度很快;
(2)并发标记:进行GCRootsTracing;
(3)重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间稍长,但远比并发标记短。
(4)并发清除;耗时最长的并发标记和并发清除都可以和用户线程一起工作。
有如下缺点:
(1)CMS收集器对CPU资源非常敏感
(2)无法处理浮动垃圾,可能出现ConcurrentModeFailure失败而导致另一次的FullGC。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然就会有新的垃圾不断产生,CMS无法在当次收集中处理掉他们,只好留待下一次GC时再清理。所以需要预留一部分空间存储这部分垃圾,当预留的内存无法满足程序需求,就会出现一次ConcurrentModeFailure失败。
(3)标记清除算法将会产生大量空间碎片。
G1收集器:
(1)并行与并发:G1能充分利用多CPU、多核环境下的硬件优势
(2)分代收集:采用不同的方式处理新建对象和已存活对象
(3)空间整合:基于标记整理算法实现,不会产生空间碎片
(4)可预测停顿:G1除了追求低停顿外,还能建立可预测的时间停顿模型
G1将整个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离了,它们都是一部分Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许收集的时间,优先回收价值最大的Region。
G1使用RememberedSet来避免全堆扫描,G1中每个Region都有一个与之对应的RememberedSet,当一个对象被引用时就会把相关引用信息记录到被引用对象所属的Region的RememberedSet之中,在进行垃圾回收时,就不会进行全堆扫描。
步骤:
(1)初始标记:仅仅只标记一下GCRoots能直接关联的对象,耗时很短
(2)并发标记:从GCRoot开始对堆中对象进行可达性分析,找出存活的对象,耗时较长,但可与用户线程并发执行。
(3)最终标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,更新RememberedSet
(4)筛选回收:对Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
优化思路:
因为已经使用了标记-整理算法,所以应该从停顿时间上进行优化
11、MinorGC与FullGC的触发机制
MinorGC(新生代GC): 大多数情况,对象在新生代Eden区中分配,当Eden区中没有足够的空间,虚拟机触发一次MinorGC。MinorGC非常频繁、速度快。
FullGC(老年代GC)):
1、MinorGC之前会先检查老年代中最大可用连续空间是否大于新生代中的对象,如果成立,则这次MinorGC是安全的。否则就要查看是否允许担保失败,如果不允许则改为进行FullGC,允许的话就尝试进行一次MinorGC;
2、此外当老年代容量满的时候,也会触发一次fullGC
12、几种常用的内存调试工具:jmap、jstack、jconsole
1、jps: 输出JVM中运行的进程状态信息
jps[options][hostid]
如果不指定hostid就默认为当前主机或者服务器
参数说明:
-q:不输出类名、Jar名和传出main方法的参数
-m:输出传入main方法的参数
-l:输出main类和Jar全限定名
-v:输出传入JVM的参数
2、jstack: 查看某个Java进程内的线程堆栈信息
jstack[option]pid
参数说明:
-l:longlistings,会打印出额外的锁信息,在发生死锁时可以用jstack–lpid来观察锁持有情况
-m:mixedmod,不仅会输出java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)
jstack可以定位到线程堆栈,根据堆栈信息可以定位到具体代码。
ps–ef或者psaux查看进程
ps–Lfppid或者top–Hppid找出该进程内最耗费cpu的线程
3、map(MemoryMap)和jhat(JavaHeapAnalysisTool): jmap用来查看堆内存使用情况,一般结合jhat使用。
jmap[option]pid
如果运行在64位JVM上,可能需要指定-J-d64命令选项参数.
4、Console: 一个内置Java性能分析器
5、statJVM: 计监测工具
13、System.gc方法有什么用
希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
14、Object.finalize方法有什么用
1、finalize()是由JVM自动调用的,JVM感觉内存空间有限时,才会开始执行finalize()。
2、gc只能清除在堆上分配的内存,而finalize回收在栈上面分配的内存。
3、finalize()方法是对象逃脱死亡命运的最后一次机会,而且任何一个对象的finalize()方法都只会被系统调用一次。
二、javaSE题目
1、jdk1.7与jdk1.8的新特性
1.7
1、switch语法支持String
2、可以直接使用0b开头表示二进制变量
3、try-with-resource语句用于生命资源,保证资源在程序结束后关闭
4、catch多个异常
5、数字类型的下划线表示方式
1.8
1、允许给接口添加default的方法的实现
2、lambda表达式
3、函数式接口
4、关于日期处理在java.time包下有了新的api
2、基本类型与包装类、基本类型的运算
类型 | 长度(位) |
---|---|
boolean | 1 |
byte | 8 |
char | 16 |
short | 16 |
int | 32 |
float | 32 |
long | 64 |
double | 64 |
Integer是一个不可变对象,自动装箱和拆箱是由编译器来完成的。
1、newInteger(i)与Integer.valueof(i)方法的区别:
构造方法产生一个新的Integer对象,valueOf()方法有可能通过缓存经常请求的值而显著提高空间和时间性能。
1 | public static Integer valueOf(int i) { |
其中IntegerCache.high值默认为127,可以通过虚拟机参数AutoBoxCacheMax自定义
1 | Integerc = 1; //这是自动装箱,编译器调用valueOf(1)方法 |
2、自动包装机制的好处:
节省了常用数值的内存开销和创建对象的开销,提高了效率。
(1)Integer和int之间可以进行各种比较:Integer对象将自动拆箱后与int值比较。
(2)两个Integer对象之间也可以用\>、\<符号比较大小:两个Integer对象都拆箱后,在比较大小,但是两个Integer对象最好不要用==比较,因为-128~127范围内是取缓存内对象,所以相等,该范围外是两个不同对象引用比较,所以不等。
3、其他基本类型的包装机制
Byte、Short、Long对应的是-128~127
Character对应的是0~127
Float和Double没有自动装箱池
4、基本类型的运算
(1)在将float、double类型转换为int类型的时候总是截尾,如果想象四舍五入可以+0.5后强转,或者使用Math.round()方法。
Math.floor()取下整,Math.ceil()取上整。
(2)char、byte、short在运算之前会自动转换为int类型,结果自然也就是int类型
1 | int a = 2;int b = 3; |
包装类的equals()方法会取值进行比较
3、switch是否能作用在byte、long、String上?
在Java5以前,switch(expr)中,expr只能是byte、short、char、int。从Java5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
4、String、StringBuffer、StringBuilder
StringBuffer是线程安全的
String对象是不可变的,当需要改变它的内容的时候会返回一个新的对象
StringBuffer、StringBuilder是变量
三者在执行速度上:StringBuilder\>StringBuffer\>String
String对+运算符的重载实际上是使用StringBuilder.append()创建了一个新的String对象
String为什么设计成不可变对象
1 | public final class String implements java.io.Serializable, Comparable<String>, CharSequence { |
包含两个成员变量:value[]:String的字符序列,hash:该String对象的hash值的缓存。我们通过普通代码对一个String的改变都是通过返回一个新的String对象来完成的。但是String也不是绝对不可变的,我们可以通过反射拿到value对象,然后改变它。(final域是不能修改的,但是如果final域指向的对象内的域是可变的话,我们就可以修改final域指向对象的内容)
设计成不可变的好处
1、因为它是不可变的,所以其hashCode()永远保持一致,也就是每次在使用一个String对象的hashCode的时候都不用重新计算,直接返回hash值即可,效率更高
2、不可变对象天生线程安全,可以无需同步的在多个线程间共享
3、安全性,常用于数据库连接、网络连接、打开文件等,避免了参数修改
5、常见String比较,intern()
1、new出来的对象引用永远指向堆内存,String.intern()返回对应pool中的对象
2、Strings1="dsfsadf";这种字面量写法直接返回pool中的对象
3、Strings2="dsafd"+s1;指向堆,只有+左右都是""字面量才返回pool中。
4、String.intern()返回字符串对象的规范化表示形式。一个初始时为空的字符串池,它由类String私有地维护。String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。记录首次出现的实例
6、请描述抽象类和接口的区别
1、抽象类的域没有特殊限制,接口的域自动为publicstaticfinal的,在声明的同时必须初始化
2、接口中定义的方法必须(自动为)是publicabstract的,抽象类中的抽象方法可以是默认类型或者public类型或者protected类型
3、Implements一个接口必须override所有方法,extends一个抽象类则只要override抽象方法。一个类只能继承一个类(抽象类),但是可以实现多个接口
4、从选择上来讲,当所表达的意思是某些事物的共性的时候最好使用接口,而当表达的意思是某一个具体的事物的时候就用抽象类
7、内部类
使用场景: 只考虑为它的外部类提供服务
每个内部类都能够独立的实现接口或者继承类
成员内部类: 可以无条件访问外部类的所有成员属性和成员方法(包括private)。当成员内部类的field或method与外部类相同时默认访问的是内部类。每个实例都包含一个指向外围实例的引用,如果需要访问外围实例可使用outer.this.fild/method。在外部类中访问内部类的成员时需要先创建一个外部类,
1 | Outter outter = new Outter(); |
局部内部类: 定义在一个方法内部或者是一段作用域内的类,仅限于在作用域内访问
匿名内部类:
静态内部类: 不依赖于外部类,不能使用外部类的非static的域或者方法
1、为什么成员内部类可以无条件的访问外部类的成员?
内部类是java的一颗语法糖,编译之后会生成两个class文件,匿名内部类名字为外部类名$x,成员内部类名字为外部类名$内部类名,编译器会默认为成员内部类添加一个参数类型为指向外部类对象的构造方法。
2、为什么局部内部类和匿名内部类只能访问部局部final变量?
比如我们在一个方法内写了一个Thread匿名内部类,当外部的方法执行完毕后,方法内的局部变量生命周期结束,而Thread对象的生命周期还没有结束,所以必须使用final修改外部类的局部变量以使它在编译阶段就被放入常量池当中
3、Object.this与Object.new
当我们需要实例化一个成员内部类的时候,可以通过OuterClass.newInnerClass的方式初始化
如果我们需要在内部类中生成对外部类对象的引用可以使用OuterClass.this
8、java多态的实现原理
多态就是同一个消息根据发送对象的不同而采取多种不同的行为方式。主要体现在Overload和Override上
9、Object类都有什么方法
getClass(),toString(),hashCode(),clone(),wait(),notify(),notifyAll(),equals(),finalize()
要用想使用clone(),目标类必须实现Cloneable接口,然后以public的方式重写clone()方法,Object中原生的native方法clone()执行的是此对象的浅复制
10、hashCode()的作用,域equals()有什么关系
HashCode()返回该对象的hash()码值,Object()中的原生hashCode()是一个native方法,它将对象的内部地址转换成一个整数来实现。
HashCode()有一个常规协定:
1、当一个对象参与hash计算的成员域的值没有改变时,重复调用hashCode()值都相等;(一致性)
2、若两个对象equals()那么他们的hashCode()必须相等;
3、但是hashCode()相等,两个对象不一定equals()。这一点在hashMap的存储上有深刻体现。
Overrideequals()方法必须重写hashCode()。重写equals()要满足对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、自反性(x.equals(x)必须返回true)、一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)。
11、深复制与浅复制区别
浅复制: 会创建一个新对象,对于源对象的基本类型属性拷贝一份,引用类型属性也只是拷贝引用。实现方式:类要重写实现Cloneable接口,以public的方式直接super重写clone()方法
深复制: 拷贝对象所有的属性,并拷贝引用类型属性所指向的对象。实现方法:重写clone()方法自定义重写过程;
对象序列化
12、说一下java异常体系
Throwable是根接口,有两个子接口Error、Exception
不可查异常:RunTimeException、Error
可查异常:除不可查异常之外的Exception,比如IOException、SQLException
注意事项:
finally语句块,总会被执行,但除以下情况:
1、finally语句块中出现异常
2、前面的代码调用了System.exit()
当try-catch语句块中有return语句时,方法会在返回前执行finally语句中的内容
finally语句块中重新抛出异常会覆盖掉之前的异常,在finally中直接使用return会丢失异常。
13、java中存在内存泄漏吗?请简单描述
从语法层面上来说没有,因为java有垃圾会收机制,然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。
1、HashMap、ArrayList这一类型的集合对象,经常会导致内存泄漏,当他们被声明为static时,生命周期是和程序一样长的。所以它们引用的对象在程序的整个生命周期内都存在。
关于集合类还有一种情况是:当已添加到Set/Map中的对象,参与hashcode和equals()计算的变量被修改后,调用remove()无效,也会造成内存泄漏
2、单例模式的使用也可能导致内存泄漏,因为单例对象初始化后将在java虚拟机的整个生命周期中存在,如果他引用了一个对象,这个对象以及这个对象引用的所有对象都无法被垃圾收集器回收。
3、各种连接,如:dataSource、getConnection()、socket、io这些连接,JVM都不会主动回收,需要我们自己显式的在finally语句中close()
4、使用ThreadLocal也可能会导致内存泄漏,ThreadLocal的get()方法是从当前线程对象的ThreadLocalMap属性中获取对应的值的,ThreadLocalMap中维护着以ThreadLocal的弱引用为key的元素,当外部没有一个引用引用ThreadLocal的时候,就会被回收,这时Map中的key就为null了,这个元素也就无法被访问了,所以可能会被回收。
5、在使用Hibernate进行批量更新数据的时候也可能导致,因为更新数据后对象为持久态,会一直在session中存在,所以需要定期session、clear()强制清空缓存、session、flush()
14、简述整理容器体系
Stack是extendsVector的
LinkedHashSet、LinkedHashMap按照插入顺序排序
Queue:
在容量不足时抛异常 | 为空或容量不足时回返回false或null | |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove(e) | poll(e) |
仅返回 | element(e) | peek(e) |
15、ArrayList、LinkedList源码分析
16、HashMap、HashSet源码分析
Jdk7:位桶+链表
初始值:16最大值:2^30负载因子:0.75
底层数组的长度总是2^n
1 | final Entry <?,?>[] EMPTY_TABLE = {}; |
Entry是一个实现了Map.Entry\<K,V\>的类。
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
核心构造方法:HashMap(intinitialCapacity,floatloadFactor);
其他的构造方法都是调用这个构造方法。
核心方法:
1 | public V put(K key, V value){ |
Entry放到数组的哪一个位置上是通过计算key的hashCode()得到的,当发生hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上。
当size大于threshold(极限值)时,会发生扩容。threshold=capacity*loadfactor(容量*负载因子),扩容为原来的22倍。
Jdk8:位桶+链表/红黑树
当某个位桶的链表的长度达到某个阀值(默认为8)的时候,这个链表就将转换成红黑树。因为链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提高效率
HashSet:有一个HashMap成员域,所有方法直接调用HashMap中的方法。
如何保证元素唯一性的?
首先要存入的对象想要保证唯一性必须重写hashcode()和equals()方法,因为HashSet的底层实现是HashMap,Set的add()方法直接调用Map的put()方法,再解释map的put()方法
17、List和Set区别
它们都实现了Collection接口,List允许重复元素,并且维护一定顺序。其中ArrayList底层为数组,随机访问O(1),但是插入和删除速度慢O(n);LinkedList底层为双向链表,插入删除快O(1),查找慢O(n),LinkedList可以用作栈、队列。
Set不保存重复元素,HashSet使用了散列,长与查询;TreeSet将元素存储在红黑树当中,元素必须实现Comparable接口,维护特定顺序;LinkedHashSet也使用了散列,但又使用了链表来维护元素插入顺序。
18、说一下I/O框架
主要包含File类、I/O流、RandomAccessFile类(支持对文件的随机读取和写入,主要结合nio使用)
I/O包含字符流和字节流,各又分为输入输出两部分
1、字节流
InputStream表示从不同数据源产生的输入类:ByteArrayInputStream、StringBufferInuputStream、FileIInputStream、PipedInputStream、SequenceInputStream、FilterInputStream
FilterInputStream做为装饰器的接口,来控制特定的输入流:DataInputStream、BufferedInputStream、LineNumInputStream
OutputStream、FilterOutputStream有与输入流相对应的类PrintStream
2、字符流,供提供Unicode操作
Reader有FileReader、StringReader、CharArrayReader、PipedReader、BufferedReader
Writer有FileWriter、StringWriter、CharArrayWriter、PipedWriter、BufferedWriter
3、对象序列化
应用场景:web服务器中的session对象,当有10万用户访问时,服务器内存可能会吃不消了,这时可以先把session序列化到磁盘上,等到需要用的时候再序列化回来。
ObjectInputStream、ObjectOutputStream:用于序列化和反序列化
默认序列化:
要序列化的类必须实现Serializable接口(默认序列化)或者Externalizable(可以控制序列化的过程)
用于普通序列化(深复制),默认序列化恢复的时候直接从存储的二进制为基础恢复,不调用任何构造器
控制序列化的过程:
(1)序列化一个Externalizable对象会按照writeExternal()方法进行,对于恢复一个Externalizable的对象,所有普通的构造器都会被调用(包括在字段定义时的初始化),然后调用readExternal()方法。
(2)使用transient关键字逐个字段的关闭序列化
HashSet中的map就是transient的
注意:对于static的字段必须手动序列化,因为static的域不属于实例
4、压缩类
压缩输出类:ZipOutputStream、GZipOutputStream
压缩输入类:ZipInputStream、GZipInputStream
BufferedOutputStreamout=newBufferedOutputStream(newGZIPOutputStream(newFileOutputStream("E:/J2EE/新建文件夹/2.gz")));
//压缩多个文件,可是用ZipOutputStream.putNextEntry()方法
19、java nio
1、与I/O的的不同点
(1)I/O使用的是基于流的方式,每次产生或消费一个字节的数据,速度慢、NIO实用的是基于块的传送方式每次产生或消费一个数据块,速度较快
(2)I/O的read()和write()方法都是阻塞的,而NIO有非阻塞模式
(3)NIO的Selector允许一个线程监视多个输入通道
2、NIO概述
NIO是基于Channel和Buffer的io方式,通道是对流的模拟,传送数据必须通过一个通道,向通道发送数据的时候必须先把数据放到缓冲区中,从通道中读取数据的时候也必须读到缓冲区中。
3、通道
FileChannel从文件中读写数据DatagramChannel通过UDP读写网络中数据
SocketChannel通过TCP读写网络中数据
ServerSocketChannel可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
FileChannel不能切换到非阻塞模式,套接字Channel可以
4、缓冲区
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer等。
Buffer的三个属性:
capacity:缓冲区的容量,只能往里边写入capacity个Char、Byte等
position、limit
写数据时:position指示当前的位置(初始值为0),最大值为capacity-1
limit表示最大能往缓冲区内写多少数据,写模式下==capacity
读数据时:从写切换到读时(调用flip()方法),position将会被置为0,limit表示最多能够读多少数据,从写切换到读时,limit将会被设置为写时候的position的值
5、使用Buffer读写数据一般步骤:
写入数据到Buffer调用flip()方法从Buffer中读数据调用clear()方法(会清除缓冲区)或者compact()方法(只清除读过的数据)
例:使用NIO读取文件中的汉字
1 | public static void read() throws IOException { |
6、ServerSocketChannel
用于监听新进来的TCP连接
1 | ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); |
默认为阻塞模式,serverSocketChannel.configureBlocking(false);设置为非阻塞模式,serverSocketChannel.accept();会立即返回,所以需要判断返回的socektChannel是否为null
7、SocketChannel
1 | socketChannel = SocketChannel.open(); |
默认为阻塞模式,socketChannel.configureBlocking(false);设置为非阻塞模式,在非阻塞模式下,connect()、read()、write()方法可能在没有完成的情况下就返回了,因此要在循环中调用write()使用socketChannel.finishConnect()判断连接是否建立关注read()方法的返回值
8、Selectors
一个Selector可以检测一到多个NIO通道,这样一个单独的线程就可以管理多个Channel,从而可以管理多个网络链接。因为线程之间的切换存在着很大的开销,所以使用的线程越少越好。
1 | Selector selector = Selector.open(); // 打开选择器 |
20、字符编码
1、iso8859-1
是一种单字节编码与ASCII类似,最多能表示的字符范围是0-255,应用于英文系列。
2、GBK
GB2312只能表示简体字,英文和ISO8859-1一致,GBK能够同时用来表示简体字和繁体字,兼容GB2312。他们两者都兼容ISO8859-1。是是不定长编码。
3、unicode
是一种定长编码,可用来表示所有语言的字符,为定长双字节,不兼容ISO8859-1,java内部使用Unicode来处理。
4、UTF-8
是一种多字节不定长格式,UTF-8将ASCII字符编码成单一的字节形式,将非ASCII字符编码成2-3个字节。字符串的长度存储在UTF-8字符串的前两个字节中。
21、泛型
1、Java允许使用List等原生类型是为了向后兼容。
2、泛型子类型化的规则:
1 | List ll; |
用使用List\<Objetct\>与与用直接使用List的区别:后者逃避了编译器的类型检查,前者明确告诉编译器持有类型。前者不能与其它泛型的引用相互,但List的引用可以指向任意泛型的List,所以使用List\<Object\>更加安全。