Java面试题

论坛 期权论坛 脚本     
已经匿名di用户   2022-7-2 21:57   2051   0
  1. synchronized与lock的区别,使用场景。看过synchronized的源码没

Lock的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的

synchronized在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。

在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,

如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断

如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

ReentrantLock

ReentrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞 争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而 ReentrantLock确还能维持常态。

ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断


Atomic
和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于 ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能 在多个Atomic之间同步。

  1. JVM自动内存管理,Minor GC与Full GC的触发机制

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的。

所有通过new创建的对象的内存都在堆中分配,堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成。

GC触发条件:Eden区满了触发Minor GC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor GC之后,就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。 新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快
  2. 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
  3. 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
  4. 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
  6. 当Old Space被放满之后,进行Full GC
  7. 从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的
  8. 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。  

      可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。

      如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

       可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

GC判断对象是否"存活"或"死去"(GC回收的对象):

1.引用计数器算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值加1;当引用失效时,计数器的值减;当该对象的计数器的值为0时,标志该对象失效。

2.跟搜索算法

基本思路:通过一系列的名为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明对象是不可用的。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

  1. JVM调优基本思路是什么

主要调优的目的:

  1. 控制GC的行为.GC是一个后台处理,但是它也是会消耗系统性能的,因此经常会根据系统运行的程序的特性来更改GC行为

  2. 控制JVM堆栈大小.一般来说,JVM在内存分配上不需要你修改,(举例)但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小.同时,还要需要控制总的JVM大小避免内存溢出

  3. 控制JVM线程的内存分配.如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果

调优总结

年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低相应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象

吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上应用。

年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

1. 并发垃圾收集信息

2. 持久代并发收集次数

3. 传统GC信息

4. 花在年轻代和年老代回收上的时间比例,减少年轻代和年老代花费的时间,一般会提高应用的效率。

吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活的对象。

较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是, 当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回 收。如果出现“碎片”,可能需要进行如下配置:

1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩。

垃圾回收的瓶颈

传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。但是它无法解决的一个问题,就是Full GC 所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几 十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小的范围内,但是这样又限制了应用本身的处理能力,同样也是不可接 受的。

分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。

为了达到实时性的要求(其实java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题,参见增量收集

JVM如何调优

观察内存释放情况、集合类检查、对象树

1. 堆信息查看

可查看堆空间大小分配(年轻代、年老代、持久代分配)

提供即时的垃圾回收功能

垃圾监控(长时间监控回收情况)

查看堆内类、对象信息:数量、类型等

对象引用情况查看

调优工具提供了堆信息查看方面的功能,我们一般可以顺利解决以下问题:

-- 年老代年轻代大小划分是否合理

-- 内存泄露

-- 垃圾回收算法设置是否合理

2. 线程监控

线程信息监控:系统线程数量

线程状态监控:各个线程都处在什么样的状态下

Dump线程详细信息:查看线程内部运行情况,死锁检查

3. 热点分析

CPU热点:检查系统哪些方法占用了大量CPU时间

内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)

这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。

4. 快照

快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题。

举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。

内存泄露检查

内存泄露是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。

