借着面试留点东西

1.barrier和latch区别,barrier更多的是等待其他线程(await),latch是在等待一个事件

2.volatile适用于可见性,但是不要求一致性的地方,适用于新值不依赖于旧值

3.读写锁,其实用的还是一把锁,fairSync和unfairSync,读锁是共享锁,写锁是互斥锁

4.threadLocal,见jdk的实现,每个threadLocal对应的thread有不同的threadLocalMap,利用threadLocal做key,因此不同的线程能够拿到不同的值(Thread.threadLocals),web应用中threadLocal用完之后要把值清除,因为web中一般都是用线程池处理用户请求,如果不及时清除,可能会有脏数据

5.sleep和yield的区别,sleep使得当前线程进入睡眠状态,释放锁,其他线程无论优先级可以获得锁,yield是让同优先级的线程获得锁

6.避免aba问题,一般采用版本戳

7.配置线程池时CPU密集型任务可以少配置线程数,大概和机器的cpu核数相当,可以使得每个线程都在执行任务IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数

有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。

8.CopyOnWriteArrayList这个容器适用于多读少写…读写并不是在同一个对象上。在写时会大面积复制数组,所以写的性能差,在写完成后将读的引用改为执行写的对象

9.主要利用方法去,类加载过程装载(把字节码装载到内存,jvm最终生成class对象存在堆上)、连接(验证:验证装载的字节码是否符合规范、类型是否符合要求等。解析:给静态变量分配内存。准备:把常量池中的符号引用替换为常量引用)、初始化(给静态变量赋值)

10。使用B(B+)树是因为高度低,查询快O(h) = logd(N), d为内部节点出度,b+数比b树好的原因是b+的非叶子节点只保存键值 不保存具体的data,所以能够使得出度更大(d更大),

dmax = floor(pagesize / (keySize + pointSize + dataSize))

11.mysql的explain,包含type,table,possible_keys,keys,keylength(最左前缀索引的时候可能有用),rows(检索的行)extra(其他信息using where, file sort等)

12,mysql最左前缀索引,只能使用一个范围查询的索引,索引严格意义上是顺序敏感的 但是查询优化器会做一些事情,使用函数或者表达式不能使用索引,如果组合索引缺少部分值(比如三个组合,缺少中间那个) 会explain 的type为range  过滤

13.volatile禁止指令冲排序。volatile的读不比普通变量慢,但是写会慢一些,因为要插入许多屏障 保障处理器不会乱序执行
14.事务隔离级别

串行化(SERIALIZABLE):所有事务都一个接一个地串行执行,这样可以避免幻读(phantom reads)。对于基于锁来实现并发控制的数据库来说,串行化要求在执行范围查询(如选取年龄在10到30之间的用户)的时候,需要获取范围锁(range lock)。如果不是基于锁实现并发控制的数据库,则检查到有违反串行操作的事务时,需要滚回该事务。

可重复读(REPEATABLE READ):所有被Select获取的数据都不能被修改,这样就可以避免一个事务前后读取数据不一致的情况。但是却没有办法控制幻读,因为这个时候其他事务不能更改所选的数据,但是可以增加数据,因为前一个事务没有范围锁。

读已提交(READ COMMITED):被读取的数据可以被其他事务修改。这样就可能导致不可重复读。也就是说,事务的读取数据的时候获取读锁,但是读完之后立即释放(不需要等到事务结束),而写锁则是事务提交之后才释放。释放读锁之后,就可能被其他事物修改数据。该等级也是SQL Server默认的隔离等级。

读未提交(READ UNCOMMITED):这是最低的隔离等级,允许其他事务看到没有提交的数据。这种等级会导致脏读(Dirty Read)。

15. get/set注入,接口注入、构造器注入

16.spring重要的类 BeanFactory, ApplicationContext, BeanFactory提供了管理bean,加载配置文件,维护bean之间的关系等。ApplcationText除了以上功能还有国际化支持、事件传递等功能

