Java面试题必成功:第一弹 (持续更新)

第一周

  1. 分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

简单来讲,当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。

  1. 分布式锁

为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁应该具备哪些条件:

1. 获取锁和释放锁的性能要好

2. 判断是否获得锁必须是原子性的,否则可能导致多个请求都获取到锁

3. 网络中断或宕机无法释放锁时,锁必须被清楚,不然会发生死锁

   4. 可重入一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;

   5.阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。

3. nginx负载均衡策略

1)轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

 

2)weight(权重) 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

 

3)ip_hash每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

 

4)fair(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

 

5)url_hash(第三方)按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效

注意:ip_hash中黏贴的时候,容易导致连环坏服务器,所以一般选择策略为第三方策略运用(原因:主要是第三方分布服务器的时候有一个算法,可以均匀的分布请求到服务器上)

4.mysql存储引擎

(1):MyISAM存储引擎:不支持事务、也不支持外键,优势是访问速度快,对事务完整性没有要求或者以select,insert为主的应用基本上可以用这个引擎来创建表

支持3种不同的存储格式,分别是:静态表;动态表;压缩表

静态表:表中的字段都是非变长字段,这样每个记录都是固定长度的,优点存储非常迅速,容易缓存,出现故障容易恢复;缺点是占用的空间通常比动态表多(因为存储时会按照列的宽度定义补足空格)ps:在取数据的时候,默认会把字段后面的空格去掉,如果不注意会把数据本身带的空格也会忽略。

动态表:记录不是固定长度的,这样存储的优点是占用的空间相对较少;缺点:频繁的更新、删除数据容易产生碎片,需要定期执行OPTIMIZE TABLE或者myisamchk-r命令来改善性能

压缩表:因为每个记录是被单独压缩的,所以只有非常小的访问开支

(2)InnoDB存储引擎

该存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。但是对比MyISAM引擎,写的处理效率会差一些,并且会占用更多的磁盘空间以保留数据和索引。 
InnoDB存储引擎的特点:支持自动增长列,支持外键约束

(3):MEMORY存储引擎

Memory存储引擎使用存在于内存中的内容来创建表。每个memory表只实际对应一个磁盘文件,格式是.frm。memory类型的表访问非常的快,因为它的数据是放在内存中的,并且默认使用HASH索引,但是一旦服务关闭,表中的数据就会丢失掉。 
MEMORY存储引擎的表可以选择使用BTREE索引或者HASH索引,两种不同类型的索引有其不同的使用范围

Hash索引优点: 
Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。 
Hash索引缺点:那么不精确查找呢,也很明显,因为hash算法是基于等值计算的,所以对于“like”等范围查找hash索引无效,不支持;

Memory类型的存储引擎主要用于哪些内容变化不频繁的代码表,或者作为统计操作的中间结果表,便于高效地对中间结果进行分析并得到最终的统计结果,。对存储引擎为memory的表进行更新操作要谨慎,因为数据并没有实际写入到磁盘中,所以一定要对下次重新启动服务后如何获得这些修改后的数据有所考虑。

(4)MERGE存储引擎

Merge存储引擎是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,merge表本身并没有数据,对merge类型的表可以进行查询,更新,删除操作,这些操作实际上是对内部的MyISAM表进行的。

第二周

 

  1. Sql优化

Sql优化首先需要找到需要优化的sql,也就是执行比较慢的sql语句,我们在项目中主要用mysql数据库较多,以mysql数据库为例,可以采用开启mysql慢日志,通过set global slow_query_log=1语句开启慢查询日志,通过show variables like ‘%slow_query_log%’ 查看慢查询日志开启状态和存储位置,通过设置long_query_time的时间,来界定执行时间超过多久的sql为慢查询sql,设置log-queries-not-using-indexes可以把未使用索引的sql语句也输出到慢查询中。也可以使用第三方数据库连接池比如德鲁伊(druid),设置logSlowSql属性为true,通过slowSqlMillis设置慢sql的界定时间。查找到执行较慢的sql语句之后,根据具体问题对sql语句进行分析,导致sql语句执行较慢的因素主要有

  1. 过多的表联查