内存泄露一般可以理解为系统资源(各方面的资源、堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。

内存泄露对系统危害比较大,因为它可以直接导致系统的崩溃。

需要区别一下,内存泄露和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄露是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。

年老代堆空间被沾满

异常:java.lang.OutOfMemoryError:Java heap space

说明:这是最典型的内存泄露方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再分配新空间。

如下图所示,这是非常典型的内存泄露垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点, 可以发现一条由底到高的线,这说明,随着时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄露。(下图仅 供示例,实际情况下收集数据的时间可能需要更长,几个小时或者几天,几个月)

解决:

这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄露点。

持久代被占满

异常:java.lang.OutOfMemoryError:PermGen space

说明:Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。

更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么它将会被 加载N次。因此,某些情况下,这个问题基本视为误解。当然,存在大量classLoader的大量反射类的情况其实也不多。

解决:

1. -XX:MaxPermSize=16m

2. 换用JDK。比如JRocket。

堆栈溢出

异常:java.lang.StackOverflowError

说明:这个就不多说了,一般就是递归没返回,或者循环调用造成。

线程堆栈满

异常:Fatal:Stack size too small

说明:java 中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。

解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄露的部分。

系统内存被占满

异常:java.lang.OutOfMemoryError:unable to create new native thread

说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在java堆中分配内存外,操作系统本身也 需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。

分配给java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给java虚拟机的内存越多,那么,系统总共能够产生的线程也就越 少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共能产生的线程数。

解决:

1. 重新设计系统减少线程数量。

2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能产生更多的线程。

垃圾回收的悖论

所谓“成也萧何败也萧何”。java的垃圾回收确实带来了很多好处,为开发带来了便利。但是在一些高性能、高并发的情况下,垃圾回收却成了制约java应 用的瓶颈。目前JDK的垃圾回收算法,始终无法解决垃圾回收时暂停问题,因为这个暂停严重影响了程序的相应时间,造成拥塞或堆积。这也是后续JDK增加 G1算法的一个重要原因。

当然,上面是从技术角度出发解决垃圾回收带来的问题,但是从系统设计方面我们就需要问一下:我们需要分配如此大的内存空间给应用吗?我们是否能够通过有效使用内存而不是通过扩大内存的方式来设计我们的系统呢?

我们的内存中都放了什么

内存中需要放什么呢?个人认为,内存中需要放的是你的应用需要在不久的将来再次要用到的东西。想想看,如果你在将来不用这些东西,何必放内存呢?放文件,数据库不是更好?这些东西一般包括:

1. 系统运行时业务相关的数据。比如web应用中的session,即时消息的session等。这些数据一般在一个用户访问周期或者一个使用过程中都需要存在。

2. 缓存。缓存就比较多了,你所要快速访问的都可以放这里面。其实上面的业务数据也可以理解为一种缓存。

3. 线程。

因此,我们是不是可以这么认为,如果我们不把业务数据和缓存放在JVM中,或者把他们独立出来,那么java应用使用时所需的内存将会大大减少,同时垃圾回收时间也会相应减少。

解决之道

数据库、文件系统

把所有数据都放入数据库或者文件系统,这是一种最为简单的方式。在这种方式下,java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取 都在每次请求时从数据库或文件系统中获取。也可以理解为,一次业务访问以后,所有对象都可以进行回收了。

这是一种内存使用最有效的方式,但是从应用角度来说,这种方式很低效。

内存-硬盘映射

上面的问题是因为我们使用了文件系统带来了低效。但是如果我们不是读写硬盘,而是写内存的话效率将会提高很多。

数据库和文件系统都是实实在在进行了持久化,但是当我们并不需要这样持久化的时候,我们可以做一些变通---把内存当硬盘使。

内存---硬盘映射很好很强大,既用了缓存又对java应用的内存使用没有影响。java应用还是java应用,他只知道读写的还是文件,但是实际上是内存。

这种方式兼得的java应用与缓存两方面的好处。memcached的广泛使用也正是这一类的代表。

同一个机器部署多个JVM

这也是一种很好的方式,可以分为纵拆和横拆。纵拆可以理解为把java应用划分为不同模块,各个模块使用一个独立的java进程。而横拆则是同样功能的应用部署多个JVM。

通过部署多个JVM,可以把每个JVM的内存控制在一个垃圾回收可以忍受的范围内即可。但是这相当于进行了分布式的处理,其额外带来的复杂性也是需要评估的。另外,也有支持分布式的这种JVM可以考虑。

线程分配

java的阻塞式线程模型基本上可以抛弃了,目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增长,而NIO 则可以转换成为常数线程。因此,对于服务端的应用而言,NIO还是唯一选择。不过,JDK7中为我们来带的AIO是否能让人眼前一亮呢?期待中。

其他JDK

本文说的都是Sun的JDK,目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很 多,不过Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面,也具有优势,其可设置垃圾回收的最大暂停时间也是很吸引人的。不过,系统Sun的G1实 现以后,在这方面会有一个质的飞跃。

http://blog.csdn.net/jediael_lu/article/details/44256825

  1. 如何设计存储海量数据的存储系统

  1. 缓存的实现原理,设计缓存要注意什么

所谓缓存,就是将程序或系统经常要调用的对象存在内存中,以便其使用时可以快速调用,不必再去创建新的重复的实例。这样做可以减少系统开销,提高系统效率。

如果某些资源或者数据会被频繁的使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都从数据库或者硬盘上去获取,速度会很慢,会造成性能问题。
一个简单的解决方法就是:把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取 它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了。从而节省大量的时间,当然,缓存是一种典型的空间换时间的方案。 缓存从本质上来说,是一种牺牲数据的实时性以换取效率的方式。

在Java中最常见的一种实现缓存的方式就是使用Map, 基本的步骤是:
先到缓存里面查找,看看是否存在需要使用的数据
如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置回到缓存中,以备下次使用
如果找到了相应的数据,或者是创建了相应的数据,那就直接使用这个数

缓存主要可分为二大类:

一、通过文件缓存,顾名思义文件缓存是指把数据存储在磁盘上,不管你是以XML格式,序列化文件DAT格式还是其它文件格式;

二、内存缓存,也就是实现一个类中静态Map,对这个Map进行常规的增删查.

缓存技术虽好,但缓存技术在Web应用中依然存在相应的问题,那就是缓存数据存放时间问题,在Web应用中,

比如说登录人员的相关权限的问题,大多数是放在session中进行缓存的,但session超时时,就会被清除掉;当然了,

如果不是在Web应用中,就得自己来控制了,另外就算是在Web应用中,也不一定非要缓存到session超时才清楚,

总之,控制缓存数据应该被缓存多长时间,是实现高效缓存的一个问题点。

另一个问题,缓存数据和真实数据的同步问题,缓存中的数据在运行期间会发生变化,那么缓存中的数据就应该和

数据库中的数据同步,以保持一致,否则就会出错,如何合理的同步数据,也是实现高效缓存的另一个问题点。

最后,就是缓存的多线程并发的控制,对于缓存的数据,有些操作是从缓存中取数据,有些操作时想缓存中添加数

据,有些操作是在清除过期的缓存数据等等,而在一个多线程的环境下,如何合理的进行对缓存进行并发控制,也是

实现缓存的一个问题点。

1. 缓存的数据的替换策略
2. 保证缓存数据与实际数据的一致性。
3. 如何有效的处理高并发的用户请求。

http://www.cnblogs.com/lei2007/p/3837288.html

http://blog.csdn.net/zhaozhenzuo/article/details/45842571

  1. 淘宝热门商品信息在JVM哪个内存区域

栈是运行时的单位,而堆是存储的单元

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放,放在哪儿

在java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享 的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等等,而堆只负责存储对象信息。

为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。

2.堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。

3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力,而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。

4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了 改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我 们在编写对象的时候,其实就是编写了数据结构,也编写了处理数据的逻辑。

  1. 操作系统的页式存储

操作系统原理:页式存储管理

内存分区存储管理的一个特点是连续性,每个程序都分有一片连续的内存区域。这种连续性导致碎片问题,包括

固定分区中的内碎片和可变分区中的外碎片。为了解决这些问题,人们又提出了“页式存储管理方案”。它的基本出发点

是打破存储分配的连续性,使一个程序的逻辑地址空间可以分布在若干个离散的内存块上,从而达到充分利用内存,提高

内存利用率的作用。

页式存储管理的基本思路是:一方面,把物理内存划分为许多个固定大小的内存块,称为物理页面,或页框;另一方面,把逻辑

地址空间也划成大小相同的块,称为逻辑页面,页面的大小要求是2^n.一般在512个字节到8KB之间。当一个用户程序被装入内存时,不是以

整个程序为单位,把它放在一整块连续的区域,而是以页面为单位来进行分配的。对于一个大小为N个页面的程序,需要有N个空闲的物理页面

把它装进去,当然,这些物理页面不一定是连续的。

在实现页式存储管理的时候需要解决以下几个问题:

1、数据结构:用于存储管理的数据结构是什么?

2、内存的分配和回收:当一个任务到来时,如何给他分配内存空间?当一个任务运行结束后,如何来回收它所占用的内存空间?

3、地址映射:当一个任务被加载到内存后,它被打散地存储在若干个不连续的物理页面当中。在这种情况下,如何把程序中使用的逻辑地址转换为内存访问时的物理地址,以确保它能正确的运行?

(1)数据结构。

在页式存储管理中,最主要的数据结构有两个。

1、页表(page table):页表给出了任务的逻辑页面号与内存中的物理页面号之间的对应关系。

2、物理页面表:用来描述内存空间中,各个物理页面的使用分配状况。在具体实现上,可以采用位示图或空闲页面链表等方法。

(2)地址映射

地址映射的基本思路如下:

1、逻辑地址分析:对于给定的一个逻辑地址,找到它所在的逻辑页面,以及它在页面内的偏移地址。

2、页表查找:根据逻辑页面号,从页表中找到它所对应的物理页面号。

3:、物理地址合成:根据物理页面号及页内偏移地址,确定最终的物理地址。

a、逻辑地址分析:

逻辑页面号=逻辑地址/页面大小

页内偏移量=逻辑地址%页面大小

b、页表查找

对于给定的一个逻辑地址,如果我们已经知道了它的逻辑页面号,就可以去查找页表,从中找到相应的物理页面号。

在具体实现上,页表通常保存在内核的地址空间中,因为它是操作系统的一个数据结构。另外,为了能够访问页表的内容,在硬件上要增加一对寄存器:

一个是页表基地址寄存器,用来指向页表的起始地址;另一个是页表长度寄存器,用来指示页表的大小,即对于当前任务,它总共包含有多少个页面。操作系统在进行任务切换的时候,会去更新这两个寄存器当中的内容。

C、物理地址的合成。

假设在程序的运行过程中,需要去访问某个内存单元,所以就给出了这个内存单元的逻辑地址。然后得出逻辑页面号和页内偏移地址。页表基地址寄存器当中,存放的是当前任务的页表首地址。这个地址加上逻辑页面号就找到了响应的页表项,里面存放的是这个逻辑页面对应的物理页面号。这个页面号与页内偏移地址进行组合,从而得到最终的物理地址,然后用这个物理地址去访问。

有一个很大的问题:当程序在运行时,如果需要访问某个内存单元,如去读写内存当中的一个数据,或是去内存取一条指令,在此情况下,需要访问两次内存:访问页表和真正访问数据或指令。这使得访问效率只有50%。

为了解决这个问题,人们引入了“快表”的概念。人们在MMU中增加一个特殊的快速查找硬件---TLB(Translation Lookaside Buffer),或者叫关联存储器,用来存放那些最常去的页表项。可以把逻辑页面号直接映射为相应的物理页面号,这样就不用访问内存了。

4、优缺点:

优点:1、没有外碎片。程序不必连续存放。便于管理。

缺点:程序必须全部装入内存,才可以运行。操作系统必须为每一个任务都维护一张页表,开销比较大,简单的页表结构已经

不能满足要求,必须设计出更复杂的结构。如多级页表结构、哈希页表结构、反置页表。

内部碎片:

内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。

为了有效的利用内存,使内存产生更少的碎片,要对内存分页,内存以页为单位来使用,最后一页往往装不满,于是形成了内部碎片。

由于被装入的数据块小于分区大小,从而导致分区内部有空间浪费,这种现象成为内部碎片。

外部碎片:

外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。

外部碎片是出于任何已分配区域或页面外部的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。

分区外的存储空间会出现很多不能使用的碎片。

为了共享要分段,在段的换入换出时形成外部碎片,比如5K的段换出后,有一个4k的段进来放到原来5k的地方,于是形成1k的外部碎片。

固定分区、简单分页存在内部碎片,简单分段存在外部碎片。

外部碎片,是由于大量信息由于先后写入、置换、删除而形成的空间碎片。为了便于理解,我们将信息比作货物,将存储空间比作仓库来举例子。假设,我们有编号 为1、2、3、4、5、6的6间仓库库房,前天送来了一大宗货,依次装入了1、2、3、4、5号仓库,昨天又因故将4号库房的货物运走了,那么数值上说我 们还有两间空仓库的空间,但是如果这时候送来两间仓库容量的货物但要求必须连续存放的话,我们实际上是装不下的。这时的4、6号仓库,就成为一种空间的碎 片。由于这样的原因形成的空间碎片,我们称之为外部碎片。从上面的例子我们可以理解,外部碎片是可以通过一些措施来改善或者解决的。对于在硬盘上的外部碎 片,我们通常用磁盘碎片整理来解决,对应上面的例子,就是将5号仓库的货物及时移动到新腾出的4号仓库,这样,1-4号仓库都是满的,而5、6号仓库则形 成了有效的、连续的空间,能够适应新的应用要求了;对于内存中的外部碎片,我们内存管理中常用的页面管理形式,就是为了解决这个问题的。这里就不详述了。
内部碎片,是由于存量信息容量与最小存储空间单位不完全相符而造成的空间碎片。还是沿用上面的例子,这次我们的6间仓库目前都是空置的,但是假设我们管理 仓库的最小空间单位是间,今天运来了容量为2.5间仓库的货物,那也要占用我们1-3号3间仓库,尽管3号仓库还闲置着一半的空间,但是这半间仓库已经不 能再利用了(因为是以间为最小单位么);这时,我们的仓库中就形成了半间仓库的空间碎片,仓库的有效容量只剩下3间仓库了。

  1. volatile关键字如何保证内存可见性

volatile的第一条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:

  • 线程对变量进行修改之后,要立刻回写到主内存。
  • 线程对变量读取的时候,要从主内存中读,而不是缓存。
  • volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

为什么volatile不能保证原子性?

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉 及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和 向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此 在CPU里面就有了高速缓存。

  也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

1

i = i + 1;

  当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

  这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程 运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

  比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

  可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

  最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

  也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

大多数的处理器都支持内存屏障的指令。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

  1. happen-before原则

不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

下面我们来解释一下前4条规则:

  对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注 意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序 代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程 中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确 性。

  第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

  第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  第四条规则实际上就是体现happens-before原则具备传递性。

//x、y为非volatile变量

//flag为volatile变量

x = 2; //语句1

y = 0; //语句2

flag = true; //语句3

x = 4; //语句4

y = -1; //语句5

  由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

参考:http://www.cnblogs.com/dolphin0520/p/3920373.html

  1. Lucene全文搜索的原理

Lucene是一个高效的,基于Java的全文检索库。全文检索的基本思路,也即将非结构化数据中的一部分信息提取 出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信 息,我们称之索引。索引:已知文档中查找包含哪些字符串;反向索引:已知字符串查找在哪些文档中包含。

我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
当然有的地方还会提到第三种,半结构化数据,如XML,HTML 等,当根据需要可按结构化
数据来处理,也可抽取出纯文本按非结构化数据来处理。
非结构化数据又一种叫法叫全文数据。
按照数据的分类,搜索也分为两种:
对结构化数据的搜索:如对数据库的搜索,用 SQL语句。再如对元数据的搜索,如利用
windows 搜索对文件名,类型,修改时间进行搜索等。
对非结构化数据的搜索:如利用 windows 的搜索也可以搜索文件内容,Linux 下的 grep
命令,再如用Google 和百度可以搜索大量内容数据。

http://blog.csdn.net/huangxiangec/article/details/8610472

  1. 说说Java锁有哪些种类,以及区别

http://ifeve.com/java_lock_see4/

  1. 如何保证内存可见性

如果线程A对共享变量X进行了修改,但是线程A没有及时把更新后的值刷入到主内存中,而此时线程B从主内存读取共享变量X的值,所以X的值是原始值,那么我们就说对于线程B来讲,共享变量X的更改对线程B是不可见的。

JMM关于synchronized的两条规定:

1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

这样,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,并能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。 
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

  1. Http请求的过程与原理

一次HTTP操作称为一个事务,其工作整个过程如下:

1 ) 、地址解析,

