类的生命周期:加载、(验证、准备、解析)链接、初始化、使用和卸载七个阶段
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也成为动态绑定或晚期绑定)。
加载阶段:通过类的全限定名取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的Class对象,作为对方法区这些数据的访问入口
验证阶段:文件格式是以0xCAFEBABE开头,版本号是否合理,元数据,字节码符号引用的验证。
准备阶段:类变量(static变量)赋初值(0,NULL……),常量被赋正确的值。
解析阶段:符号引用替换为直接引用,类或接口的解析(需要判断是否为数组),字段解析(从本类找到接口->父接口->父类->祖父类 依次查找),类方法解析(与字段差不多,但是先父类后接口 ),接口方法解析(只搜父接口)
初始化:执行类构造器(static{}),static变量赋值语句,子类的
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这些类加载器的父子关系不是以继承的关系实现,而都是使用组合关系来复用父加载器的代码。
双亲委派模式的方法:向上不断查看是否加载了类(findLoadedClass()),向下尝试加载类(loadClass()),自定义类加载器需要回调(findClass())。
即使两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的 Class 对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。
具体请查看Blog Java类装载过程与类装载器
在JDK1.7及以前,HashMap中维护着Entry,Entry中维护着key,value以及hash和next指针,而整个HashMap实际就是一个Entry数组
当向 HashMap 中 put 一对键值时,它会根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。
如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是否是否值相同,map不允许
get方法类似,通过key取hash找到数组的某个位置,然后遍历这个数组上的每个Entry,直到key值equals则返回。
如果Hash碰撞严重,那么JDK1.7中的实现性能就很差,因为每次插入都要遍历完整条链去查看key值是否重复,每次get也要遍历整个链,在JDK1.8中,由于链表的查找复杂度为O(n),而红黑树的查找复杂度为O(logn),JDK1.8中采用链表/红黑树的方式实现HashMap,达到某个阀值时,链表转成了红黑树。
具体请查看 JDK7与JDK8中HashMap的实现
HashMap不是线程安全的,ConcurrentHashMap是线程安全的,HashMap内部维护着一个Entry数组,而ConcurrentHashMap内部有一个Segment段,它将大的HashMap切分成若干个段(小的HashMap),然后让数据在每一段上Hash,这样多个线程在不同段上的Hash操作一定是线程安全的,所以只需要同步同一个段上的线程就可以了,这样实现了锁的分离,大大增加了并发量。ConcurrentHashMap的实现中还使用了不变模式final和volatile来保障线程安全
具体请查看 ConcurrentHashMap总结
传统的进程间通信的方式有大致如下几种:
# 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
# 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
# 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
# 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
# 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
# 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
# 套接字( socket ) : 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
Java如何支持进程间通信。我们把Java进程理解为JVM进程。很明显,传统的这些大部分技术是无法被我们的应用程序利用了(这些进程间通信都是靠系统调用来实现的)。但是Java也有很多方法可以进行进程间通信的。
除了上面提到的Socket之外,当然首选的IPC可以使用Rmi,或者Corba也可以。另外Java nio的MappedByteBuffer也可以通过内存映射文件来实现进程间通信(共享内存)。
有两种类型的进程间通信(IPC):
本地过程调用(LPC):LPC用在多任务操作系统中,使得同时运行的任务能互相会话。这些任务共享内存空间使任务同步和互相发送信息。
远程过程调用(RPC):RPC类似于LPC,只是在网上工作。RPC开始是出现在Sun微系统公司和HP公司的运行UNIX操作系统的计算机中。
虚拟机将所管理的内存分为以下几个部分:
具体请查看 Java内存区域
JVM通过可达性(可触及性)分析算法标记出哪些对象是垃圾对象,然后将垃圾对象进行回收,在新生代中采用复制算法,在老年代中采用标记清理或标记压缩算法。新生代存储了新new出的对象,老年代存储了大的对象和多次GC后仍然存在的老年对象,持久代存储了类信息,常量(JDK7中String常量池被移到堆中),静态变量(JDK7中被移到了Java堆),类方法
具体请查看 JVM 垃圾回收机制 , Java内存区域
总体来说就是,全局中的引用(例如常量或者静态属性)与执行上下文(例如栈帧中的本地变量表)。
具体请查看 JVM 垃圾回收机制
快排的基本思想是:
1.先从数列中取出一个数作为基准数(pivot)。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
平均复杂度O(NlogN),最差O(N^2),最好O(NlogN)
具体请查看 排序总结(不断更新)
二叉平衡树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
AVL树的插入和删除,主要是依靠左旋和右旋来达到平衡状态。
红黑树的插入,插入节点是红色,通过一系列选择和着色使其成为红黑树。
步骤为:
所以红黑树的插入需要最多两次旋转,删除需要最多三次旋转
具体请查看 红黑树
TCP用三次握手和滑动窗口机制来保证传输的可靠性和进行流量控制。
第一次握手:
客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。
第二次握手:
服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的ISN加1以.即X+1。
第三次握手.
客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1
TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快
1. 首先是AB之间三次握手建立TCP连接。在报文的交互过程中,A将自己的缓冲区大小(窗口大小)3发送给B,B同理,这样双方就知道了对端的窗口大小。
2. A开始发送数据,A连续发送3个单位的数据,因为他知道B的缓冲区大小。在这一波数据发送完后,A就不能再发了,需等待B的确认。
3. A发送过来的数据逐渐将缓冲区填满。
4. 这时候缓冲区中的一个报文被进程读取,缓冲区有了一个空位,于是B向A发送一个ACK,这个报文中指示窗口大小为1。
A收到B发过来的ACK消息,并且知道B将窗口大小调整为1,因此他只发送了一个单位的数据并且等待B的下一个确认报文。
5. 如此反复。
在Linux中,进程的运行时间不可能超过分配给他们的时间片,他们采用的是抢占式多任务处理,所以进程之间的挂起和继续运行无需彼此之间的协作。
产生死锁的原因:一是系统提供的资源数量有限,不能满足每个进程的使用;二是多道程序运行时,进程推进顺序不合理。
银行家算法:安全状态一定没有死锁发生
产生死锁的必要条件是:1、互斥条件;2、不可剥夺条件(不可抢占);3、部分分配;4、循环等待。
详情查看操作系统总结
实际工作中需要视不同的情况采用不同的hash函数:
处理冲突的方法:
consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在key 映射关系,尽可能的满足单调性的要求。
通过一个环形hash空间,将服务器和需要缓存的内容都映射到环形空间内,将缓存的内容映射到下一个服务器中,当一个服务器down了以后,就映射到下一个服务器上。
但是这样会对下一个服务器造成很大的压力,没有平均到其他服务器上。这里就提出了增加虚拟节点,虚拟节点保证了映射到每个服务器的概率均衡。
详情查看一致性Hash算法
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
分布式锁可以使用数据库锁,redis(缓存),zookeeper来实现
数据库锁主要是使用唯一索引来代替锁,加锁时就往表中插入一个记录,其他线程要加锁则会唯一性约束无法成功。缺点是
1. 无法阻塞(加锁失败后,需要再发一次请求再次尝试)。
2. 如果服务器宕机,则无法解锁,造成死锁(可以从应用层上加定时任务,超过时间则强制解锁)
redis作为分布式锁:
第一种方式是缓存锁,就是使用setnx,即只有在某个key不存在情况才能set成功该key,这样就达到了多个进程并发去set同一个key,只有一个进程能set成功。(缺点和数据库锁一样,但是redis自带过期时间EX,则不需要从应用层加定时任务,虽然redis有主从复制,由于主从复制是异步的,仍然无法保证宕机后锁丢失)
第二种方式是Redlock
为了解决锁丢失,提出了Redlock算法
Redlock算法假设有N个redis节点,这些节点互相独立,一般设置为N=5,这N个节点运行在不同的机器上以保持物理层面的独立。
算法的步骤如下:
redlock由于和时间有关,比如在应用程序发生长时间的fullgc时,也会导致问题。
zookeeper的分布式锁:
zookeeper实现锁的方式是客户端一起竞争写某条数据,比如/path/lock(路径),只有第一个客户端能写入成功,其他的客户端都会写入失败。写入成功的客户端就获得了锁,写入失败的客户端,注册watch事件,等待锁的释放,从而继续竞争该锁。
优点:
具体关于分布式锁的详情请查看分布式锁
第一范式:确保每列的原子性.
如果每列(或者每个属性)都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式.
第二范式:在第一范式的基础上更进一层,目标是确保表中的每列都和主键相关.
如果一个关系满足第一范式,并且除了主键以外的其它列,都依赖于该主键,则满足第二范式.
第三范式:在第二范式的基础上更进一层,目标是确保每列消除传递依赖.
如果一个关系满足第二范式,并且没有传递依赖,则满足第三范式.
为了理解第三范式,需要根据Armstrong公里之一定义传递依赖。假设A、B和C是关系R的三个属性,如果A-〉B且B-〉C,则从这些函数依赖中,可以得出A-〉C,如上所述,依赖A-〉C是传递依赖。
例如:订单表(订单编号,定购日期,顾客编号,顾客姓名,……),初看该表没有问题,满足第二范式,每列都和主键列"订单编号"相关,再细看你会发现"顾客姓名"和"顾客编号"相关,"顾客编号"和"订单编号"又相关,最后经过传递依赖,"顾客姓名"也和"订单编号"相关。为了满足第三范式,应去掉"顾客姓名"列,放入客户表中。
mysql索引结构是B+树和hash。(hash索引只有在等值查询时,并且重复值少时才高效,具体两者区别请查看MySQL B+树索引和哈希索引的区别)
两种情况下不建议建索引。
第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。
另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:
Index Selectivity = Cardinality / #T
显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。
关于mysql索引的详情请查看MySQL索引背后的数据结构及算法原理
(24)用什么工具调试程序?JConsole,用过吗?
(25)JVM中某个线程挂起,如何用工具查出原因?
visualVM Dump线程信息出来。然后查看是因为死锁,还是阻塞等
(26)线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
(27)同步和异步有什么区别?
(28)线程池用过吗?
(29)如何创建单例模式?说了双重检查,他说不是线程安全的。如何高效的创建一个线程安全的单例?
(30)concurrent包下面,都用过什么?
(31)常用的数据库有哪些?redis用过吗?
(32)了解hadoop吗?说说hadoop的组件有哪些?hdfs,hive,hbase,zookeeper。说下mapreduce编程模型。
(33)你知道的开源协议有哪些?
(34)你知道的开源软件有哪些?
(35)你最近在看的书有哪些?
(37)了解哪些设计模式?说说都用过哪些设计模式
给定一个单链表,只给出头指针h:
1、如何判断是否存在环?
2、如何知道环的长度?
3、如何找出环的连接点在哪里?
4、带环链表的长度是多少?
解法:
1、对于问1,使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。
2、对于问2,记录下问1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。
3、问3:有定理:碰撞点p到连接点的距离=头指针到连接点的距离,因此,分别从碰撞点、头指针开始走,相遇的那个点就是连接点。
4、问3中已经求出连接点距离头指针的长度,加上问2中求出的环的长度,二者之和就是带环单链表的长度
(39)操作系统如何进行分页调度?
(40)匿名内部类是什么?如何访问在其外面定义的变量?