17.tcp/ip

    • TCP协议和UDP协议的区别是什么
      • TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的
      • TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。
      • TCP协议所需资源多,TCP首部需20个字节(不算可选项),UDP首部字段只需8个字节。
      • TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率
      • TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。
      • TCP面向的是字节流的服务,UDP面向的是报文的服务。
      • TCP介绍和UDP介绍
    • 请详细介绍一下TCP协议建立连接和终止连接的过程?
      • 助于理解的一段话
      • 两幅图(来源):
        • 建立连接:三次握手
        • image
        • 关闭连接:四次挥手
        • image
    • 三次握手建立连接时,发送方再次发送确认的必要性?
      • 主要是为了防止已失效的连接请求报文段突然又传到了B,因而产生错误。假定出现一种异常情况,即A发出的第一个连接请求报文段并没有丢失,而是在某些网络结点长时间滞留了,一直延迟到连接释放以后的某个时间才到达B,本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后,就误认为是A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了,这样一直等待A发来数据,B的许多资源就这样白白浪费了。
    • 四次挥手释放连接时,等待2MSL的意义?
      • 第一,为了保证A发送的最有一个ACK报文段能够到达B。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN和ACK报文段的确认。B会超时重传这个FIN和ACK报文段,而A就能在2MSL时间内收到这个重传的ACK+FIN报文段。接着A重传一次确认。
      • 第二,就是防止上面提到的已失效的连接请求报文段出现在本连接中,A在发送完最有一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。

 

      • 常见的应用中有哪些是应用TCP协议的,哪些又是应用UDP协议的,为什么它们被如此设计?
        • 以下应用一般或必须用udp实现?
          • 多播的信息一定要用udp实现,因为tcp只支持一对一通信。
          • 如果一个应用场景中大多是简短的信息,适合用udp实现,因为udp是基于报文段的,它直接对上层应用的数据封装成报文段,然后丢在网络中,如果信息量太大,会在链路层中被分片,影响传输效率。
          • 如果一个应用场景重性能甚于重完整性和安全性,那么适合于udp,比如多媒体应用,缺一两帧不影响用户体验,但是需要流媒体到达的速度快,因此比较适合用udp
          • 如果要求快速响应,那么udp听起来比较合适
          • 如果又要利用udp的快速响应优点,又想可靠传输,那么只能考上层应用自己制定规则了。
          • 常见的使用udp的例子:ICQ,QQ的聊天模块。
        • 以qq为例的一个说明(转载自知乎

登陆采用TCP协议和HTTP协议,你和好友之间发送消息,主要采用UDP协议,内网传文件采用了P2P技术。总来的说: 
1.登陆过程,客户端client 采用TCP协议向服务器server发送信息,HTTP协议下载信息。登陆之后,会有一个TCP连接来保持在线状态。 
2.和好友发消息,客户端client采用UDP协议,但是需要通过服务器转发。腾讯为了确保传输消息的可靠,采用上层协议来保证可靠传输。如果消息发送失败,客户端会提示消息发送失败,并可重新发送。 
3.如果是在内网里面的两个客户端传文件,QQ采用的是P2P技术,不需要服务器中转。

image

 

 

18. shell进程间共享信息,利用命名管道、全局变量(export)、写文件

19. redis高可用,sentinel、zookeeper、keepalived

1,keepalived:通过keepalived的虚拟IP,提供主从的统一访问,在主出现问题时,通过keepalived运行脚本将从提升为主,待主恢复后先同步后自动变为主,该方案的好处是主从切换后,应用程序不需要知道(因为访问的虚拟IP不变),坏处是引入keepalived增加部署复杂性; 
2,zookeeper:通过zookeeper来监控主从实例,维护最新有效的IP,应用通过zookeeper取得IP,对Redis进行访问; 
3,sentinel:通过Sentinel监控主从实例,自动进行故障恢复,该方案有个缺陷:因为主从实例地址(IP&PORT)是不同的,当故障发生进行主从切换后,应用程序无法知道新地址,故在Jedis2.2.2中新增了对Sentinel的支持,应用通过redis.clients.jedis.JedisSentinelPool.getResource()取得的Jedis实例会及时更新到新的主实例地址。 

 

笔者所在的公司先使用了方案1一段时间后,发现keepalived在有些情况下会导致数据丢失,keepalived通过shell脚本进行主从切换,配置复杂,而且keepalived成为新的单点,后来选用了方案3,使用Redis官方解决方案;(方案2需要编写大量的监控代码,没有方案3简便,网上有人使用方案2读者可自行查看)

20.MySQL默认操作模式就是autocommit自动提交模式。这就表示除非显式地开始一个事务,否则每个查询都被当做一个单独的事务自动执行。我们可以通过设置autocommit的值改变是否是自动提交autocommit模式。

21.jdbc优化。

使用连接池,合理设置min值和max值。
p 尽量使用批量处理接口:
p 批量写入(addBatch,executeBatch)
p 批量查询 (inlist,fetchsize)。
p 减少事务数,合并不必要事务。(autocommit=false)
p 选择Preparedstatement, Preparedstatementcache。
p 存储过程和匿名块可以减少网络传输,但会给数据库带来复
杂度。
p 其它层面的sql调优,如尽量不要使用select *等。

 

算法相关:

 

咱们试着一步一步解决这个问题(注意阐述中数列有序无序的区别):

 

    1. 直接穷举,从数组中任意选取两个数,判定它们的和是否为输入的那个数字。此举复杂度为O(N^2)。很显然,我们要寻找效率更高的解法。
    2. 题目相当于,对每个a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的时间都要花费为O(N),这样下来,最终找到两个数还是需要O(N^2)的复杂度。那如何提高查找判断的速度呢?答案是二分查找,可以将O(N)的查找时间提高到O(logN),这样对于N个a[i],都要花logN的时间去查找相对应的sum-a[i]是否在原始序列中,总的时间复杂度已降为O(N*logN),且空间复杂度为O(1)。(如果有序,直接二分O(N*logN),如果无序,先排序后二分,复杂度同样为O(N*logN+N*logN)=O(N*logN),空间总为O(1))。
    3. 有没有更好的办法呢?咱们可以依据上述思路2的思想,a[i]在序列中,如果a[i]+a[k]=sum的话,那么sum-a[i](a[k])也必然在序列中,举个例子,如下:
      原始序列:1、 2、 4、 7、11、15     用输入数字15减一下各个数,得到对应的序列为:
      对应序列:14、13、11、8、4、 0      
      第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,如果下面出现了和上面一样的数,即a[*i]=a[*j],就找出这俩个数来了。如上,i,j最终在第一个,和第二个序列中找到了相同的数4和11,,所以符合条件的两个数,即为4+11=15。怎么样,两端同时查找,时间复杂度瞬间缩短到了O(N),但却同时需要O(N)的空间存储第二个数组(@飞羽:要达到O(N)的复杂度,第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,首先初始i指向元素1,j指向元素0,谁指的元素小,谁先移动,由于1(i)>0(j),所以i不动,j向左移动。然后j移动到元素4发现大于元素1,故而停止移动j,开始移动i,直到i指向4,这时,i指向的元素与j指向的元素相等,故而判断4是满足条件的第一个数;然后同时移动i,j再进行判断,直到它们到达边界)。
    4. 当然,你还可以构造hash表,正如编程之美上的所述,给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1)的时间,这样的话,总体的算法通上述思路3 一样,也能降到O(N),但有个缺陷,就是构造hash额外增加了O(N)的空间,此点同上述思路 3。不过,空间换时间,仍不失为在时间要求较严格的情况下的一种好办法。
    5. 如果数组是无序的,先排序(n*logn),然后用两个指针i,j,各自指向数组的首尾两端,令i=0,j=n-1,然后i++,j--,逐次判断a[i]+a[j]?=sum,如果某一刻a[i]+a[j]>sum,则要想办法让sum的值减小,所以此刻i不动,j--,如果某一刻a[i]+a[j]<sum,则要想办法让sum的值增大,所以此刻i++,j不动。所以,数组无序的时候,时间复杂度最终为O(n*logn+n)=O(n*logn),若原数组是有序的,则不需要事先的排序,直接O(n)搞定,且空间复杂度还是O(1),此思路是相对于上述所有思路的一种改进。(如果有序,直接两个指针两端扫描,时间O(N),如果无序,先排序后两端扫描,时间O(N*logN+N)=O(N*logN),空间始终都为O(1))。(与上述思路2相比,排序后的时间开销由之前的二分的n*logn降到了扫描的O(N))。

 

 