如用客户端浏览器请求这个页面:http://localhost.com:8080/index.htm

从中分解出协议名、主机名、端口、对象路径等部分,对于我们的这个地址,解析得到的结果如下:
协议名:http
主机名:localhost.com
端口:8080
对象路径:/index.htm

在这一步,需要域名系统DNS解析域名localhost.com,得主机的IP地址。


2)、封装HTTP请求数据包

把以上部分结合本机自己的信息,封装成一个HTTP请求数据包


3)封装成TCP包,建立TCP连接(TCP的三次握手)

在 HTTP工作开始之前,客户机(Web浏览器)首先要通过网络与服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建 Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则, 只有低层协议建立之后才能,才能进行更层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。这里是8080端口

4)客户机发送请求命令

建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可内容。

5)服务器响应

服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。

实体消息是服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据

6)服务器关闭TCP连接

一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码

Connection:keep-alive

TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

建立TCP连接

在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建 立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP /IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能,才能进行更层协议的连接,因此,首先要建立TCP连接,一般 TCP连接的端口号是80

(2) Web浏览器向Web服务器发送请求命令

一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令

例如:GET/sample/hello.jsp HTTP/1.1

(3) Web浏览器发送请求头信息

浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。

(4) Web服务器应答

客户机向服务器发出请求后,服务器会客户机回送应答,

