2018初冬阿里巴巴面试题——(部分一)

1.开发中用了比较多的数据结构有哪些?

 

原贴:https://blog.csdn.net/qq_31615049/article/details/80545713

2.谈谈你对HashMap的理解,底层的基本实现。HashMap怎么解决碰撞的问题的?

理解:

hashMap是基于哈希表的map接口的非同步实现,相对hashtable来说,是hashtable的轻量级的实现.

允许null值的出现,通过键值对来存储,主要通过get和put来操作数据的插入和查询

基本实现:

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。 

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

碰撞问题:

1.碰撞的产生

 Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {

Entry<K,V> e = table[bucketIndex];

table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

if (size++ >= threshold)

resize(2 * table.length);

}

   上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。 HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

   通过上面可知如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。也就是说我们是通过链表的方式来解决这个Hash碰撞问题的。

Hash碰撞性能分析

  Java 7:随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。不过Java 8的表现要好许多!它是一个log的曲线,因此它的性能要好上好几个数量级。尽管有严重的哈希碰撞,已是最坏的情况了,但这个同样的基准测试在JDK8中的时间复杂度是O(logn)。单独来看JDK 8的曲线的话会更清楚,这是一个对数线性分布:

Java8碰撞优化提升

   为什么会有这么大的性能提升,尽管这里用的是大O符号(大O描述的是渐近上界)?其实这个优化在JEP-180中已经提到了。如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。它是如何工作的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。这个性能提升有什么用处?比方说恶意的程序,如果它知道我们用的是哈希算法,它可能会发送大量的请求,导致产生严重的哈希碰撞。然后不停的访问这些key就能显著的影响服务器的性能,这样就形成了一次拒绝服务攻击(DoS)。JDK 8中从O(n)到O(logn)的飞跃,可以有效地防止类似的攻击,同时也让HashMap性能的可预测性稍微增强了一些。

原贴:https://blog.csdn.net/luo_da/article/details/77507315

3.对JVM熟不熟悉?简单说说类加载过程,里面执行了哪些操作?GC和内存管理,平时在tomcat里面有没有进行过相关的配置?

关于JVM和类加载过程

1、什么是JVM ?
JVM, 中文名是Java虚拟机, 正如它的名字, 是一个虚拟机器,来模拟通用的物理机。 JVM是一个标准,一套规范,  规定了.class文件在其内部运行的相关标准和规范。 及其相关的内部构成。 比如:所有的JVM都是基于栈结构的运行方式。那么不符合这种要求的,不算是JVM, 如Android中所使用的Dalvik 虚拟机就不能称作是JAVA 虚拟机, 因为它是基于寄存器(最新的Android系统据说已经放弃了Dalvik VM, 而是使用ART)。

JVM相关的产品有很多, 通常最有名的莫过于现在Oracle公司所有的HotSpot 虚拟机。因此, 这里讨论的都是HotSpot虚拟机, 如果没有特别说明。 

2、类加载?
类加载, 是通过JVM的类加载器从JVM外部以二进制字节流的方式加载到JVM中。但JVM本身有至少三种类加载器:BootStrap(根类加载器,C++实现, 加载位于jre/lib/rt.jar)、Extension(扩展类加载器, 主要用于加载jre/lib/ext/下的jar)、System(加载classpath环境变量所指定的class);当然还有,自定义的类加载器(用于实现自己的类加载器, 如Tomcat中就实现多个类加载器,用来管理不同的jar)。
如果, 我有一个HelloWorld的类需要加载, 首先类加载器会去从最底层的类加载器去验证这个类是否被加载, 如果没有, 则委托给上一次的类加载器验证是否被加载, 如果到BootStrap类加载器都没有发现HelloWorld类被加载, 那么类加载器将执行加载任务, 如果根类加载器没有加载, 则委托给下一级的Extension类加载器去尝试加载,直到这个类被加载成功。 参考下图:

需要注意的是:如果一个类被不同的类加载器加载, 那么就是两个不同的类。

3、类加载的具体过程?
被java编译器(不仅限于, 还有其他任何的可以编辑成为.class的编译器)编译过的.class文件(可能是以jar、war、jsp等形式), 经过类加载器加载 、 验证、准备、解析、初始化之后, 才可以被使用。基本的过程如下:

加载: 首先,通过一个类的全类名来获取此类的二进制字节流。其次,将类中所代表的静态存储结构转换为运行时数据结构, 最后,生成一个代表加载的类的java.lang.Class对象, 作为方法区这个类的所有数据的访问入口。加载完成之后, 虚拟机外部的二进制静态数据结构就转换成了虚拟机所需要的结构存储在方法区中(至于如何转换, 则由具体虚拟机自己定义实现), 而所生成的Class对象, 则存放在方法区中, 用来作为程序访问方法区中数据的外部接口。


验证:其目的就是保证加载进来的.class文件不会危害到虚拟机本身, 且内容符合当前虚拟机规范要求。主要验证的内容大致有:文件格式、元数据验证、字节码验证、符号引用验证。其中文件格式验证, 主要确保符合class文件格式规范(如文本后缀为.class的文件将验证不通过), 以及主次版本号, 验证是否当前JVM可以处理等。元数据验证,主要验证编译后的字节码描述信息是否符合java语法规范。字节码验证, 其最为复杂, 主要通过控制流和数据流确定语义是否合法、符合逻辑。符号引用验证,可以看做是除自身以外(常量池中各种引用符号)的信息匹配校验,如通过持有的引用能否找到对应的实例。