21.rpc和rmi

远程对象方法调用并不是新概念,远程过程调用 (RPC) 已经使用很多年了。远程过程调用被设计为在应用程序间通信的平台中立的方式,它不理会操作系统之间以及语言之间的差异。即 RPC 支持多种语言,而 RMI 只支持 Java 写的应用程序。 [1]

另外 RMI 调用远程对象方法,允许方法返回 Java 对象以及基本数据类型。而 RPC 不支持对象的概念,传送到 RPC 服务的消息由外部数据表示 (External Data Representation, XDR) 语言表示,这种语言抽象了字节序类和数据类型结构之间的差异。只有由 XDR 定义的数据类型才能被传递, RPC 不允许传递对象。可以说 RMI 是面向对象方式的 Java RPC 。

 

RMI (Remote Method Invocation)

RMI 采用stubs 和 skeletons 来进行远程对象(remote object)的通讯。stub 充当远程对象的客户端代理,有着和远程对象相同的远程接口,远程对象的调用实际是通过调用该对象的客户端代理对象stub来完成的,通过该机制RMI就好比它是本地工作,采用tcp/ip协议,客户端直接调用服务端上的一些方法。优点是强类型,编译期可检查错误,缺点是只能基于JAVA语言,客户机与服务器紧耦合。

RPC(Remote Procedure Call Protocol)

RPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.methodname”形式。优点是跨语言跨平台,C端、S端有更大的独立性,缺点是不支持对象,无法在编译器检查错误,只能在运行期检查。

 

22. java和c通讯

字节序问题:这个是通讯的大问题。。前面几篇文章也转载了查阅到的一些资料。总的来说C一般使用的是小尾存储数据,而java使用大尾存储,所谓大 尾存储就是数据高字节在前,低字节在后存储。而网络中的数据则都是大尾存储。另字符串在传输过程中不会发生变化,而int,long等数值类型的数据会经 过根据大小尾进行存储传输。所以当java与c进行通信的时候,java一段数据基本不用进行大小尾转化,而c收到数据后要进行NToH转化,发送数据的 时候也要进行HToN数据转化。再加上字符串,打成包传输即可。

23.innodb 多版本一致性读

 MVCC多版本一致性读
在innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较。
具体的算法如下:
1. 设该行的当前事务id为trx_id_0,read view中最早的事务id为trx_id_1, 最迟的事务id为trx_id_2。
2. 如果trx_id_0< trx_id_1的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。跳到步骤6.
3. 如果trx_id_0>trx_id_2的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见.跳到步骤5。
4. 如果trx_id_1<=trx_id_0<=trx_id_2, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从trx_id_1到trx_id_2进行遍历,如果trx_id_0等于他们之中的某个事务id的话,那么不可见。跳到步骤5.
5. 从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该trx_id_0,然后跳到步骤2.
6. 将该可见行的值返回。

 

24.IOC机制模拟:读配置文件、利用反射实例化

spring中的BeanHelper做这个事情,反射创建实例,然后赋值给对应的引用

25. aop实现方案,aspectj和spring aop ,spring aop运用了代理,对目标对象动态生成代理对象,包含目标对象所有方法,但是会有回调。

26.二叉树最近公共父节点

情况一:root未知,但是每个节点都有parent指针
此时可以分别从两个节点开始,沿着parent指针走向根节点,得到两个链表,然后求两个链表的第一个公共节点,这个方法很简单,不需要详细解释的。

两个链表相交问题