HTTP/1.1 200 OK

应答的第一部分是协议的版本号和应答状态码

(5) Web服务器发送应答头信息

正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。

(6) Web服务器向浏览器发送数据

Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据

(7) Web服务器关闭TCP连接

一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码

Connection:keep-alive

TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

客户机发起一次请求的时候:

客户机会将请求封装成http数据包-->封装成Tcp数据包-->封装成Ip数据包--->封装成数据帧--->硬件将帧数据转换成bit流(二进制数据)-->最后通过物理硬件(网卡芯片)发送到指定地点。

服务器硬件首先收到bit流....... 然后转换成ip数据包。于是通过ip协议解析Ip数据包,然后又发现里面是tcp数据包,就通过tcp协议解析Tcp数据包,接着发现是http数据包通过http协议再解析http数据包得到数据。

http://blog.csdn.net/witsmakemen/article/details/8994963

  1. TCP连接的特点

TCP服务特点:

  1、面向连接的传输;

  2、端到端的通信;

  3、高可靠性,确保传输数据的正确性,不出现丢失或乱序;

  4、全双工方式传输;

  5、采用字节流方式,即以字节为单位传输字节序列;

  6、紧急数据传送功能;

提供IP环境下的数据可靠传输(一台计算机发出的字节流会无差错的发往网络上的其他计算机,而且计算机A接收数据包的时候,也会向计算机B回发数据 包,这也会产生部分通信量),有效流控,全双工操作(数据在两个方向上能同时传递),多路复用服务,是面向连接,端到端的传输;

  2、面向连接:正式通信前必须要与对方建立连接。事先为所发送的数据开辟出连接好的通道,然后再进行数据发送,像打电话。

