面试知识点1
Java8张图 1
1、字符串不变性 1
2、equals()方法、hashCode()方法的区别 1
3、Java异常类的层次结构 1
4、集合类的层次结构 2
5、Java同步 3
6、别名 3
7、堆和栈 3
8、Java虚拟机运行时数据区域 3
一、Java基础 4
1、java中的Synchronized实现 http://blog.csdn.net/hsuxu/article/details/9472371 4
2、hashmap面试问题集 http://blog.csdn.net/song19890528/article/details/16891015 6
3、Java中的有些锁 http://www.cnblogs.com/qq78292959/p/4252800.html 9
4、Hashset为什么不加重复的值 http://blog.sina.com.cn/s/blog_69fca0a20100knsh.html 12
5、hashmap实现原理浅析 http://www.cnblogs.com/lzrabbit/p/3721067.html 13
6、如何理解 Java反射机制 http://blog.csdn.net/nemo2011/article/details/6585683 14
7、什么是动态代理 http://huangnx.com/2016/10/17/proxyDesignDesc/ 14
8、JavaCAS原理深度分析 http://blog.csdn.net/hsuxu/article/details/9467651 14
9、 Java类加载机制 http://blog.csdn.net/love_Javc_you/article/details/38081683 18
10、 JDK7与JDK8中HashMap的实现 https://my.oschina.net/hosee/blog/618953 19
11、 Java中常用的锁分析总结 http://www.tuicool.com/articles/NnQjyq 19
12、volatile与synchronized的区别 http://blog.csdn.net/fanaticism1/article/details/9966163 20
13、 Java中的object类有哪些方法 : 21
14、 深复制与浅复制的区别 21
15、 深入剖析Java中的装箱和拆箱 http://www.cnblogs.com/dolphin0520/p/3780005.html 21
17、java8的新特性: 22
18、 Java动态绑定总结 http://www.cnblogs.com/lyp3314/archive/2013/01/26/2877205.html 23
19、 Java堆和栈详解 http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html 23
16、 JDK 源代码研究 Hash 存储机制 http://www.ibm.com/developerworks/cn/java/j-lo-hash/ 24
17、 JVM内存管理, 新生代, 旧生代 http://blog.sina.com.cn/s/blog_55ba8b4601014nzm.html 24
18、 图解Tomcat 与jvm 类加载机制 http://www.cnblogs.com/xing901022/p/4574961.html 26
19、 Java异常处理和设计 http://www.cnblogs.com/dolphin0520/p/3769804.html 27
20、 B树、B-树、B+树、B*树 http://www.cnblogs.com/oldhorse/archive/2009/11/16/1604009.html 27
21、 死锁产生的原因和如何解决? 30
22、如何检测两个线程死锁,或者说找出死锁的线程 30
23、进程与线程的区别和联系,进程执行的方式。 31
24、jvm虚拟机溢出都是发生在哪个地方?什么情况下会发生溢出?原因何在? 31
25、equals、==、hashcode http://blog.csdn.net/hla199106/article/details/46907725 33
二、 三大架构问题 35
1、 Spring事务传播机制和数据库隔离级别 http://comedsh.iteye.com/blog/698733 35
2、 Spring工厂模式和单态模式 http://blog.csdn.net/titilover/article/details/6729058 36
3、 理解IoC和DI、AOP 38
4、 动态代理 http://huangnx.com/2016/10/17/proxyDesignDesc/ 38
5、 Struts2的工作机制? 38
6、 Hibernate与Mybaits的区别? 38
7、 Hibernate如何实现级联操作? 39
8、 Spring有哪些优点?为什么要用spring?(总结几点就好) 39
9、http的get和post区别,servlet都有哪些方法?service有哪些方法? 40
三、 数据库问题 41
1、SQL中char、varchar、nvarchar的区别 41
2、事务是什么,以及事务四个特性 42
3、什么情况下需要创建MySQL索引 http://www.jb51.net/article/56532.htm 42
4、如何优化SQL语句? 43
5、数据库事务隔离级别和锁实现机制 http://comedsh.iteye.com/blog/698733 43
6、SQL Select语句完整的执行顺序: 43
7、聚簇索引和非聚簇索引的区别?(具体可百度) 44
四、 杂七杂八 44
1、什么是单工、半双工、全双工? 44
2、三次握手、四次挥手的原理? 44
3、http与https的区别 http://www.mahaixiang.cn/internet/1233.html 44
Java8张图
https://blog.csdn.net/zivensonice/article/details/51465531
1、字符串不变性
下面这张图展示了这段代码做了什么
String s = "abcd";
s = s.concat("ef");
2、equals()方法、hashCode()方法的区别
HashCode被设计用来提高性能。equals()方法与hashCode()方法的区别在于:
1)如果两个对象相等(equal),那么他们一定有相同的哈希值。
2)如果两个对象的哈希值相同,但他们未必相等(equal)。
3、Java异常类的层次结构
图中红色部分为受检查异常。它们必须被捕获,或者在函数中声明为抛出该异常。
4、集合类的层次结构
注意Collections和Collection的区别。(Collections包含有各种有关集合操作的静态多态方法)
5、Java同步
Java同步机制可通过类比建筑物来阐明。
6、别名
别名意味着有多个变量指向同一可被更新的内存块,
这些别名分别是不同的对象类型。
7、堆和栈
图解表明了方法和对象在运行时内存中的位置。
8、Java虚拟机运行时数据区域
图解展示了整个虚拟机运行时数据区域的情况。
一、Java基础
1、java中的Synchronized实现 http://blog.csdn.net/hsuxu/article/details/9472371
(1)同步的基础:
Java中的每一个对象都可以作为锁。
对于同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前对象的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁存在哪里呢?锁里面会存储什么信息呢?
(2)同步的原理
JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
(3) Java对象头
锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
长度 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等。
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:
25 bit 4bit 1bit
是否是偏向锁 2bit
锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01
在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
锁状态 25 bit 4bit 1bit 2bit
23bit 2bit 是否是偏向锁 锁标志位
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 空 11
偏向锁 线程ID Epoch 对象分代年龄 1 01
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
锁状态 25bit 31bit 1bit 4bit 1bit 2bit
cms_free 分代年龄 偏向锁 锁标志位
无锁 unused hashCode 0 01
偏向锁 ThreadID(54bit) Epoch(2bit) 1 01
(4)锁的升级
Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。
(5)偏向锁
Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。
(6)轻量级锁
轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
(7)锁的优缺点对比
锁 优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。
同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。
同步块执行速度较长。
2、hashmap面试问题集 http://blog.csdn.net/song19890528/article/details/16891015
(1)HashMap与哈希表
HashMap从本质上说就是哈希表,其底层实现就是围绕哈希表展看的。
哈希表的核心思想就是让记录的关键字和存储位置建立一一映射关系,这样我们就可以通过Key直接获得相对应的Value,好比我们通过索引可以直接获得数组对应的某个值一样,而这种一一映射关系要通过某个数学函数来构造出来,这个函数就是所谓的哈希函数。
而哈希函数有五种实现方式:
A. 直接定址法:取关键字的线性函数值作为哈希地址。
B. 数字分析法:取关键字的中的若干位作为哈希地址。
C. 平方取中法:取关键字平方后的中间几位作为哈希地址。
D. 折叠法:将关键字分割成位数相同的几部分(最后一部分可以不同),然后取这几部分的叠加和作为哈希地址。
E. 除留余数法:H(key) = key MOD p ,p<=m ,m为不大于哈希表的数。
F. 随机函数法
上述五中实现方式中最常用的是除留余数法,而通过哈希函数寻址的过程可能出现“冲突”------即若干个不同的key却对应相同的哈希地址。解决哈希冲突有如下的方法:
A. 开放地址法:H=(H(kyt)+d) MOD m ,m为哈希表表长。
(1)d=1,2,3------> m-1 时,称谓线性探测再散列
(2)d=12,-12---->+(-)k^2时,称为二次线性再散列。
(3)d为伪随即序列时,称为伪随即序列再散列。
B .再哈希法 :H=RH(key),RH()为不同的哈希函数,即在地址冲突时计算另一个哈希函数地址,直到不再发生冲突。
C .链地址法:将所有哈希地址冲突的记录存储在同一个线性链表中
D 公共溢出区法:将所有哈希地址冲突的记录都填入到溢出表中
而HashMap的实现与哈希函数的选择和哈希地址冲突的解决方案密切相关
(2)HashMap的具体实现
HashMap的实现采用了除留余数法形式的哈希函数和链地址法解决哈希地址冲突的方案。这样就涉及到两种基本的数据结构:数组和链表。数组的索引就是对应的哈希地址,存放的是链表的头结点即插入链表中的最后一个元素,链表存放的是哈希地址冲突的不同记录。
链表的结点设计如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
next作为引用指向下一个记录。在HashMap中设计了一个Entry类型的数组用来存放Entry的实例即链表结点。
/** The table, resized as necessary. Length MUST Always be a power of two. /
transient Entry[] table;
除留余数法形式的哈希函数:
/* Returns index for hash code h. */
static int indexFor(int h, int length) { return h & (length-1); //和除留余数等价 }
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾,数组中存储的是最后插入的元素 。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
1)HashMap与Hashtable的区别:
a、HashMap可以接受null键值和值,而Hashtable则不能。b、Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。
2)当两个对象的hashcode相同怎么办
当哈希地址冲突时,HashMap采用了链地址法的解决方式,将所有哈希地址冲突的记录存储在同一个线性链表中。具体来说就是根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
3)如果两个键的hashcode相同,你如何获取值对象
HashMap在链表中存储的是键值对,找到哈希地址位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象
4)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
HashMap默认的负载因子大小为0.75,也就是说,当一个map填满了75%的空间的时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组中。
5)为什么String, Interger这样的wrapper类适合作为键?
String, Interger这样的wrapper类是final类型的,具有不可变性,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
6)ConcurrentHashMap和Hashtable的区别
Hashtable和ConcurrentHashMap有什么分别呢?它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
7)HashMap的遍历
第一种:
Map map = new HashMap();
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
效率高,以后一定要使用此种方式!
第二种:
Map map = new HashMap();
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
Object key = iter.next();
Object val = map.get(key);
}
效率低,以后尽量少使用!
可是为什么第一种比第二种方法效率更高呢?
HashMap这两种遍历方法是分别对keyset及entryset来进行遍历,但是对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,它把key和value都放到了entry中,即键值对,所以就快了。
3、Java中的有些锁 http://www.cnblogs.com/qq78292959/p/4252800.html
1)什么是线程安全?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。代码本省封装了所有必要的正确性保障手段(互斥同步等),令调用者无需关心多线程的问题,更无需自己实现任何措施来保证多线程的正确调用。
2)线程之间的交互机制?
不同的线程之间会产生竞争,同样也有交互,最典型的例如数据库连接池,一组数据库连接放在一个数组中,如果线程需要数据库操作,则从池中获取链接,用完了就放回去。JVM提供了wair/notify/notifyAll方式来满足这类需求,典型的代码如下:
package lock;
public class Pool {
public Connection get(){
synchronized (this) {
if(free>0){
free--;
}else{
this.wait();
}
return cacheConnection.poll();
}
}
public void close(Connection conn){
synchronized (this) {
free++;
cacheConnection.offer(conn);
this.notifyAll();
}
}
}
3)如何实现线程安全?
A、互斥同步,最常见的并发正确性保障手段,同步至多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。
B、非阻塞同步,互斥同步的主要问题就是进行线程的阻塞和唤醒所带来的性能问题,因此这个同步也被称为阻塞同步,阻塞同步属于一种悲观的并发策略,认为只要不去做正确的同步措施,就肯定会出问题,无论共享的数据是否会出现竞争。随着硬件指令的发展,有了另外一个选择,基于冲突检测的乐观并发策略,通俗的讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(最常见的措施就是不断的重试,直到成功为止),这种策略不需要把线程挂起,所以这种同步也被称为非阻塞同步。
C、无同步方案,简单的理解就是没有共享变量需要不同的线程去争用,目前有两种方案,一个是“可重入代码”,这种代码可以在执行的任何时刻中断它,转而去执行其他的另外一段代码,当控制权返回时,程序继续执行,不会出现任何错误。一个是“线程本地存储”,如果变量要被多线程访问,可以使用volatile关键字来声明它为“易变的“,以此来实现多线程之间的可见性。同时也可以通过ThreadLocal来实现线程本地存储的功能,一个线程的Thread对象中都有一个ThreadLocalMap对象,来实现KV数据的存储。
4)主内存和工作内存?
Java内存模型中规定了所有变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对于变量的所有操作(读取和赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间无法直接访问对方工作内存中的变量,线程间值的传递均需要通过主内存来完成。这里的主内存和工作内存,和java中堆的模型不是一个层次,主内存主要对应java堆中对象的实例数据部分。
5)什么是自旋锁?
自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。
在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定。
6)什么是锁消除?
虚拟机即时编译器在运行时,对于代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如果判断一段代码,在椎上的所有数据都不会逃逸出去被其他线程访问到,那么认为他是线程私有的,同步加锁也就没有必要做了。
7)什么是锁粗化?
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小,仅仅在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快的拿到锁。大部分情况下,这儿原则是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至锁出现在循环体内,即使没有线程竞争,频繁的进行互斥操作也会导致不必要的性能损耗。
8)什么是偏向锁?
偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
9)关于轻量级锁?
为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
10)什么场景下适合volatile?
volatile能够实现可见性,但是无法保证原子性。可见性指一个线程修改了这个变量的指,新值对于其他线程来说是可以立即得知的。而普通变量是不能做到这一点的,变量值在线程间传递均需要通过主内存完成。volatile的变量在各个线程的工作内存中不存在一致性问题(各个线程的工作内存中volatile变量也可以存在不一致的情况,但是由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题)但是java里面的运算并非原子操作的,导致volatile变量运算在并发下一样是不安全的。
11)什么是CAS?
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。Java中通过Unsafe来实现了CAS。
12)如何实现互斥同步?
java中最基本的互斥就是synchronized关键字,synchronized在经过编译后,会在同步块的前后分别形成monitorenter和moitorexit这两个字节码指令。在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会把锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象的锁失败,当当前线程就要阻塞等待,直到对象的锁被另一个线程释放为止。synchronized对于同一个线程来说是可重入的,不会出现自己把自己锁死的问题。除了synchronized指望,JUC中的Lock也能实现互斥同步,ReentrantLock,写法上更加可见,lock和unlock配合try/finally来配合完成,ReentrantLock比synchronized有几个高级的特性。
13)ReentrantLock的高级特性有那几个?
1、等待可中断,当持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,改为处理其他事情;
2、可以实现公平锁,公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序依次获得锁,synchronized是非公平锁,ReentrantLock默认也是非公平的,只不过可以通过构造函数来制定实现公平锁;
3、锁绑定多个条件,ReentrantLock对象可以同时绑定多个Condition对象,在synchronized中,锁对象的wait/notify/notifyall方法可以实现一个隐含的条件,如果要多一个条件关联的时候,就需要额外的增加一个锁;
14)关于锁的几个使用建议?
1、使用并发包中的类,并发包中的类大多数采用了lock-free等算法,减少了多线程情况下的资源的锁竞争,因此对于线程间的共享操作的资源而言,应尽量使用并发包中的类来实现;
2、尽可能少用锁,没必要用锁的地方就不要用了;
3、拆分锁,即把独占锁拆分为多把锁(这个不一定完全适用);
4、去除读写操作的互斥锁,在修改时加锁,并复制对象进行修改,修改完毕之后切换对象的引用,而读取是则不加锁,这种方式成为CopyOnWrite,CopyOnWriteArrayList就是COW的典型实现,可以明显提升读的性能;
15)关于synchronized的几个注意点?
1、当一个线程访问object的一个synchronized(this)同步代码块时, 另一个线程仍然可以访问该object中的非synchronized(this)同步代码块;
2、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时, 一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块;
3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时, 其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞;
4、Java中的每一个对象都可以作为锁,对于同步方法,锁是当前实例对象,对于静态同步方法,锁是当前对象的Class对象,对于同步方法块,锁是Synchonized括号里配置的对象;
4、Hashset为什么不加重复的值 http://blog.sina.com.cn/s/blog_69fca0a20100knsh.html
- HashSet中不允许有重复的元素。在向HashSet中添加(add())元素的时候,对于重复的元素,只在HashSet中保留一个副本。
- HashSet中元素的顺序是随机的,包括添加(add())和输出都是无序的。
使用迭代器输出HashSet中的元素,例如:
Iterator it = hashSet.iterator();
while(it.hasNext()){
System.out.println((String)it.next());
}
假设有一个Person类:
class Person{
private String name;
private Integer age;
getter、setter方法
}
构造两个Person的实例:
Person p1 = new Person();
p1.setName("shirdrn");
p1.setAge(new Integer(26));
Person p2 = new Person();
p2.setName("shirdrn");
p2.setAge(new Integer(26));
加入到HashSet中:
Set hashSet = new HashSet();
hashSet.add(p1);
hashSet.add(p2);
此时的hashSet.size()=2。这主要是由于Object拥有hashCode()和equals()两个方法,它认为具有相同的hashcode的对象才是同一个对象,即对同一个对象的引用。
所以必须在对应的实体类中重写hashCode()和equals()两个方法。在Person类中重写hashCode()和equals()两个方法,如下所示:
public boolean equals(Object o){
if(this == o){
return true;
}
if(! (o instanceof Person)){
return false;
}
final Person other = (Person)o;
if(this.name.equals(other.getName()) && this.age.equals(other.getAge())){
return true;
}
else{
return false;
}
}
public int hashCode(){
int result;
result = (name == null?0:name.hashCode());
result=37*result+(age==null?0:age.hashCode());
return result;
}
这时,再进行上面的测试,发现hashSet.size()=1。此时,对象p1和p2具有相同的hashcode,HashSet认为添加的两个Person实例是同一个对象,只把一个添加到集合里面。
5、hashmap实现原理浅析 http://www.cnblogs.com/lzrabbit/p/3721067.html
1)HashMap和Hashtable的区别
·两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全
Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合
·HashMap可以使用null作为key,而Hashtable则不允许null作为key
HashMap以null作为key时,总是存储在table数组的第一个节点上
·HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类
·HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
·HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1
·两者计算hash的方法不同
·HashMap和Hashtable的底层实现都是数组+链表结构实现
2)HashSet和HashMap、Hashtable的区别
HashSet不是key value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,只是包含HashMap中的key而已。HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的,至于HashSet和Hashtable的区别,HashSet就是个简化的HashMap的
3)HashMap的创建
HashMap默认初始化时会创建一个默认容量为16的Entry数组,默认加载因子为0.75,同时设置临界值为16*0.75
6、如何理解 Java反射机制 http://blog.csdn.net/nemo2011/article/details/6585683
·Java反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
·Java反射机制主要提供了以下功能:
·在运行时判断任意一个对象所属的类;
·在运行时构造任意一个类的对象;
·在运行时判断任意一个类所具有的成员变量和方法;
·在运行时调用任意一个对象的方法;
·生成动态代理。
·获取类的方法:
Class.forName(“类的路径”);
类名.class
实例.getClass()
7、什么是动态代理 http://huangnx.com/2016/10/17/proxyDesignDesc/
静态代理: 在程序运行前就已经存在代理类的字节码文件。代理类和委托类的关系在运行前就确定了
动态代理: 动态代理类的源码是在程序运行期由JVM根据反射机制动态生成的。代理类和委托类的关系是在程序运行时确定的
8、JavaCAS原理深度分析 http://blog.csdn.net/hsuxu/article/details/9467651
CAS:Compare and Swap, 翻译成比较并交换。
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
·CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
·非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。
public final int get() {
return value;
}
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);类似:
if (this == expect) { this = update return true;} else {return false;}
·CAS原理
CAS通过调用JNI的代码实现的。JNI:java Native Interface为JAVA本地调用,允许java调用其他语言。
而compareAndSwapInt就是借助C来调用CPU底层指令实现的。 下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
intel的手册对lock前缀的说明如下:
确保对内存的读-改-写操作原子执行;禁止该指令与之前和之后的读和写指令重排序;把写缓冲区中的所有数据刷新到内存中。
备注知识:
关于CPU的锁有如下3种:
3.1 处理器自动保证基本内存操作的原子性
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
3.2 使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
3.3 使用缓存锁保证原子性
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
·CAS缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
- ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
·concurrent包的实现
由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
A线程写volatile变量,随后B线程读这个volatile变量。
A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
9、Java类加载机制 http://blog.csdn.net/love_Javc_you/article/details/38081683
·简单过程
Java程序运行的场所是内存,当在命令行下执行:
java HelloWorld命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。其中的过程就是类加载过程:
1、寻找jre目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器);
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5、最后由AppClass Loader加载HelloWorld类。
·类加载器的特点
1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。
2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.
·类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
//使用Class.forName()来加载类,默认会执行初始化块
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
10、 JDK7与JDK8中HashMap的实现 https://my.oschina.net/hosee/blog/618953
11、 Java中常用的锁分析总结 http://www.tuicool.com/articles/NnQjyq
ReentrantLock、ReentrantReadWriteLock及Sychronized简介
(a) 类继承结构
ReentrantLock类继承结构:
ReentrantReadWriteLick类继承结构:
简述:通过类的继承结构可以看出ReentrantLock 和 ReentrantReadWriteLock是拥有者两个不同类继承结构的体系,两者并无关联。
Ps:Sychronized是一个关键字
(b) 几个相关概念
什么是可重入锁 :可重入锁的概念是自己可以再次获取自己的内部锁。举个例子,比如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的(如果不可重入的锁的话,此刻会造成死锁)。说的更高深一点可重入锁是一种递归无阻塞的同步机制。
什么叫读写锁 :读写锁拆成读锁和写锁来理解。读锁可以共享,多个线程可以同时拥有读锁,但是写锁却只能只有一个线程拥有,而且获取写锁的时候其他线程都已经释放了读锁,而且该线程获取写锁之后,其他线程不能再获取读锁。简单的说就是写锁是排他锁,读锁是共享锁。
获取锁涉及到的两个概念即 公平和非公平 :公平表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。而非公平就是一种获取锁的抢占机制,和公平相对就是先来不一定先得,这个方式可能造成某些线程饥饿(一直拿不到锁)。
(c) ReentrantLock,ReentrantReadWriteLock,Sychronized用法即作用
ReentrantLock : 类ReentrantLock实现了Lock,它拥有与Sychronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断等候的一些特性。此外,它还提供了在与激烈争用情况下更佳的性能(说白了就是ReentrantLock和Sychronized差不多,线程间都是完全互斥的,一个时刻只能有一个线程获取到锁,执行被锁住的代码,但ReentrantLock相对于Sychronized提供了更加丰富的功能并且在线程调度上做了优化,JVM调度使用ReentrantLock的线程会更快)
ReentrantReadWriteLock: 类ReentrantReadWriteLock实现了ReadWirteLock接口。它和ReentrantLock是不同的两套实现,在类继承结构上并无关联。和ReentrantLock定义的互斥锁不同的是,ReentrantReadWriteLock定义了两把锁即读锁和写锁。读锁可以共享,即同一个资源可以让多个线程获取读锁。这个和ReentrantLock(或者sychronized)相比大大提高了读的性能。在需要对资源进行写入的时候在会加写锁达到互斥的目的
12、volatile与synchronized的区别 http://blog.csdn.net/fanaticism1/article/details/9966163
1)锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
2)在Java中,为了保证多线程读写数据时保证数据的一致性,可以采用两种方式:
·同步:如用synchronized关键字,或者使用锁对象.
·volatile:使用volatile关键字
用一句话概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道.
·volatile详解
首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存.而在这个过程,变量的新值对其他线程是不可见的.而volatile的作用就是使它修饰的变量的读写操作都必须在内存中进行!
·volatile与synchronized
volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.
3)因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
总结:volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。可以实现synchronized的部分效果,但当n=n+1,n++等时,volatile关键字将失效,不能起到像synchronized一样的线程同步的效果。
13、 Java中的object类有哪些方法 :
hashcode() 、equal() 、wait() 、notify() 、notifyall()、 finalize() 等等
14、 深复制与浅复制的区别
浅拷贝是指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行拷贝,有对引用指向的对象进行拷贝。
而深拷贝是指在拷贝对象时,同时会对引用指向的对象进行拷贝。
区别就在于是否对 对象中的引用变量所指向的对象进行拷贝。
15、 深入剖析Java中的装箱和拆箱 http://www.cnblogs.com/dolphin0520/p/3780005.html
在Java SE5之前,如果要生成一个数值为10的Integer对象:Integer i = new Integer(10);
而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,Integer i = 10;
这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。
拆箱 跟装箱对应,就是自动将包装器类型转换为基本数据类型:
Integer i = 10; //装箱
int n = i; //拆箱
装箱就是 :自动将基本数据类型转换为包装器类型;
拆箱就是 :自动将包装器类型转换为基本数据类型。
在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
A、 在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
B、谈谈Integer i = new Integer(xxx)和Integer i =xxx;这两种方式的区别。
1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;
2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况(注意这并不是绝对的)。
C、下面程序的输出结果是什么?
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d); //true
System.out.println(e==f); //false
System.out.println(c==(a+b)); //true
System.out.println(c.equals(a+b));//true
System.out.println(g==(a+b)); //true
System.out.println(g.equals(a+b));//false
System.out.println(g.equals(a+h));//true
}
}
先别看输出结果,读者自己想一下这段代码的输出结果是什么。这里面需要注意的是:当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。
第一个和第二个输出结果没有什么疑问。第三句由于 a+b包含了算术运算,因此会触发自动拆箱过程(会调用intValue方法),因此它们比较的是数值是否相等。而对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。同理对于后面的也是这样,不过要注意倒数第二个和最后一个输出的结果(如果数值是int类型的,装箱过程调用的是Integer.valueOf;如果是long类型的,装箱调用的Long.valueOf方法)。
17、java8的新特性:
1)并行流,流可以是顺序的也可以是并行的。顺序流的操作是在单线程上执行的,而并行流的操作是在多线程上并发执行的。并行排序所花费的时间大约是顺序排序的一半
2)lumbda表达式,匿名内部类,集合排序
3)接口的default方法和静态方法
4)map新方法 -合并map中的实体,maps不支持流。然而现在maps包括了许多新的非常有用的方法用于执行通用任务: /forEach使用consumer来对map中的每个元素进行操作,执行通用任务
18) 通过关键字::来传递方法和构造函数的引用
18、 Java动态绑定总结 http://www.cnblogs.com/lyp3314/archive/2013/01/26/2877205.html
19、 Java堆和栈详解 http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
看《深入理解Java虚拟机》更详细
栈内存主要用于存放局部变量和方法调用。局部变量指的是那些在方法中定义的基本类型变量和引用类型变量(即对象的引用变量)。当在一段代码块中定义一个变量时,Java就会在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存主要用于存放对象(在Java中,数组也是对象)。在堆中分配的内存,由JVM的自动垃圾回收器来管理。在堆中产生一个对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于该对象在堆内存中的首地址,在栈中的这个特殊的变量就是我们常说到的“对象的引用变量”。
·栈和堆的区别
Java中栈和堆的区别自然是面试中的常见问题,下面几点就是其具体的区别。
1)各司其职
最主要的区别就是栈内存是用来存储局部变量和方法调用。而堆内存是用来存储Java中的对象。引用变量指向的对象都存储在堆内存中。
2)独有还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问,是共享的。
3)异常错误
如果栈内存没有可用的空间用来存储方法调用和局部变量,那么JVM会抛出java.lang.StackOverFlowError。而如果是堆内存没有可用的空间来存储生成的对象,那么JVM会抛出java.lang.OutOfMemoryError。
4)空间大小
栈内存的存储空间要远远小于堆内存。如果你使用递归的话,那么你的栈很快就会被填满。如果递归没有及时跳出,很可能发生StackOverFlowError异常。你可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小和堆的最大值。
在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
16、 JDK 源代码研究 Hash 存储机制 http://www.ibm.com/developerworks/cn/java/j-lo-hash/
17、 JVM内存管理, 新生代, 旧生代 http://blog.sina.com.cn/s/blog_55ba8b4601014nzm.html
·JVM内存组成结构
JVM内存结构由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:
1)堆
所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,结构图如下所示:
新生代。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例旧生代。用于存放新生代中经过多次垃圾回收仍然存活的对象
2)栈
每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果
3)本地方法栈
用于支持native方法的执行,存储了每个native方法调用的状态
4)方法区
存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(PermanetGeneration)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。介绍完了JVM内存组成结构,下面我们再来看一下JVM垃圾回收机制。
·JVM内存管理和JVM垃圾回收机制
JVM垃圾回收机制
JVM分别对新生代和旧生代采用不同的垃圾回收机制
新生代的GC:
新生代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代,
用javavisualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当旧生代也满了后,就会报outofmemory的异常,如下图所示:
在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
1)串行GC
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
2)并行回收GC
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数
3)并行GC
与旧生代的并发GC配合使用
旧生代的GC:
旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。
以上各种GC机制是需要组合使用的,指定方式由下表所示:
垃圾收集算法:
· 标记清除算法:效率不高,会产生大量碎片,标记所有要回收的对象,再清除
· 复制算法:将内存划分为相等的两块,每次只使用一块,每次内存用完后,便将存活着的对象复制到另一块。效率高,实现简单;浪费一般空间
· 标记-整理算法:将所存活的对象都像一段移动
· 分代收集算法:新生代每次都有大批对象死去,只有少量存活,选用“复制算法”;老年代存活率高,采用“标记-清除”或“标记-整理”算法。
18、 图解Tomcat 与jvm 类加载机制 http://www.cnblogs.com/xing901022/p/4574961.html
JVM类加载:
当JVM运行过程中,用户需要加载某些类时,会按照下面的步骤(父类委托机制):
1 用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层。
2 最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类。
3 如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException。
19、 Java异常处理和设计 http://www.cnblogs.com/dolphin0520/p/3769804.html
Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。
unchecked exception(非检查异常),也称运行时异常(RuntimeException),比如常见的NullPointerException、IndexOutOfBoundsException。对于运行时异常,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。
checked exception(检查异常),也称非运行时异常(运行时异常以外的异常就是非运行时异常),java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于非运行时异常如果不进行捕获或者抛出声明处理,编译都不会通过。
1)Try,catch,finally
·try...catch...; try....finally......; try....catch...finally...
·在使用try..catch..finally块的时候,注意千万不要在finally块中使用return,因为finally中的return会覆盖已有的返回值。
2)throws,throw
throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
20、 B树、B-树、B+树、B*树 http://www.cnblogs.com/oldhorse/archive/2009/11/16/1604009.html
注意:B-树==B树,两者是一样的。它是一种平衡的多叉树,称为B树(或B-树、B_树)。
B-树是一种多路搜索树(并不一定是二叉的)
M为树的阶数,B-树或为空树,否则满足下列条件:
1.定义任意非叶子结点最多只有M个儿子;且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字,根节点至少一个关键字);
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[m-1],m<M+1;且K[i]< K[i+1] ;
7.非叶子结点的指针:P[1], P[2], …, P[m];其中P[1]指向关键字小于K[1]的子树,P[m]指向关键字大于K[m-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
M=3的B-树
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
B-树的特性:
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率
B+树
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
如:(M=3)
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在
非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B+的特性:
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好
是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储
(关键字)数据的数据层;
4.更适合文件索引系统;
B*树
是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;
B树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3
(代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据
复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父
结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分
数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字
(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之
间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B树分配新结点的概率比B+树要低,空间使用率更高;
总结:
B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键
字范围的子结点;
所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点
中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;
B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率
从1/2提高到2/3;
21、 死锁产生的原因和如何解决?
死锁:指多个进程因竞争共享资源而造成的一种僵局,若无外力作用,这些进程都将永远不能再 向前推进。
安全状态与不安全状态:安全状态指系统能按某种进程顺序来为每个进程分配其所需资源,直至最大需求,使每个进程都可顺利完成。若系统不存在这样一个序列, 则称系统处于不安全状态。
产生死锁的原因:(1)竞争系统资源 (2)进程的推进顺序不当
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
解决死锁的基本方法:
预防死锁:
资源一次性分配:(破坏请求和保持条件)
可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
避免死锁:
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
22、如何检测两个线程死锁,或者说找出死锁的线程
有两个容器,一个用于保存线程正在请求的锁,一个用于保存线程已经持有的锁。每次加锁之前都会做如下检测:
1)检测当前正在请求的锁是否已经被其它线程持有,如果有,则把那些线程找出来。
2)遍历第一步中返回的线程,检查自己持有的锁是否正被其中任何一个线程请求
如果第二步返回真,表示出现了死锁。
23、进程与线程的区别和联系,进程执行的方式。
进程概念
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。
线程概念
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一
进程和线程的关系
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
(3)处理机分给线程,即真正在处理机上运行的是线程。
(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
24、jvm虚拟机溢出都是发生在哪个地方?什么情况下会发生溢出?原因何在?
http://www.cnblogs.com/time-info/p/4514164.html
·内存泄露与内存溢出
内存泄露一般是代码设计存在缺陷导致的,指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但是已经无用;通过了解内存泄露的场景,可以避免不必要的内存溢出和提高自己的代码水平;
·内存泄露的几种场景:
1、长生命周期的对象持有短生命周期对象的引用
例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露;
2、修改hashset中对象的参数值,且参数是计算哈希值的字段
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法中使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露;
3、机器的连接数和关闭时间设置;长时间开启非常耗费资源的连接,也会造成内存泄漏。
内存溢出指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于Old段或者Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况;通过了解内存溢出的集中常见情况,可以在出现内存溢出的时候快速定位问题的位置,缩短解决故障的时间。
·内存溢出的几种情况:
1、堆内存溢出(OutOfMemoryError:java heap space)
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常;
在JVM规范中,堆中的内存是用来存放对象实例和数组的,如果细分,堆内存可以分为年轻代和年老代,年轻代又是包含1个Eden区和2个Survivor区;
当生成新对象时,内存的申请过程如下:
1.1:JVM先尝试在Eden区分为新对象所需的内存,如果内存大小足够,那么申请结束;
1.2:如果Eden区的内存不够新对象所需的内存,JVM会启动Minor GC,试图将Eden区中不活跃的对象释放掉,释放后若Eden区仍然不足存放新的对象,则会试图将Eden中活跃的对象放入Survivor区;
1.3:Survivor区被用来作为Eden区和Old区的中间交换区域,当Old区空间足够,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
1.4:若Old区的空间不足,那么会触发一次Mijor GC(Full GC)
1.5:Full GC后,若Survivor以及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则会出现“OutOrMermoryError”错误;
2、虚拟机栈和本地方法栈溢出
在HotSpot虚拟机中是不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,虽然-Xoss参数(设置本地方法栈)存在,实际上此参数是无效的,栈容量只由-Xss参数设置。
Java虚拟机规范中对虚拟机栈和本地方法栈定义了2个异常:
2.1 如果线程请求的栈深度大于虚拟机所允许的栈深度,会抛出StackOverflowError异常;
2.2 如果虚拟机在扩展时,无法申请到足够的内存空间,会抛出OutOfMemoryError异常;
单线程中测试,无论是栈深度大于虚拟机允许的栈深度,还是无法申请到足够的内存空间,都是抛出StackOverflowError异常;
1.使用-Xss参数减小栈内存容量,抛出StackOverflowError异常;2.定义大量的本地变量,增大此方法帧本地变量表的长度,抛出StackOverflowError异常;
如果测试不限于单线程,通过不断建立线程的方式倒是可以产生内存溢出异常;
虚拟机提供了参数来控制Java堆和方法区这两个部分内存的最大值。
剩余的内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量),程序计数器消耗内存较小。如果虚拟机进程本身消耗的内存不计算在内,那么剩余的内存就是被虚拟机栈和本地方法栈瓜分了。
每个线程分配到的栈容量越大,可以建立的线程数量自然越少,建立线程时就容易把剩下的内存耗尽;
如果是建立过多线程导致内存溢出,在不能减少线程数或者跟换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程;
3、方法区和运行时常量池溢出(OutOfMemoryError:PermGen space)
运行时常量池是方法区的一部分。
JVM规范中,方法区主要是存放类的信息、常量、静态变量等;
在JDK1.6及之前的版本中,运行时常量池分配在永久代中,可以使用-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。
在测试运行结果中会看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是PermGen space,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分;
jvm参数:-XX:PermSize=2m -XX:MaxPermSize=2m
将方法区的大小设置很低即可,在启动加载类库时就会出现内存不足的情况
方法区用于存储Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于此区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出;
如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出。
4、线程栈溢出(java.lang.StackOverflowError)
线程栈是线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。
一般线程栈溢出是由于递归太深或方法调用层级过多导致的。
发生栈溢出的错误信息为:java.lang.StackOverflowError;
为了避免内存泄漏,在编写代码的过程中可以参考下面的建议:
1、尽早释放无用对象的引用
2、使用字符串处理,避免使用String,应大量使用StringBuffer,每个String对象都得独立占用内存一块区域;
3、尽量少用静态变量,因为静态变量存在的永久代(方法区),永久代基本不参与垃圾回收;
4、避免在循环中创建对象;
5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
25、equals、==、hashcode http://blog.csdn.net/hla199106/article/details/46907725
1、==
Java中的数据类型,可分为两类:
1).基本数据类型,也称原始数据类型
byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比较的是他们的值。
2).引用类型(类、接口、数组)
当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。
对象是放在堆中的,栈中存放的是对象的引用(地址)。由此可见'=='是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。
2、equals
1)、默认情况(没有覆盖equals方法)下equals方法都是调用Object类的equals方法,而Object的equals方法主要用于判断对象的内存地址引用是不是同一个地址(是不是同一个对象)。
定义的equals与==是等效的
2)、要是类中覆盖了equals方法,那么就要根据具体的代码来确定equals方法的作用了,覆盖后一般都是通过对象的内容是否相等来判断对象是否相等。
3、 hashCode的作用
要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?
这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次.
4、hashcode和equals:
1)、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
2)、如果两个对象不equals,他们的hashcode有可能相等。
3)、如果两个对象hashcode相等,他们不一定equals。
4)、如果两个对象hashcode不相等,他们一定不equals。
5、为什么覆盖equals时总要覆盖hashCode
一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。