情况二:节点只有左、右指针,没有parent指针,root已知
思路:有两种情况,一是要找的这两个节点(a, b),在要遍历的节点(root)的两侧,那么这个节点就是这两个节点的最近公共父节点;
二是两个节点在同一侧,则 root->left 或者 root->right 为 NULL,另一边返回a或者b。那么另一边返回的就是他们的最小公共父节点。
递归有两个出口,一是没有找到a或者b,则返回NULL;二是只要碰到a或者b,就立刻返回。

 

27.java集合类

集合类

    • Set
      • HashSet
        • 优点: 
            后台实现一个hash table 加速get和contains方法。后台使用数组保存 
          缺点: 
           默认大小为16, 如果超过则需要重新申请内存空间,大小为原来的两倍,并把原来的数据内容复制到 
           新的内存空间中。 
           线程不安全(需通过Collections.synchronizedList方法设置) 
           加入的元素顺序会因其内部的hash排序而改变 

          注:通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。

      • LinkedHashSet
        • 优点: 
            后台实现一个hash table 加速get和contains方法。后台使用链表保存 
          缺点: 
           默认大小为16, 如果超过则需要重新申请内存空间,大小为原来的两倍,并把原来的数据内容复制到 
           新的内存空间中。 
           线程不安全(需通过Collections.synchronizedList方法设置) 
           加入的元素顺序会因其内部的hash排序而改变 

          注:通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。

      • TreeSet
        • 优点: 
            通过一个HashMap来实现数据的保存,内部实现红黑树数据结构,使所有元素按升序保存。 
            提供高效的get和contains方法,保存操作的效率为log(n) 
          缺点: 
           默认大小为16, 如果超过则需要重新申请内存空间,大小为原来的两倍,并把原来的数据内容复制到 
           新的内存空间中(来自HashMap)。 
           线程不安全(需通过Collections.synchronizedList方法设置) 
           加入的元素升级排序而改变 

          注:treeset对元素有要求,必须实现Comparable接口或是Comparator 接口) 

          注:通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。

      • CopyOnWriteArraySet
        • 优点: 
           针对于对Set操作的情况有很多变化时使用,优其是在高并发的情况不想使用同步控制锁时 
          缺点: 
           消耗比较大的资料,每次作更新操作时,都会重新Copy一块内存后,再做合并操作。
    • List
      • ArrayList
        • 优点: 
             使用数组,提供快速的get,add和iterate方法,占用比较小的内存空间 
          缺点: 
             线程不安全(需通过Collections.synchronizedList方法设置) 
             insert和remove操作,非常慢(需要移动数组元素来实现) 
             当size超过时,需要新建一个较大的数据(默认大小是10,增量是 (size * 3)/2 + 1, 
             且把原来的数据都复制到新的上面)

      • LinkedList
        • 优点: 
             使用链表结构,提供快速的add, insert, remove方法,占用比较小的内存空间 
          缺点: 
             线程不安全(需通过Collections.synchronizedList方法设置) 
             get操作,非常慢(需要从head一级级遍历查找)

      • Vector
        • 优点: 
           线程安全。 
          缺点: 
           相对于ArrayList效率要低。拥有ArrayList的缺点。
      • CopyOnWriteArrayList
        • 优点: 
           针对于对List操作的情况有很多变化时使用,优其是在高并发的情况不想使用同步控制锁时 
          缺点: 
           消耗比较大的资料,每次作更新操作时,都会重新Copy一块内存后,再做合并操作。
      • TreeList(apache commons-collections)提供
        • 优点: 
           基于二叉数  提供比较快速的get, add,insert,iterate,remove方法。其中get,add和iterate方法比ArrayList稍慢一点。 
          缺点: 
           相对于ArrayList和LinkedList占比较多的内存空间 
           线程不安全(需通过Collections.synchronizedList方法设置)

    • Map
      • ConcurrentHashMap
        • 优点: 
           基于二叉数  提供比较快速的get, add,iterate方法。默认大小的16. 
           它是线程安全 
          缺点: 
           如果大小超过设定的大小时,效率会非常低。它会重新申请内存空间(原来空间的两倍),同时把原来的值复制到新内存空间上。

 

 总结
  如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
  如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
  要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
  尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。

 

TreeMap:内部实现是红黑树,有序的map

 

28。jvm内存模型:jvm堆、方法区、虚拟机栈、程序计数器、本地方法栈

线程隔离数据区

程序计数器(Program Counter Register):
一小块内存空间,单前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM虚拟机栈(Java Virtual Machine Stacks):
Java方法执行内存模型,用于存储局部变量,操作数栈,动态链接,方法出口等信息。是线程私有的。

本地方法栈(Native Method Stacks):
为JVM用到的Native方法服务,Sun HotSpot 虚拟机把本地方法栈和JVM虚拟机栈合二为一。是线程私有的。

 

线程共享的数据区

方法区(Method Area):
用于存储JVM加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool):
是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法取得运行时常量池中。具备动态性,用的比较多的就是String类的intern()方法。
JVM堆( Java Virtual Machine Heap):
存放所有对象实例的地方。
新生代,由Eden Space 和大小相同的两块Survivor组成
旧生待,存放经过多次垃圾回收仍然存活的对象

 

 

为了支持跨平台的特性,java语言采用源代码编译成中间字节码,然后又各平台的jvm解释执行的方式。字节码采用了完全与平台无关的方式进行描述,java只给出了字节码格式的规范,并没有规定字节码最终来源是什么,它可以是除了java语言外的其他语言产生,只要是满足字节码规范的,都可以在jvm中很好的运行。正因为这个特性,极大的促进了各类语言的发展,在jvm平台上出现了很多语言,如scala,groovy等