TCP协议用于控制数据段是否需要重传的依据是设立重发定时器。在发送一个数据段的同时启动一个重发定时器,如果在定时器超时前收到确认 (Acknowlegement)就关闭该定时器,如果定时器超时前没有收到确认,则重传该数据段。在选择重发时间的过程中,TCP必须具有自适应性。它 需要根据互联网当时的通信情况,给出合适的数据重发。

  TCP协议提供的是可靠的、面向连接的传输控制协议,即在传输数据前要先建立逻辑连接,然后再传输数据,最后释放连接3个过程。TCP提供端到端、全双工通信;采用字节流方式,如果字节流太长,将其分段;提供紧急数据传送功能。

  1. TCP连接如何保证安全可靠的

在TCP的连接中,数据流必须以正确的顺序送达对方。TCP的可靠性是通过顺序编号和确认(ACK)来实现的。TCP在开始传送一个段时,为准备重传而首 先将该段插入到发送队列之中,同时启动时钟。其后,如果收到了接受端对该段的ACK信息,就将该段从队列中删去。如果在时钟规定的时间内,ACK未返回, 那么就从发送队列中再次送出这个段。TCP在协议中就对数据可靠传输做了保障,握手与断开都需要通讯双方确认,数据传输也需要双方确认成功,在协议中还规 定了:分包、重组、重传等规则;而UDP主要是面向不可靠连接的,不能保证数据正确到达目的地。 另外,TCP是面向流的,发送和接收对于此协议来说,没有什么头和尾,全部顺序投递;而UDP是面向包的,每次接收与发送都是一个数据块。这样在编程时需 要注意程序应提供不同的处理模型。在进行传输之前,首先发送请求信号,目的端接收信号后,回复信息,之后建立连接开始传输数据,俗称TCP三次握手。

TCP如何保证可靠性?

TCP如何保证可靠性?

TCP如何保证可靠性?

  1. 为什么TCP连接需要三次握手,两次不可以吗,为什么

TCP三次握手过程
1 主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B ,向主机B 请求建立连接,通过这个数据段,
主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.
2 主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:
我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我
3 主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了

这样3次握手就完成了,主机A和主机B 就可以传输数据了.
3次握手的特点
没有应用层的数据
SYN这个标志位只有在TCP建产连接时才会被置1
握手完成后SYN标志位被置0
三次对话的目的是使数据包的发送和接收同步

TCP建立连接要进行3次握手,而断开连接要进行4次

1 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求
2 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1
3 由B 端再提出反方向的关闭请求,将FIN置1
4 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.

名词解释
ACK TCP报头的控制位之一,对数据进行确认.确认由目的端发出,用它来告诉发送端这个序列号之前的数据段
都收到了.比如,确认号为X,则表示前X-1个数据段都收到了,只有当ACK=1时,确认号才有效,当ACK=0时,确认号无效,这时会要求重传数据,保证数据的完整性.

SYN 同步序列号,TCP建立连接时将这个位置1
FIN 发送端完成发送任务位,当TCP完成数据传输需要断开时,提出断开连接的一方将这位置1

  1. AOP的原理

在传统的编写业务逻辑处理代码时,我们通常会习惯性地做几件事情: 日志记录、事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码。真正用于核心业务逻 辑处理才那么几行,如图6-4所示。方法复方法,类复类,倘若到了项目的尾声,突然决定在权限控 制上需要进行大的变动时,成千上万个方法又得一一"登门拜访",痛苦"雪上加霜"。

如果能把图6-4中众多方法中的所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码的话,最起码可以解决两个问题:

Java EE程序员在编写具体的业务逻辑处理方法时,只需关心核心的业务逻辑处理,既提高了工作效率,又使代码变更简洁优雅。

在日后的维护中由于业务逻辑代码与共有代码分开存放,而且共有代码是集中存放的,因此使维护工作变得简单轻松。

面向切面编程AOP技术就是为解决这个问题而诞生的,切面就是横切面,如图6-5所示,代表的是一个普遍存在的共有功能,例如,日志切面、权限切面及事务切面等。

下面我们以用户管理业务逻辑组件UserService的AOP实现过程(见图6-6)为例,深度剖析一下AOP技术的实现原理。AOP技术是建立在Java语 言的反射机制与动态代理机制之上的。业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按Java EE程序员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能同时得以执行。从原理上讲,调用者直接调用的其实是AOP容器动 态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。

  1. 动态代理与cglib实现的区别