在拿到执行较慢的sql语句之后,分析其有无冗余的表联查,若存在,则去掉,使用嵌套查询解决多表直接联查问题,可以把多个表的查询结果分开执行,然后再把结果合并到一块。若无法解决表联查问题,可以把表的联查结果写入mongoDB这样的noSQl数据库中,查询时直接走NoSql数据库获取表联查结果,这种适用于查询较多,写入请求较少的情况。

  1. 数据量较大的表查询未使用索引,或使用了索引,但是索引未生效。

首先使用sql的执行计划查看当前sql语句有无使用索引,使用索引查询的执行类型是什么,使用EXPLAIN关键字对sql语句进行分析,返回结果中的TYPE字段表示索引的使用情况,ALL代表全表扫描,也就是未使用索引,const表示主键索引或唯一索引,查询效率最高,ref引用查找,查找非唯一索引匹配项,range索引范围查找。Key代表当前查询所使用的实际索引。根据sql执行计划分析索引的使用情况,对于有条件的表(数据量超过10万条以上,查询较频繁)添加索引,对于查询添加了所有,应注意一下几点,a.添加索引的字段必须为where或group by中使用的字段,否则sql语句不会执行索引,多个字段同时查询可以考虑组合索引。b.添加索引的字段不能进行运算操作,因为索引的使用是在sql编译器编译时选择的,若对索引字段进行运算,其值只有在sql语句执行时才能确定,所以会导致索引失效。c使用like模糊查询时前后都有%或前面有%时则索引失效,使用组合索引时第一个字段匹配则走索引,第一个字段不匹配则索引失效,各个字段都匹配索引效率最高。字段顺序尽量与索引顺序保持一致d.使用in或not in 也会使索引失效,若非必须使用,可以考虑使用其它方式替代,例如between或exists。E 加索引的字段不能为空值。F 一个表的索引尽量不要超过6个,否则会降低写入表的性能。

  1. 一次查询返回的数据量较大

考虑需求的合理性,采用分页减少数据量的返回

  1. 不同数据库之间Sql语句解析器解析方式不同

Mysql查询采用自上而下查询,过滤掉最大数据量的条件必须紧跟where子句之后,缩小查询的范围,例如查询北京市有多少男性用户,则要先查询地址为北京,再查询性别。Oralce采用自下而上的查询方式,过滤最大数据量的条件必须放在where子句的末尾。

  1. 数据库优化

  当数据库数据量逐渐增多,sql优化不足以满足系统需求时,则要对数据库进行优化,平时使用mysql数据库较多,以mysql数据库为例分析,数据库优化主要分为

  1. 数据库配置优化

选择合理的数据库存储引擎,mysql常用的存储引擎主要有两种,一个是MyISAM,不支持事务处理,读性能处理快,表级别锁。另一个是InnoDB,支持事务处理(ACID),设计目标是为处理大容量数据发挥最大化性能,行级别锁。考虑到数据库事务问题,我们选择使用的是InnoDB,以InnoDB为例设置数据库相关参数,innodb_buffer_pool_size设置索引和数据缓冲区大小,一般设置为物理内存的60%,innodb_flush_log_at_trx_commit关键参数,0代表大约每秒写入到日志并同步到磁盘,数据库故障会丢失1秒左右事务数据。1为每执行一条SQL后写入到日志并同步到磁盘,I/O开销大,执行完SQL要等待日志读写,效率低。2代表只把日志写入到系统缓存区,再每秒同步到磁盘,效率很高,如果服务器故障,才会丢失事务数据。对数据安全性要求不是很高的推荐设置2,性能高,修改后效果明显。innodb_file_per_table = OFF  默认是共享表空间,共享表空间idbdata文件不断增大,影响一定的I/O性能。推荐开启独立表空间模式,每个表的索引和数据都存在自己独立的表空间中,可以实现单表在不同数据库中移动

  1. 系统内核优化