准备:正式为类变量分配内存,并设置类变量的初始值。这些变量都会在方法区中进行分配。


解析:将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄等。


初始化:加载的最后阶段, 程序真正运行的开始。

原贴:https://blog.csdn.net/weiguolong0306/article/details/60324988

GC和内存管理

根据JVM规范,JVM把内存划分了如下几个区域:

1. 方法区
2. 堆区
3. 本地方法栈
4. 虚拟机栈
5. 程序计数器 

其中,方法区和堆是所有线程共享的。

è¿éåå¾çæè¿°

而关于GC(垃圾回收)这里我们要介绍几种垃圾收集算法

①Mark-Sweep(标记-清除)算法

  这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

  标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。 

  ②.Copying(复制)算法

  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

  这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。 很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。我们的新生代GC算法采用的是这种算法

  ③Mark-Compact(标记-整理)算法

  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。在一般厂商JVM         中老年代GC就是使用的这种算法,由于老年代的特点是每次回收都只回收少量对象。

下面有几种创建的垃圾收集器,用户可以根据自己的需求组合出新年代和老年代使用的收集器

新生代GC :串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

  串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

  并行回收GC:在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

     并行GC:与老年代的并发GC配合使用。

老年代GC:串行GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)

   串行GC(Serial MSC):client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。

   并行GC(Parallel MSC):吞吐量大,但是GC的时候响应很慢:server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。

   并发GC(CMS):响应比并行gc快很多,但是牺牲了一定的吞吐量。

原贴:https://blog.csdn.net/suifeng3051/article/details/48292193

ps:思考“GC是在什么时候,对什么东西,做了什么事情?”

  • 什么时候

  从字面上翻译过来就是什么时候触发我们的GC机制

  ①在程序空闲的时候。这个回答无力吐槽

  ②程序不可预知的时候/手动调用system.gc()。关于手动调用不推荐

  ③Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。就是

  这时候如果你们讲出新生代和老年代的话或许会更细的了解一下Minor GC、Full GC、OOM什么时候触发!

  创建对象是新生代的Eden空间调用Minor GC;当升到老年代的对象大于老年代剩余空间Full GC;GC与非GC时间耗时超过了GCTimeRatio的限制引发OOM。

  • 什么东西

  从字面的意思翻译过来就是能被GC回收的对象都有哪些特征

  ①超出作用域的对象/引用计数为空的对象。

  引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

  ②从GC Root开始搜索,且搜索不到的对象

  跟搜索算法:以一系列名为 GC Root的对象作为起点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,则就证明此对象是不可用的。

  这里会提出一个思考,什么样的对象能成为GC Root : 虚拟机中的引用的对象、方法区中的类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中jni的引用对象。

  ③从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。

  • 做什么

  不同年代、不同种类的收集器很多,不过总体的作用是删除不使用的对象,腾出内存空间。补充一些诸如停止其他线程执行、运行finalize等的说明。

ok  现在来回答一下我们最上面的问题,上面时候容易发生内存泄露

  ①静态集合类像HashMap、Vector等

  ②各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

  ③监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

关于tomcat的相关配置,https://www.cnblogs.com/xuwc/p/8523681.html 这篇文章有详细的介绍。

4.http协议,get和post的基本区别?tcp/ip协议,三次握手。

GET和POST是什么?HTTP协议中的两种发送请求的方法。

HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。

HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。 

在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。

但是,我们只看到HTTP对GET和POST参数的传送渠道(url还是requrest body)提出了要求。“标准答案”里关于参数大小的限制又是从哪来的呢?

大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。

好了,现在你知道,GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。 

GET和POST还有一个重大区别,简单的说:

GET产生一个TCP数据包;POST产生两个TCP数据包。

长的说:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?

1. GET与POST都有自己的语义,不能随便混用。

2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

原贴:https://www.cnblogs.com/logsharing/p/8448446.html

tcp/ip

TCP/IP 是一类协议系统,它是用于网络通信的一套协议集合.
传统上来说 TCP/IP 被认为是一个四层协议


1) 网络接口层:
主要是指物理层次的一些接口,比如电缆等.

2) 网络层:
提供独立于硬件的逻辑寻址,实现物理地址与逻辑地址的转换.

在 TCP / IP 协议族中,网络层协议包括 IP 协议(网际协议),ICMP 协议( Internet 互联网控制报文协议),以及 IGMP 协议( Internet 组管理协议).

3) 传输层:
为网络提供了流量控制,错误控制和确认服务.

在 TCP / IP 协议族中有两个互不相同的传输协议: TCP(传输控制协议)和 UDP(用户数据报协议).

4) 应用层:
为网络排错,文件传输,远程控制和 Internet 操作提供具体的应用程序


三次握手

简单举个例子,A(client)打电话给B(server),

第一次:A跟B说:喂,你听得到吗?

第二次:B跟A说:听得到,你听得到吗?

第三次:A跟B说:听得到。

这样的三次握手之后,client跟server就可以进行正常的通话了。

猜你喜欢

转载自blog.csdn.net/qq_33471815/article/details/83856001