DK动态代理只能对实现了接口的类生成代理,而不能针对类
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
因为是继承,所以该类或方法最好不要声明成final 与静态代理类对照的是动态代理类,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。 动态代理:未知需要代理的类,也未知需要代理的方法,都是调用方告诉需要 代理XX类YY方法 , 静态代理的缺点:如果接口添加一个新的方法,所有的实现类和代理类都要做这个实现,这就增加了代码的复杂度,而动态代理就可以解决这个问题。 态代理和静态代理相比较,最大的好处就是接口中声明的所有的方法都被转移到一个集中的方法中去处理,这样在接口中声明的方法比较多的情况下我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

  1. 说说代理的实现原理

通过将一个代理对象交给调用者,使得调用者不能直接使用相应的功能模块,所有的调用被传递给代理对象,代理对象负责对真实模块完成调用,在调用者与被调用 者之间建立了一个隔离带,我们可以使用这个隔离带进行权限检查、对象的延迟加载等功能的实现。

代理模式角色分为 4 种:

  1. 主题接口:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

  2. 真实主题:真正实现业务逻辑的类;

  3. 代理类:用来代理和封装真实主题;

  4. Main:客户端,使用代理类和主题接口完成一些工作。

http://www.cnblogs.com/silverLee/archive/2010/02/05/1664577.html

http://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/

  1. 说说Ioc容器的加载过程

http://www.cnblogs.com/ITtangtang/p/3978349.html

  1. 字节码的编译过程

http://blog.csdn.net/owen_william/article/details/50988046

http://www.importnew.com/13107.html

http://yhjhappy234.blog.163.com/blog/static/3163283220122935349772/

  1. 说一下你对哪个项目比较熟悉
  2. 为什么做这个项目
  3. 项目采用了什么架构,数据库如何设计的

比如说你公司开发是用struts+spring+hibernate,你就可以说ssh架构模式;
说到架构,主要还是应用的框架,什么jsf, struts2...

比如web层采用struts+tomcat实现,
中间层采用无状态会话Bean+DAO+helper类,
数据库层的操作是自己写的通用类实现等等。
分别业务和技术架构两方面来说明一下

业务的话先介绍总体功能,再介绍自己比较熟悉的功能。 技术则是主要的技术框架,使用了什么开源软件,有什么特殊和创新的部分。 如果是很偏的技术则只要略略提提就好(可能考官听不懂)

  1. 数据库有哪些表,为什么有这些表
  2. 主要有哪些核心模块,模块之间如何通信的
  3. session放在哪里

Session代表服务器与浏览器的一次会话过程,这个过程是连续的,也可以时断时续的。在Servlet中,session指的是HttpSession类的对象 ,放在服务器端的内存中 , 直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建 , 一个会话只能有一个session对象 , 对于多标签的浏览器(比如360浏览器)来说,在一个浏览器窗口中,多个标签同时访问一个页面,session是一个。对于多个浏览器窗口之间,同时或者 相隔很短时间访问一个页面,session是多个的,和浏览器的进程有关。对于一个同一个浏览器窗口,直接录入url访问同一应用的不同资 源,session是一样的。

  1. 如何保存会话状态,有哪些方式、区别如何

会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。

每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,服务器要想办法为每个用户保存这些数据。

例如:多个用户点击超链接通过一个servlet各自购买了一个商品,服务器应该想办法把每一个用户购买的商品保存在各自的地方,以便于这些用户点结帐servlet时,结帐servlet可以得到用户各自购买的商品为用户结帐。

这些数据都是需要保存下来的,这时就用到了Cookie技术。

我们知道web网站在客户端存储数据有三种形式:1. Cookie 2. hidden(隐藏域) 3.QueryString 其中viewstate什么的都是通过第二种方式隐藏域存储滴。

客户端存储数据有三种形式,那服务器端有几种呢? 嘿嘿 服务器端有:1. Session 2. Application 3. database 4. caching(缓存) 其中session用的较多,当然数据库是必须的。

Cookice在客户端有两种保存形式:(1)保存在硬盘上(设置了cookice的失效时间的情况下) (2)保存在内存中(在没有设置cookice的失效时间的情况下)

浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。

如果创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie(即存储在浏览器的内存中),用户退出浏览器之后即被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。

  1. 分布式session如何管理,你有哪些方案

Session Replication 方式管理 (即session复制)

简介:将一台机器上的Session数据广播复制到集群中其余机器上

使用场景:机器较少,网络流量较小

优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问

缺点:广播式复制到其余机器有一定廷时,带来一定网络开销

二、Session Sticky 方式管理

简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上

使用场景:机器数适中、对稳定性要求不是非常苛刻

优点:实现简单、配置方便、没有额外网络开销

缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障

三、缓存集中式管理

简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息

使用场景:集群中机器数多、网络环境复杂

优点:可靠性好

缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入

基于cookie的session共享

这种方案也在大型互联网中普遍使用,将用户的session加密序列化后以cookie的方式保存在网站根域名下(比如taobao.com),当 用户访问所有二级域名站点式,浏览器会传递所有匹配的根域名的cookie信息,这样实现了用户cookie化session的多服务共享。 此方案能够节省大量服务器资源,缺点是存储的信息长度受到http协议限制;cookie的信息还需要做加密解密; 请求任何资源时都会将cookie附加到http头上传到服务器,占用了一定带宽。