我们的mysql部署在64位的linux系统中,系统为centos7,对linux内核进行适当的优化也能提高数据库性能。net.ipv4.tcp_fin_timeout设置连接超时时间,避免过长的错误等待,net.ipv4.tcp_tw_reuse = 1 允许TIME_WAIT socket重新用于新的TCP连接,减少开销。设置net.ipv4.tcp_tw_recycle=1,开启TIME_WAIT socket快速回收。设置net.ipv4.tcp_max_tw_buckets增大TIME_WAIT socket数量, 设置net.ipv4.tcp_max_syn_backlog,进入SYN队列最大长度,加大队列长度可容纳更多的等待连接

 

  1. 硬件配置

增大物理内存,通过文件系统延迟写入机制使数据暂时先存入内存中增加写入效率,使用固态硬盘代替传统机械硬盘。

  1. 数据库架构扩展

随着业务量的增大,单节点数据库无法满足业务需求,考虑做数据库集群

  1. 增加缓存

使用缓存适当降低数据库I/O访问频率,把操作较多的数据存入缓存库中。减少对数据库的访问。

  数据库的分表分库策略:

        分表:

横切割:数据量过大时,把表按照数据规则进行分割

       常见分表策略,

1:按照时间区间分割,将不同年或月的数据存入不同的数据库中,视单位时间内数据的产生量而定。

2:使用hash取模分表,根据表中的一个关键字段值例如用户id进行hash运算后取模,得到固定余数,根据得到的数值存入不同的数据库中,

纵切割:表中字段过多,但是常用的字段比较固定,此时就可以根据字段把表分成两个一对一的表,经常被查询的字段分为一张表,不经常查询的字段放在另外一张表

例如user表和user_info表,用户表要使用用户名密码登录,而很少有用户经常去查看自己的个人信息,此时就可以把表分为一个user表和一个user_info表,两张表之间使用一对一关联,当用户想看详细信息时,再去查询user_info表,类似于hibernate的懒加载机制。

分库:

水平分割:大体上和分表的横切割是一致的,就是数据量较大,按照数据规则分割

例如按照账户位数(与qq类似)分割,6位数的用户查A库,7位数的用户查B库

垂直分割:按照项目的业务逻辑进行分割例如日志相关的功能存日志库,库存相关的功能存库存库,订单相关的功能存订单库。

  1. JVM优化

1.JVM内存结构

虚拟机栈

主要存放对象的引用和局部变量,以及基本数据类型(byte,int,long等)

方法区

存放类定义 结构 方法等数据

堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden(伊甸)空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配,新创建的对象存放在Eden区,垃圾回收时优先回收伊甸区,没有回收的对象会在两个Survivor区互相copy

Pc寄存器(程序计数器):程序计数器是一个以线程私有的一块较小的内存空间,用于记录所属线程所执行的字节码的行号指示器;字节码解释器工作时,通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳准、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成,程序计数器不会有内存溢出风险,随线程的创建而产生,线程的结束而死亡。

6.JVM优化方案

 a.参数方面根据当前服务器配置设置JVM参数进行参数优化

-Xms设置堆的最小空间大小。

-Xmx设置堆的最大空间大小。

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

-XX:PermSize设置永久代最小空间大小。

-XX:MaxPermSize设置永久代最大空间大小。

-Xss设置每个线程的堆栈大小

-XX:MaxTenuringThreshold 设置对象在年轻代存活次数,大于该值则进入年老代

 

  b.GC垃圾收集器方面选用合适的垃圾回收器

Serial收集器 串行收集器 使用停止复制算法,使用一个线程进行GC,串行,其它工作线程暂停。适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上

Parallel 并行收集器 在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式

CMS 并发收集器 与并行收集器类似,响应比并行gc快很多,但是牺牲了一定的吞吐量。

c.对JVM内存进行监控

   使用Jconsole,jProfile,VisualVM等监控工具,对JVM的内存以及运行情况监控,找出对应的问题进行优化

 

Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里

 

JProfiler:商业软件,需要付费。功能强大。

 

VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。