由于字节码来源并没有做限制,因此jvm必须在字节码正式使用之前,即在加载过程中,对字节码进行检查验证,以保证字节码的可用性和安全性。

 

1. jvm运行时内存结构划分

在正式介绍之前,先看看jvm内存结构划分:

结合垃圾回收机制,将堆细化:

在加载阶段主要用到的是方法区:

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

如果把方法的代码看作它的“静态”部分,而把一次方法调用需要记录的临时数据看做它的“动态”部分,那么每个方法的代码是只有一份的,存储于JVM的方法区中;每次某方法被调用,则在该调用所在的线程的的Java栈上新分配一个栈帧,用于存放临时数据,在方法返回时栈帧自动撤销。

2. 类加载过程

jvm将类加载过程分成加载,连接,初始化三个阶段,其中连接阶段又细分为验证,准备,解析三个阶段。

 

上述三个阶段总体上会保持这个顺序,但是有些特殊情况,如加载阶段与连接阶段的部分内容(一部分字节码的验证工作)是交叉进行的。再如:解析阶段可以是推迟初次访问某个类的时候,因此它可能出现在初始化阶段之后。

2.1 装载

装载阶段主要是将java字节码以二进制的方式读入到jvm内存中,然后将二进制数据流按照字节码规范解析成jvm内部的运行时数据结构。java只对字节码进行了规范,并没有对内部运行时数据结构进行规定,不同的jvm实现可以采用不同的数据结构,这些运行时数据结构是保存在jvm的方法区中(hotspot jvm的内部数据结构定义可以参见撒迦的博文借助HotSpot SA来一窥PermGen上的对象)。当一个类的二进制解析完毕后,jvm最终会在堆上生成一个java.lang.Class类型的实例对象,通过这个对象可以访问到该类在方法区的内容。

jvm规范并没有规定从二进制字节码数据应该如何产生,事实上,jvm为了支持二进制字节码数据来源的可扩展性,它提供了一个回调接口将通过一个类的全限定名来获取描述此类的二进制字节码的动作开放到jvm的外部实现,这就是我们后面要讲到的类加载器,如果有需要,我们完全可以自定义一些类加载器,达到一些特殊应用场景。由于有了jvm的支持,二进制流的产生的方式可以是:

(1) 从本地文件系统中读取

(2) 从网络上加载(典型应用:java Applet)

(3) 从jar,zip,war等压缩文件中加载

(4) 通过动态将java源文件动态编译产生(jsp的动态编译)

(5) 通过程序直接生成。

 

2.2 连接

连接阶段主要是做一些加载完成之后的验证工作,和初始化之前的准备一些工作,它细分为三个阶段。

2.2.1 验证

验证是连接阶段的第一步,它主要是用于保证加载的字节码符合java语言的规范,并且不会给虚拟机带来危害。比如验证这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。按照验证的内容不同又可以细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证往往会与解析阶段交叉进行)。

2.2.2 准备

准备阶段主要是为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。

在jvm中各类型的初始值如下:

int,byte,char,long,float,double 默认初始值为0

boolean 为false(在jvm内部用int表示boolean,因此初始值为0)

reference类型为null

对于final static基本类型或者String类型,则直接采用常量值(这实际上是在编译阶段就已经处理好了)。

2.2.3 解析

解析过程就是查找类的常量池中的类,字段,方法,接口的符号引用,将他们替换成直接引用的过程。

a.解析过程主要针对于常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量。

b. jvm规范并没有规定解析阶段发生的时间,只是规定了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic这13个指令应用于符号指令时,先对它们进行解析,获取它们的直接引用.

c. jvm对于每个加载的类都会有在内部创建一个运行时常量池(参考上面图示),在解析之前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程中当需要使用某个符号引用时,就会促发解析的过程,解析过程就是通过符号引用查找对应的类实体,然后用直接引用替换符号引用。由于符号引用已经被替换成直接引用,因此后面再次访问时,无需再次解析,直接返回直接引用。

2.3 初始化

初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法<clinit>()方中。该方法由编译器在编译阶段生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。

2.3.1 初始化执行时机

jvm规范明确规定了初始化执行条件,只要满足以下四个条件之一,就会执行初始化工作

(1) 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。

(2) 通过反射方式执行以上行为时。

(3) 初始化子类的时候,会触发父类的初始化。

(4) 作为程序入口直接运行时的主类。

2.3.2 初始化过程

初始化过程包括两步:

(1) 如果类存在直接父类,并且父类没有被初始化则对直接父类进行初始化。

(2) 如果类当前存在<clinit>()方法,则执行<clinit>()方法。

需要注意的是接口(interface)的初始化并不要求先初始化它的父接口。(接口不能有static块)

2.3.3 <clinit>()方法存在的条件

并不是每个类都有<clinit>()方法,如下情况下不会有<clinit>()方法:

a. 类没有静态变量也没有静态语句块

b.类中虽然定义了静态变量,但是没有给出明确的初始化语句。

c.如果类中仅包含了final static 的静态变量的初始化语句,而且初始化语句采用编译时常量表达时,也不会有<clinit>()方法。

例子:

代码:

 

  1. public class ConstantExample {  
  2.   
  3.     public static final int   a = 10;  
  4.     public static final float b = a * 2.0f;  
  5. }  

编译之后用 javap -verbose ConstantExample查看字节码,显示如下:

 

 

  1. {  
  2. public static final int a;  
  3.   Constant value: int 10  
  4. public static final float b;  
  5.   Constant value: float 20.0f  
  6. public ConstantExample();  
  7.   Code:  
  8.    Stack=1, Locals=1, Args_size=1  
  9.    0:   aload_0  
  10.    1:   invokespecial   #15//Method java/lang/Object."<init>":()V  
  11.    4:   return  
  12.   LineNumberTable:   
  13.    line 120  
  14.   
  15.   LocalVariableTable:   
  16.    Start  Length  Slot  Name   Signature  
  17.    0      5      0    this       LConstantExample;  
  18.   
  19. }  

这里由于编译器直接10,当作常量来处理,看到是没有<clinit>()方法存在的。可以当作常量来处理的类型包括基本类型和String类型

 

对于其他类型:

 

  1. public class ConstantExample1 {  
  2.   
  3.     public static final int   a = 10;  
  4.     public static final float b = a * 2.0f;  
  5.     public static final Date  c = new Date();  
  6. }  

这里虽然c被声明成final,但是仍然会产生<clinit>()方法,如下所示:

 

  1. {  
  2. public static final int a;  
  3.   Constant value: int 10  
  4. public static final float b;  
  5.   Constant value: float 20.0f  
  6. public static final java.util.Date c;  
  7.   
  8. static {};  
  9.   Code:  
  10.    Stack=2, Locals=0, Args_size=0  
  11.    0:   new #17//class java/util/Date  
  12.    3:   dup  
  13.    4:   invokespecial   #19//Method java/util/Date."<init>":()V  
  14.    7:   putstatic   #22//Field c:Ljava/util/Date;  
  15.    10:  return  
  16.   LineNumberTable:   
  17.    line 190  
  18.    line 1410  

 

2.3.4 并发性

在同一个类加载器域下,每个类只会被初始化一次,当多个线程都需要初始化同一个类,这时只允许一个线程执行初始化工作,其他线程则等待。当初始化执行完后,该线程会通知其他等待的线程。

 

2.4 在使用过程中类,对象在方法区和堆上的分布状态

先上代码

 

  1. public class TestThread extends Thread implements Cloneable {  
  2.   
  3.     public static void main(String[] args) {  
  4.         TestThread t = new TestThread();  
  5.         t.start();  
  6.     }  
  7. }  

 

上面这代码中TestThread及相关类在jvm运行的存储和引用情况如下图所示:



其中 t 作为TestThread对象的一个引用存储在线程的栈帧空间中,Thread对象及类型数据对应的Class对象实例都存储在堆上,类型数据存储在方法区,前面讲到了,TestThread的类型数据中的符号引用在解析过程中会被替换成直接引用,因此TestThread类型数据中会直接引用到它的父类Thread及它实现的接口Cloneable的类型数据。

在同一个类加载器空间中,对于全限定名相同的类,只会存在唯一的一份类的实例及类型数据。实际上类的实例数据和其对应的Class对象是相互引用的。

 

3. 类加载器

上面已经讲到类加载器实际上jvm在类加载过程中的装载阶段开放给外部使用的一个回调接口,它主要实现的功能就是:将通过一个类的全限定名来获取描述此类的二进制字节码。当然类加载器的优势远不止如此,它是java安全体系的一个重要环节(java安全体系结构,后面会专门写篇文章讨论),同时通过类加载器的双亲委派原则等类加载器和class唯一性标识一个class的方式,可以给应用程序带来一些强大的功能,如hotswap。

3.1 双亲委派模型

在jvm中一个类实例的唯一性标识是类的全限定名和该类的加载器,类加载器相当于一个命名空间,将同名class进行了隔离。

从jvm的角度来说,只存在两类加载器,一类是由c++实现的启动类加载器,是jvm的一部分,一类是由java语言实现的应用程序加载器,独立在jvm之外。

jkd中自己定义了一些类加载器:

 

(1).BootStrap ClassLoader:启动类加载器,由C++代码实现,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。

(2).Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

(3).Application ClassLoader:应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。

参考ClassLoader源代码会发现,这些Class之间并不是采用继承的方式实现父子关系,而是采用组合方式。

正常情况下,每个类加载在收到类加载请求时,会先调用父加载器进行加载,若父加载器加载失败,则子加载器进行加载。

3.2 两种主动加载方式

在java中有两种办法可以在应用程序中主动加载类:

一种是Class类的forName静态方法

 

  1. public static Class<?> forName(String className)   
  2.                 throws ClassNotFoundException   
  3. //允许指定是否初始化,并且指定类的类加载器  
  4. public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException   

另一种就是ClassLoader中的loadClass方法

 

  1.  protected synchronized Class<?> loadClass(String name, boolean resolve) //第二个参数表示是否在转载完后进行连接(解析)  
  2.     throws ClassNotFoundException  
  3.   
  4. public Class<?> loadClass(String name) throws ClassNotFoundException  

 

上面这两种方式是有区别的,如下例所示

 

  1. public class InitialClass {  
  2.   
  3.     public static int i;  
  4.     static {  
  5.         i = 1000;  
  6.         System.out.println("InitialClass is init");  
  7.     }  
  8.   
  9. }  



 

  1. public class InitClassTest {  
  2.   
  3.     public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {  
  4.         Class classFromForName = Class.forName("com.alibaba.china.jianchi.example.InitialClass",  
  5.                                                true,  
  6.                                                new URLClassLoader(  
  7.                                                                   new URL[] { new URL(  
  8.                                                                                       "file:/home/tanfeng/workspace/springStudy/bin/") },  
  9.                                                                   InitClassTest.class.getClassLoader()));  
  10.   
  11.         Class classFromClassLoader = (new URLClassLoader(  
  12.                                                          new URL[] { new URL(  
  13.                                                                              "file:/home/tanfeng/workspace/springStudy/bin/") },  
  14.                                                          InitClassTest.class.getClassLoader())).loadClass("com.alibaba.china.jianchi.example.InitialClass");  
  15.   
  16.     }  
  17. }  

通过运行可以考到用Class.forName()方法会将装载的类初始化,而ClassLoader.loadClass()方法则不会。

我们经常会看到在数据库操作时,会用Class.forName()的方式加载驱动类,而不是ClassLoader.loadClass()方法,为何要这样呢?

来看看mysql的驱动类实现,可以看到在类的初始化阶段,它会将自己注册到驱动管理器中(static块)。

 

  1. package com.mysql.jdbc;  
  2. public class Driver extends NonRegisteringDriver implements java.sql.Driver {  
  3.   
  4.     static {  
  5.         try {  
  6.             java.sql.DriverManager.registerDriver(new Driver());  
  7.         } catch (SQLException E) {  
  8.             throw new RuntimeException("Can't register driver!");  
  9.         }  
  10.     }  
  11.       ... ...  
  12. }  

 

3.3 自定义类加载器的应用

3.3.1 Tomcat中类加载器分析

3.3.1.1 tomcat中通过自定义一组类加载器,解决了以下几个问题:

 

(1)部署在一个服务器上的两个Web应用程序自身所使用的Java类库是相互隔离的。

(2)部署在一个服务器上的两个Web应用程序可以共享服务器提供的java共用类库。

(3)服务器尽可能的保证自身安全不受部署的Web应用程序影响。

(4)支持对JSP的HotSwap功能。

3.3.1.2 tomcat的目录结构

tomcat主要根据根据java类库的共享范围,分为4组目录:

(1)common目录:能被Tomcat和所有Web应用程序共享。
(2)server目录:仅能被Tomcat使用,其他Web应用程序不可见。
(3)Shared目录:可以被所有Web应用程序共享,对Tomcat不可见。
(4)WEB-INF目录:只能被当前Web应用程序使用,对其他web应用程序不可见。

3.3.1.3 tomcat自定义类加载器

 

这几个类加载器分别对应加载/common/*、/server/*、/shared/*和 /WEB-INF/*类库, 其中Webapp类加载器和Jsp类加载器会存在多个,每个Web应用对应一个Webapp类加载器。

CommonClassLoader加载的类可以被CatalinaClassLoader和ShareClassLoader使用;CatalinaClassLoader加载的类和ShareClassLoader加载的类相互隔离; WebappClassLoader可以使用ShareClassLoader加载的类,但各个WebappClassLoader间相互隔离;JspClassLoader仅能用JSP文件编译的class文件。

 

29.防止sql注入

  严格区分权限、使用参数化语句,不要直接嵌入在sql语句中、加强验证等

 

30. 大文件交集,利用hash函数或者md5等 把url转化为不重复的整数,然后用bloomfilter过滤

 

 

31 jdk里的设计模式


 
 
 
 
 

细数JDK里的设计模式

原文出处: javacodegeeks   译文出处: deepinmind。欢迎加入技术翻译小组

这也是篇老文了,相信很多人也看过。前面那些废话就不翻译了,直接切入正题吧~

结构型模式:

适配器模式:

用来把一个接口转化成另一个接口。

  • java.util.Arrays#asList()
  • javax.swing.JTable(TableModel)
  • java.io.InputStreamReader(InputStream)
  • java.io.OutputStreamWriter(OutputStream)
  • javax.xml.bind.annotation.adapters.XmlAdapter#marshal()
  • javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal()
桥接模式:

这个模式将抽象和抽象操作的实现进行了解耦,这样使得抽象和实现可以独立地变化。

  • AWT (It provides an abstraction layer which maps onto the native OS the windowing support.)
  • JDBC

 

组合模式

使得客户端看来单个对象和对象的组合是同等的。换句话说,某个类型的方法同时也接受自身类型作为参数。

    • javax.swing.JComponent#add(Component)
    • java.awt.Container#add(Component)
    • java.util.Map#putAll(Map)
    • java.util.List#addAll(Collection)
    • java.util.Set#addAll(Collection)

 

装饰者模式:

动态的给一个对象附加额外的功能,这也是子类的一种替代方式。可以看到,在创建一个类型的时候,同时也传入同一类型的对象。这在JDK里随处可见,你会发现它无处不在,所以下面这个列表只是一小部分。

      • java.io.BufferedInputStream(InputStream)
      • java.io.DataInputStream(InputStream)
      • java.io.BufferedOutputStream(OutputStream)
      • java.util.zip.ZipOutputStream(OutputStream)
      • java.util.Collections#checkedList|Map|Set|SortedSet|SortedMap

 

门面模式:

给一组组件,接口,抽象,或者子系统提供一个简单的接口。

      • java.lang.Class
      • javax.faces.webapp.FacesServlet

 

享元模式

使用缓存来加速大量小对象的访问时间。

      • java.lang.Integer#valueOf(int)
      • java.lang.Boolean#valueOf(boolean)
      • java.lang.Byte#valueOf(byte)
      • java.lang.Character#valueOf(char)
代理模式

代理模式是用一个简单的对象来代替一个复杂的或者创建耗时的对象。

      • java.lang.reflect.Proxy
      • RMI

创建模式

抽象工厂模式

抽象工厂模式提供了一个协议来生成一系列的相关或者独立的对象,而不用指定具体对象的类型。它使得应用程序能够和使用的框架的具体实现进行解耦。这在JDK或者许多框架比如Spring中都随处可见。它们也很容易识别,一个创建新对象的方法,返回的却是接口或者抽象类的,就是抽象工厂模式了。

      • java.util.Calendar#getInstance()
      • java.util.Arrays#asList()
      • java.util.ResourceBundle#getBundle()
      • java.sql.DriverManager#getConnection()
      • java.sql.Connection#createStatement()
      • java.sql.Statement#executeQuery()
      • java.text.NumberFormat#getInstance()
      • javax.xml.transform.TransformerFactory#newInstance()
建造模式(Builder)

定义了一个新的类来构建另一个类的实例,以简化复杂对象的创建。建造模式通常也使用方法链接来实现。

      • java.lang.StringBuilder#append()
      • java.lang.StringBuffer#append()
      • java.sql.PreparedStatement
      • javax.swing.GroupLayout.Group#addComponent()
工厂方法

就是一个返回具体对象的方法。

      • java.lang.Proxy#newProxyInstance()
      • java.lang.Object#toString()
      • java.lang.Class#newInstance()
      • java.lang.reflect.Array#newInstance()
      • java.lang.reflect.Constructor#newInstance()
      • java.lang.Boolean#valueOf(String)
      • java.lang.Class#forName()
原型模式

使得类的实例能够生成自身的拷贝。如果创建一个对象的实例非常复杂且耗时时,就可以使用这种模式,而不重新创建一个新的实例,你可以拷贝一个对象并直接修改它。

      • java.lang.Object#clone()
      • java.lang.Cloneable
单例模式

用来确保类只有一个实例。Joshua Bloch在Effetive Java中建议到,还有一种方法就是使用枚举。

      • java.lang.Runtime#getRuntime()
      • java.awt.Toolkit#getDefaultToolkit()
      • java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
      • java.awt.Desktop#getDesktop()

行为模式

责任链模式

通过把请求从一个对象传递到链条中下一个对象的方式,直到请求被处理完毕,以实现对象间的解耦。

      • java.util.logging.Logger#log()
      • javax.servlet.Filter#doFilter()
命令模式

将操作封装到对象内,以便存储,传递和返回。

      • java.lang.Runnable
      • javax.swing.Action
解释器模式

这个模式通常定义了一个语言的语法,然后解析相应语法的语句。

      • java.util.Pattern
      • java.text.Normalizer
      • java.text.Format
迭代器模式

提供一个一致的方法来顺序访问集合中的对象,这个方法与底层的集合的具体实现无关。

      • java.util.Iterator
      • java.util.Enumeration
中介者模式

通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖。

      • java.util.Timer
      • java.util.concurrent.Executor#execute()
      • java.util.concurrent.ExecutorService#submit()
      • java.lang.reflect.Method#invoke()
备忘录模式

生成对象状态的一个快照,以便对象可以恢复原始状态而不用暴露自身的内容。Date对象通过自身内部的一个long值来实现备忘录模式。

      • java.util.Date
      • java.io.Serializable
空对象模式

这个模式通过一个无意义的对象来代替没有对象这个状态。它使得你不用额外对空对象进行处理。

      • java.util.Collections#emptyList()
      • java.util.Collections#emptyMap()
      • java.util.Collections#emptySet()
观察者模式

它使得一个对象可以灵活的将消息发送给感兴趣的对象。

      • java.util.EventListener
      • javax.servlet.http.HttpSessionBindingListener
      • javax.servlet.http.HttpSessionAttributeListener
      • javax.faces.event.PhaseListener

 

状态模式

通过改变对象内部的状态,使得你可以在运行时动态改变一个对象的行为。

      • java.util.Iterator
      • javax.faces.lifecycle.LifeCycle#execute()
策略模式

使用这个模式来将一组算法封装成一系列对象。通过传递这些对象可以灵活的改变程序的功能。

      • java.util.Comparator#compare()
      • javax.servlet.http.HttpServlet
      • javax.servlet.Filter#doFilter()
模板方法模式

让子类可以重写方法的一部分,而不是整个重写,你可以控制子类需要重写那些操作。

      • java.util.Collections#sort()
      • java.io.InputStream#skip()
      • java.io.InputStream#read()
      • java.util.AbstractList#indexOf()
访问者模式

提供一个方便的可维护的方式来操作一组对象。它使得你在不改变操作的对象前提下,可以修改或者扩展对象的行为。

      • javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor
      • javax.lang.model.type.TypeMirror and javax.lang.model.type.TypeVisitor

译者注:很多地方可能会存在争议,是否是某种模式其实并不是特别重要,重要的是它们的设计能为改善我们的代码提供一些经验。

posted on 2014-04-18 20:50  aiguang  阅读(428)  评论(0编辑  收藏  举报

导航