基于redis存储session时,如何实现动态扩容。 redis目前并没有内置高可用集群,很多客户端代理基于一致性hash算法能够实现分布式存储,但是扩容并不方便(需要成倍扩容),目前我们采用了淘宝 fourinone中session方案的思想:

a. 用于存储session的redis集群有多个redis节点

b. proxy记录了每个节点加入到集群的时间,并按照时间顺序对节点进行了编号(0—n)

c. session key的生成算法方面,需要在session key中包含生成时的当前日期(可考虑细化到小时还是分秒),在session key生成后,再进行取模运算 hash(sesionKey)%redisHost_count, 根据取模结果决定当前session值存储到落到哪个节点上存储。

d. 在通过sessionkey获取session信息时,根据当前sessionKey取出时间部分,再根据取出的时间与redis集群中所有的节点的添加 时间进行比较,筛选出所有addtime<sessionkey_time的节点,再进行c中的取模计算,由于即使这期间进行了扩容,由于进行了时 间匹配,redisHost_count也不会发生变化,所以取模结果和存储此session时一样,还会落到当时存储这个session的节点上,在那 个节点能够得到此session的值。

  1. 说说二分搜索的过程
检索也就是查找一般来说就两种方式,顺序查找和对分查找(也叫对分检索)顺序查找对原始数据无要求,但是对分查找必须要求待查找数据有序,升降都行,我们就以升序为例,我设定4个变量p,q 和T以及X,p是你要查找范围的起点位置,q是你要查找范围的终点位置,T是这个范围的中间位置,X是待查找的数,要求任何时刻p<=q,否则这样的范围不存在,也就是你要找的数X找不到。
假定待查找数都是在数组A里面
当你知道了起点位置p和终点位置q你必然根据这两者能确定中间位置T,(T=int((p+q)/2)因为p q 之和可能是奇数,所以我们习惯上取其一半和的整数部分)而中间位置所指向的数为A(T),如果A(T)=X,则找到了输出位置T,如果不相等,则要么A(T)<X,要么A(T)>X,若小于则X如果存在,必然在范围p和q(q=T-1)之间;若大于则X如果存在,必然在范围p(q=T+1)和q之间,这样你的查找范围就变了,则根据新的查找范围,你能确定新的中间位置T,重复上面的步骤,就会找到,或者范围变化使得p>q,待查找数X不存在。

优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表

算法时间复杂度:假设其数组长度为n,其算法复杂度为o(log(n))

空间复杂度:O(1)

总共有n个元素,

渐渐跟下去就是n,n/2,n/4,....n/2^k,其中k就是循环的次数

由于你n/2^k取整后>=1

即令n/2^k=1

可得k=log2n,(是以2为底,n的对数)

所以时间复杂度可以表示O()=O(logn)

伪代码:

BinarySearch(max,min,des)
mid=(max+min)/2
while(min<=max)
mid=(min+max)/2
if mid=des then
return mid
elseif mid >des then
max=mid-1
else
min=mid+1
return max

  1. 说一下快排的过程,写一下伪代码

它的基本思想是:通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按次方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

快速排序算法的时间复杂度,有时候是O(nlgn), 有时候就是O(n2), 空间复杂度O(1)。 但需要注意递归栈上需要花费最少logn 最多n的空间。 在理想状况下,每一次都将待排序数组划分成等长两个部分,则需要logn次划分 。

假设要排序的数组是A[1]……A[N],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一躺快速排序。一趟快速排序的算法是:

1)、设置两个变量I、J,排序开始的时候I:=1,J:=N;

2)以第一个数组元素作为关键数据,赋值给X,即X:=A[1];

3)、从J开始向前搜索,即由后开始向前搜索(J:=J-1),找到第一个小于X的值,两者交换;

4)、从I开始向后搜索,即由前开始向后搜索(I:=I+1),找到第一个大于X的值,两者交换;

5)、重复第3、4步,直到I=J;

例如:待排序的数组A的值分别是:(初始关键数据X:=49)

A[1] A[2] A[3] A[4] A[5] A[6] A[7]:

49 38 65 97 76 13 27

进行第一次交换后: 27 38 65 97 76 13 49

( 按照算法的第三步从后面开始找)

进行第二次交换后: 27 38 49 97 76 13 65

( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时I:=3 )

进行第三次交换后: 27 38 13 97 76 49 65

( 按照算法的第五步将又一次执行算法的第三步从后开始找)

进行第四次交换后: 27 38 13 49 76 97 65

( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时J:=4 )

此时再执行第三步的时候就发现I=J,从而结束一躺快速排序,那么经过一躺快速排序之后的结果是:27 38 13 49 76 97 65,即所以大于49的数全部在49的后面,所以小于49的数全部在49的前面。

  1. public int getMiddle(Integer[] list, int low, int high) {
  2. int tmp = list[low]; //数组的第一个作为中轴
  3. while (low < high) {
  4. while (low < high && list[high] > tmp) {
  5. high--;
  6. }
  7. list[low] = list[high]; //比中轴小的记录移到低端
  8. while (low < high && list[low] < tmp) {
  9. low++;
  10. }
  11. list[high] = list[low]; //比中轴大的记录移到高端
  12. }
  13. list[low] = tmp; //中轴记录到尾
  14. return low; //返回中轴的位置
  15. }
  1. public void _quickSort(Integer[] list, int low, int high) {
  2. if (low < high) {
  3. int middle = getMiddle(list, low, high); //将list数组进行一分为二
  4. _quickSort(list, low, middle - 1); //对低字表进行递归排序
  5. _quickSort(list, middle + 1, high); //对高字表进行递归排序
  6. }
  7. }