d.减少线程的创建数量,若无法减少,则使用-Xss缩减每个线程的大小

e.硬件配置上

   根据应用所需,匹配相应配置的硬件服务器,硬件跟不上,再优化也好不到哪去。

 

4.Java IO 和 和 NIO  区别

①NIO 操作直接缓存区,直接与 OS 交互,Selector IO 复用机制。

IO NIO

面向流 面向缓冲

阻塞 IO 非阻塞 IO

无 选择器

Selector:Java NIO 的选择器允许一个单独的线程来监视多个输入通

道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程

来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准

备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多

个通道。

②NIO 与 Netty:A、NIO 的类库和 API 复杂,使用麻烦,需要熟练使

用 Selector、ServerSocketChannel、SOcketChannel、ByteBuffer 等。

B、NIO 涉及到 Reactor 模式,需要了解 Java 多线程和网络编程。C、

JDKNIO Bug-epoll bug 容易导致 Selector 空轮询,最终导致 CPU100%

占用,虽然 JDK1.6 update18 修复了这个问题,但是直到 JDK1.7 问题

依然存在,只是降低了发生的概率。

③Netty 的优点:A、API 简单,开发门槛低;B、功能强大,预置了

多种解码功能,支持多种主流协议;C、可以通过 ChannelHandler 对

通信框架进行灵活的扩展;D、性能高,Netty 的综合性能是最好的;

E、Netty 修复了一经发现了所有的 JDKNIO BUG,成熟,稳定。

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用

户线程发起 IO 请求后需要等待或者轮询内核 IO 操作完成后才能继

续执行;而异步是指用户线程发起 IO 请求后仍继续执行,当内核 IO

操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

引申:

Java 中 IO 的种类和应用场景:

A、同步阻塞式:BIO。用于连接数目较小且固定的架构,对服务器资

源占用高。

B、伪异步 IO 变成:线程池和任务队列。

C、NIO 编程:a、缓冲徐 ByteBuffer;b、通道 channel 全双工,同时

用于读写;c、多路复用器 selector。用于连接数目多且较短的架构,

如聊天服务器等,但是编程复杂,存在epoll bug,导致 Selector 空轮

询,直至 CPU 占用达到 100%,虽然在 JDK1.6 update18 中有对这个

bug 的修复,但是在 JDK1.7 中依然可能会出现这个问题,只是降低

了 bug 出现的概率。

D、AIO 编程:用于连接数目多且较长的架构,如相册服务器等,充

分调用 OS 参与并发操作,基于 JDK1.7。

阻塞和非阻塞的概念描述的是用户线程调用内核 IO 操作的方式:阻

塞是指 IO 操作需要彻底完成后才返回到用户空间;而非阻塞是指 IO

操作被调用后立即返回给用户一个状态值,无需等到 IO 操作彻底完

成。

5.事物的传播行为

我们一般都是将事务设置在Service层 那么当我们调用Service层的一个方法的时候它能够保证我们的这个方法中执行的所有的对数据库的更新操作保持在一个事务中,在事务层里面调用的这些方法要么全部成功,要么全部失败。那么事务的传播特性也是从这里说起的。

       如果你在你的Service层的这个方法中,还调用了本类的其他的Service方法,那么在调用其他的Service方法的时候,这个事务是怎么规定的呢,我必须保证我在我方法里掉用的这个方法与我本身的方法处在同一个事务中,否则如果保证事物的一致性。事务的传播特性就是解决这个问题的,“事务是会传播的”

一言蔽之:在一个事务方法里面调用了另一个含有事务的方法,事务就会传播。

此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。

TransactionDefinition.PROPAGATION_REQUIRES_NEW:无论当前方法中有没有事务都会创建一个新的事务,如果当前方法中存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

第三周

 

1.Hadoop启动哪些进程,它们的作用分别是什么/strong>

1)NameNode它是hadoop中的主服务器,管理文件系统名称空间和对集群中存储的文件的访问,保存有metadate。

2)SecondaryNameNode它不是namenode的冗余守护进程,而是提供周期检查点和清理任务。帮助NN合并editslog,减少NN启动时间。

3)DataNode它负责管理连接到节点的存储(一个集群中可以有多个节点)。每个存储数据的节点运行一个datanode守护进程。

4)ResourceManager(JobTracker)JobTracker负责调度DataNode上的工作。每个DataNode有一个TaskTracker,它们执行实际工作。

5)NodeManager(TaskTracker)执行任务

 

2.HDFS的存储机制

HDFS存储机制,包括HDFS的写入过程和读取过程两个部分

①写入过程

1)客户端namenode请求上传文件,namenode检查目标文件是否已存在,父目录是否存在。

2)namenode返回是否可以上传。

3)客户端请求第一个 block上传到哪几个datanode服务器上。

4)namenode返回3个datanode节点,分别为dn1dn2dn3

5)客户端请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成

6)dn1dn2dn3逐级应答客户端

7)客户端开始往dn1上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,dn1收到一个packet就会传给dn2dn2传给dn3dn1每传一个packet会放入一个应答队列等待应答

8)当一个block传输完成之后,客户端再次请求namenode上传第二个block的服务器。(重复执行3-7步)

读取过程

1)客户端向namenode请求下载文件,namenode通过查询元数据,找到文件块所在的datanode地址。

2)挑选一台datanode(就近原则,然后随机)服务器,请求读取数据

3)datanode开始传输数据给客户端(从磁盘里面读取数据放入流,以packet为单位来做校验)。

4)客户端以packet为单位接收,先在本地缓存,然后写入目标文件。

3.Secondary namenode工作机制

1)第一阶段:namenode启动

(1)第一次启动namenode格式化后创建fsimageedits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。

(2)客户端对元数据进行增删改的请求

(3)namenode记录操作日志,更新滚动日志

(4)namenode在内存中对数据进行增删改查

2)第二阶段:Secondary NameNode工作

(1)Secondary NameNode询问namenode是否需要checkpoint。直接带回namenode是否检查结果。

(2)Secondary NameNode请求执行checkpoint

(3)namenode滚动正在写的edits日志

(4)将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode

(5)Secondary NameNode加载编辑日志和镜像文件到内存,并合并。

(6)生成新的镜像文件fsimage.chkpoint

(7)拷贝fsimage.chkpointnamenode

  1. namenode将fsimage.chkpoint重新命名成fsimage

 

4.HDFS优点

1.容量可以先行扩展

2.有了副本机制,存储可靠性高,吞吐量增大

3.有了NameNode后,客户端访问文件只需要制定HDFS上的路径

4.支持超大文件。

超大文件在这里指的是几百M,几百GB,甚至几TB大小的文件。一般来说hadoop的文件系统会存储TB级别或者PB级别的数据。所以在企业的应用中,数据节点有可能有上千个。

5.检测和快速应对硬件故障

在集群的环境中,硬件故障是常见的问题。因为有上千台服务器连接在一起,这样会导致高故障率。因此故障检测和自动恢复是hdfs文件系统的一个设计目标。

6.流式数据访问

Hdfs的数据处理规模比较大,应用一次需要访问大量的数据,同时这些应用一般都是批量处理,而不是用户交互式处理。应用程序能以流的形式访问数据集。主要的是数据的吞吐量,而不是访问速度。

7.简化的一致性模型

大部分hdfs操作文件时,需要一次写入,多次读取。在hdfs中,一个文件一旦经过创建、写入、关闭后,一般就不需要修改了。这样简单的一致性模型,有利于提高吞吐量。

 

5.HDFS实现机制

1.文件是被切块存储在多台服务器上,存在各台服务器的本地文件系统中

2.对于客户端来说,不需要关心分布式的细节,HDFS提供了一个统一的抽象的路径

3.每一个文件块都可以保存多个版本

4.Hdfs中的文件和具体实际存储位置之间的对应关系交由一个专门的服务器来管理(NAMENODE)

第四周

1.HashMap底层

数组结构:数组的空间复杂度高,数组的二分查找法使数组时间复杂度低,所以随机查找容易,增加和删除比较困难,链表存储结构比较离散,空间复杂度低,时间复杂度大,所以其寻址困难,随机插入和删除容易,而hashMap则采用了两者的优势,结合了数组和链表的特性,使其变成了一种即查找容易,又增删容易的数据结构——哈希表结构,在hashMap底层采用了链表数组结构。

hashMap底层基于Entry(jdk1.8之后采用Node,Node继承了Entry)数组实现,在hashMap内部有一个静态的Entry内部类,其属性是key value 和next,在hashMap的内部中有一个Entry[]的线性数组结构,Entry对象在存入Entry数组时会根据Entry对象的key进行hash运算而拿到key值的hashcode码,hashCode码是一个固定的int值,取到hashCode值之后再根据当前数组长度取模,也就是key.hashCode%Entry[].length(高版本采用key.hashCode&( Entry[].length-1),位移运算效率更高),通过计算hash所得到的值就是此Entry对象存在链表数组中的下标,若根据key值所计算的数组下标一致,则在数组中存入最新存入的数据,在这里可能会担心之前存入的值会被覆盖,但是在Entry对象中有一个next属性,会把之前的Entry对象关联起来,形成一个链表结构,所有之前存入的值也不会被覆盖,也就是说假如ABC三个Entry对象的key经过hash所计算的下标都为0时,在链表数组中存储的结构就是C.next=B,B.next=A,Entry[0]=C 数组的下标所指向的是最新存入的Entry对象,而根据最新存入的Entry对象的next属性,可以找到之前存入的Entry对象,此种方式被称为拉链法,链表数组的默认长度是16,当数组下标达到初始下标的0.75倍时,则会把链表数组散列扩展两倍。当hashMap在调用get方法时,先根据key值进行hash散列算出其在链表数组种的下标,算出下标之后根据下标返回Entry对象,判断当前Entry对象的key值是否与参数中的key值一致,若不一致则遍历当前下标所关联的Entry链表,直到找到对应的key值,若找不到则返回null。

 

注释(也要背会):

   Hash碰撞:通过hash值计算Entry数组下标,所得到的下标一致即为hash碰撞,jdk1.8之前采用数组+链表的形式解决hash碰撞,当hash碰撞过多后,链表形式的时间复杂度就会增加,所以jdk1.8之后改用红黑树形式来解决hash碰撞,当hash碰撞节点较少时依然采用链表数组形式存储,当碰撞节点较多时(>8个)采用红黑树解决hash碰撞问题。

   空间复杂度:空间复杂度是对一个算法在运行过程中所占用的临时内存空间大小的度量。

   时间复杂度:时间复杂度是对一个算法的运行所需要消耗时间的计算函数,先找出算法的基本操作,在找出算法的执行次数就可以算出时间复杂度

   算法的空间复杂度和时间复杂度用来衡量一个算法的效率,一般情况下,一个算法的时间复杂度和空间复杂度不可兼得。

   红黑树:红黑树是一种特殊的二叉查找树,红黑树的每个节点都有存储位来存储节点颜色,是一种自平衡二叉查找树,红黑树的特性:

(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

红黑树的特性,保证了没有一条路径比其他路径长出两倍,因此红黑树是相对平衡的二叉树,在一定程度上降低了查找的时间复杂度。

2.Java常用垃圾回收器配置

Java提供多种类型的垃圾回收器。JVM中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。

Serial收集器:新生代收集器,使用复制算法,使用一个线程进行GC,串行,其它工作线程暂停。

ParNew收集器:新生代收集器,使用复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

Parallel Scavenge 收集器:吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾。

Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。

Parallel Old收集器:吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理算法,在Parallel Old执行时,仍然需要暂停其它线程。

CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见Full GC和并发垃圾回收一节),当用户线程内存不足时,采用备用方案Serial Old收集。

3.JVM方法区介绍

方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。

Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

 

4.本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

5.Java 堆

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;

再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAlloc

来源:听风动

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2019年4月15日
下一篇 2019年4月15日

相关推荐