考虑如下极端情况,

T[n] = T[n-1] + T[1] + O(n),

问题来了,这一次的划分白玩了,划分之后一边是一个,一边是n-1个,这种极端情况的时间复杂度就是O(n2).

  1. 了解哪些设计模式,举例说说在jdk源码哪些用到了你说的设计模式

http://blog.csdn.net/gtuu0123/article/details/6114197

以下讲解基于JDK8.0

在JDK中最能体现迭代器模式的地方莫过于JDK中的容器类了,首先有一个Iterator接口,该接口包含了迭代过程中需要用到的几个方法,最重要的两个方法是hasNext()和next();简化之后的代码如下所示:

[java] view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. public interface Iterator<E> {
  2. boolean hasNext();
  3. E next();
  4. }

以ArrayList为例,看一下它的继承层次:ArrayList实现了List接口,List接口继承了Collection接 口,Collection接口继承了Iterable接口,Iterable接口中包含了一个iterator()方法,因此到ArrayList中就需 要实现该iterator方法,该方法的实现很简单,就是返回一个实现了Iterator接口的迭代器实例。ArrayList中迭代器的实现采用的是内 部类的形式,由于内部类可以直接访问外部类的成员变量,所以该迭代器内部类可以很方便地实现Iterator接口中的hasNext()和next()方 法,ArrayList中的迭代器内部类名字是Itr,简化代码如下所示:

[java] view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. private class Itr implements Iterator<E> {
  2. int cursor; // index of next element to return
  3. public boolean hasNext() {
  4. return cursor != size;
  5. }
  6. public E next() {
  7. Object[] elementData = ArrayList.this.elementData;
  8. return (E) elementData[cursor++];
  9. }
  10. }

这是ArrayList中的一个内部类,该类实现了Iterator接口,在实现hasNext()和next()方法的时候可以很方便地访问ArrayList的成员变量size和elementData数组。

ArrayList还有一个public成员方法iterator()该方法就可以直接返回一个该内部类迭代器的实例,其代码如下

[java] view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. public Iterator<E> iterator() {
  2. return new Itr();
  3. }

得到迭代器实例之后就可以用该迭代器实例对集合进行迭代了。代码如下所示:

[java] view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. ArrayList list = new ArrayList();
  2. for(int i=0; i<5; i++) {
  3. list.add(i);
  4. }
  5. Iterator iter = list.iterator();
  6. while(iter.hasNext()) {
  7. System.out.println(iter.next());
  8. }

迭代器模式让我们在遍历集合元素的时候无需了解集合内部的实现形式,比如说有另外一种集合类LinkedList,它的底层是用链表实现的,只要它也同样提供一个返回实现了Iterator接口的迭代器实例的方法,我们就可以用统一的方式对集合进行遍历。

一、jdk源码中的设计模式-单例模式

我们先看java.lang包下的class Runtime

复制代码

 1 public class Runtime {
 2 
 3     private Runtime() {}//私有的构造方法
 4 
 5     //Runtime类的私有静态实例
 6     private static Runtime currentRuntime = new Runtime();
 7 
 8     //获得静态实例的唯一方法
 9     public static Runtime getRuntime() {
10         return currentRuntime;
11     }
12 }

复制代码

解释:

(1)存在私有的构造函数,不能通过new来得到类的实例。

(2)私有的静态实例,一个class只能对应唯一的对象。

(3)只能通过:类.get方法获得这个唯一的对象实例。

我们可以通过如下代码验证

Runtime r1 = Runtime.getRuntime();
Runtime r2 = Runtime.getRuntime();
//“==”为地址判断,为true表示:r1与r2指向同一对象。
System.out.println(r1 == r2);

结果显然为:true

使用原型模式的意图是用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

Prototype 原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一 个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

原型模式适用于当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。原型 模式能够达到:1、运行时刻增加和删除产品;2、改变值以指定新对象;3、改变结构以指定新对象;4、减少子类的构造;5、用类动态配置应用的效果。

Prototype原型模式的类图如下:

二 、jdk中的Prototype原型模式

jdk中实现Prototype原型模式的类有很多,应该说,所有实现了cloneable接口的类都实现了Prototype原型模式。

java语言的设计者一开始就帮我们设计好了实现Prototype原型模式的接口,在所有类的基类Object中就有一个名为clone的方法,当clone方法被正确实现后,创建一个对象的方式可以是clone现有的对象,并修改clone后的对象来进行创建的操作,可见原型模式是多么重要。

使用原型模式的又一个例子是spring,spring设置bean的scope属性时,可以选择singleton或者prototype,其中前者使用的是单例模式,后者使用的是原型模式

http://blog.csdn.net/wutongyu0123wutongyu/article/details/50994575

  1. 项目中某个比较重要的点是如何实现的(需要深入技术的原理)
    遇到的最大困难是什么(有哪些),你怎么解决的?
    如果需要扩展某个功能,如何降低系统的耦合度
    如果针对某个功能进行优化,你会怎么设计和优化
  2. 有那么多人使用支付宝,这些数据如果给你存储,你会怎么设计呢

http://wenku.baidu.com/link?url=ADI6Wcy5f0379m20kdIbc7CIZjxMZCkRGTQiUfYETvMSjBNYTjOrBjVQDWDonrFfKSsdAlILB6tuEnNvLGpEQVPGZ1vHjeiXy16tE9aDfRG

转载于:https://my.oschina.net/u/2822116/blog/735643

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP