阿里巴巴面试题总结

1. JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括哪几个阶段?

答:7个阶段,分别是:加载、验证、准备、解析、初始化、使用和卸载。

 

2. java有多少个类加载器?分别的作用是什么?一个Class文件是怎么被加载到JVM里的,描述一下加载流程。

答:java有三个类加载器,分别为:根类加载器,扩展类加载器,系统类加载器。

根类加载器负责java核心类的加载,扩展类加载器负责扩展jar包的加载;系统类加载器负责自定义类的加载。

类加载的全过程包括加载验证准备解析初始化5个阶段。其中,验证、准备、解析三个部分统称为连接。

加载阶段。虚拟机利用类加载器将Class文件加载到内存中,准确的讲,是加载到内存中的方法区,并为这个Class文件生成一个Class文件对象(类的字节码对象),作为方法区中这个Class文件的访问入口。(注意,Class文件存放在方法区,但是Class文件对象并不一定存放在堆中,还可能在方法区中)

验证阶段。这一阶段的目的是为了确保Class文件的中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备阶段。这一阶段是正式在方法区内为类变量分配内存,并设置初始值。

这里有两个特别容易混淆的概念需要强调一下。首先,这个时候进行内存分配的类变量指的仅是静态变量,而不是成员变量(实例变量),成员变量将会在对象实例化时随着对象一起分配在堆内存中。其次,这里的初始值指的是数据类型的零值(假设有一个类的静态变量为static int value = 123,那变量value在准备阶段过后的初始值为0而不是123)。

解析阶段。这一阶段是将类文件中的符号引用替换为直接引用。

符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在java中,一个java类将会被编译成一个Class文件。在编译时,java类并不知道所引用的类实际地址,因此只能使用符号引用来替代。比如People类引用了Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个符号,当然,实际不是)来表示Language类的地址。

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。直接引用就是类的实际内存地址,如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化阶段。执行类中定义的java代码,初始化类变量和其它资源(假设有一个类的静态变量为static int value = 123,准备阶段过后,value初始值为0,而初始化阶段,就是执行java代码,将123赋给valuevalue的初始值为123)。

 

1. 简介一下JVM的内存机制。

答:为了提高程序的效率,JVM中的内存具体划分了如下5个内存空间:

:主要存放局部变量。栈内存描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

注意:栈内存存在内存溢出问题。

:存放的是所有new出来的东西。包括数组和对象实例(成员变量及类中方法在方法区中的内存地址)。

注意:并不是所有的对象都会放在堆内存中;堆内存是垃圾回收器管理的主要区域;堆内存存在内存溢出问题。

方法区:被虚拟机加载的Class文件,常量(运行时常量池),静态变量,静态方法,成员方法。

注意:方法区中有一部分叫运行时常量池,该常量池中存放着常量以及直接声明的String字符串;方法区也会被垃圾回收器管理;方法区存在内存溢出问题。

本地方法栈:本地方法栈与栈所发挥的作用非常相似,它们之间的区别不过是栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

注意:本地方法栈存在内存溢出问题。

程序计数器:程序计数器是一块较小的内存空间,是“线程私有”的内存空间。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

注意:程序计数器主要用来管理线程;程序计数器不存在内存溢出问题。

 

2. 在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样的一个过程呢?

答:虚拟机遇到一条new指令时,首先检查相应的类文件是否已被加载到内存中,如果没有,就必须先执行相应的类加载过程。

在类加载完毕后,虚拟机将会在堆内存中为新生对象分配内存空间。

内存分配完毕后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置(具体做了哪些设置,不需要知道)。

在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,因为这时所有的字段都还为零。所以,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

 

3.  synchronized关键字和Lock接口有什么区别?(阿里巴巴面试)

答:1.sychronized是关键字,Lock是接口;

2.sychronized关键字修饰的同步代码块或同步方法,锁的获取和释放,并不直观;而使用Lock接口,可以指定获取锁和释放锁的位置,比较直观。

3.synchronized关键字是托管给JVM执行的,在发生异常时,会自动释放锁,因此不会出现死锁;而Lock通过代码来操作,在发生异常时,不会自动释放锁,可能出现死锁,所以我们一般将Lock释放锁的操作放在finally块里。

4.代码写法上有区别。sychronized关键字实现同步,是通过同步代码块或同步方法实现的,在代码块上或方法上加sychronized关键字进行修饰。而Lock接口实现同步,是通过调用其lock()unlock()方法实现的,并且lock()unlock()方法要配合try/finally语句块来完成。

5.相比于synchronized关键字,Lock接口功能更高级。比如:等待可中断,可实现公平锁,以及锁可以绑定多个条件。

 

23.你了解多线程中的volatile关键字吗?简单介绍一下。

答:volatile关键字是Java虚拟机提供的最轻量级的同步机制。通过直接修饰共享变量,可以保证共享变量在内存中的可见性。

     什么是内存可见性?

    “可见性”指的是当一条线程修改了共享变量的值时,其他线程可以立即得知这个修改。

     什么是内存不可见?

      在多线程中,程序运行时,共享变量会存放在主内存中,每个线程在创建的时候都会创建一个属于自己的工作内存,它会将主内存中的共享变量保存一份在自己的工作内存中,这样一来就很容易造成当一个线程对共享变量进行修改时,另一个线程不知道的情况,即变量在内存中彼此不可见。 造成这个问题的原因是因为:普通变量的值在线程间传递均需要通过主内存来完成,即当线程A对共享变量进行修改后,需要同步到主内存中,而线程B在获取共享变量时,获取到的是修改前的值,而不是修改后的值。

        而当共享变量被volatile关键字修饰后,就能够保证共享变量的内存可见性了

      volatile关键字如何保证共享变量的内存可见呢?

      volatile保证共享变量内存可见性的原理是在线程每次访问共享变量时都要进行一次刷新,线程访问共享变量访问的是它本身工作内存中拷贝的主内存的那一份,刷新保证了线程每次访问之前,工作内存都要重新从主内存中拷贝一份共享变量,这就保证每次在工作内存中访问到的共享变量都是主内存中的最新版本。

     注意:线程对共享变量进行修改,要分两步分,第一步是线程对工作内存中的共享变量副本进行修改,第二步是将工作内存中修改后的共享变量同步到主内存中,只有这两步全部完成,才实现了线程对共享变量的修改操作。

所以,这就出现了一种情况:主内存中的共享变量volatile int i=1,假如两个线程同时对共享变量进行修改,线程Ai =2,修改了工作内存中的共享变量,还没有同步到主内存中,这时,线程B抢到了CPU的执行权,也开始对共享变量进行修改,修改之前先刷新,获取到的最新的i=1,修改为i=3,并同步到主内存中,主内存i=3,这时,CPU执行权被线程A抢走了,主内存i=2,最后发现i=2。有人就有疑问了?我觉着i应该为3呀,为什么是2?这不就是共享变量并没有内存可见嘛!你看,我线程A先修改的i = 2,我修改完之后,线程B刷新获取到的值还是i=1,并不是i=2,这不是没有实现内存可见吗?我的回答是这样:我前面说过了,对共享变量的修改分两步,第一步是修改工作内存中的共享变量副本,第二步是将修改结果同步到主内存中去。上面的情况,当线程B抢到CPU执行权,对共享变量进行修改时,线程A还没有完成对共享变量的修改,所以这是主内存中i=1就是最新版本,线程B刷新获取到i=1并没有违背内存可见性。

 

24.你了解多线程中的synchronized关键字吗?简单介绍一下。

答:当多个线程操作同一资源的时候,容易产生线程安全问题,解决线程安全问题的方法是同步,synchronized关键字是同步的一种方式。它可以加在代码块上构成同步代码块,也可以加在方法上构成同步方法,用synchronized关键字解决同步问题需要锁对象,同步代码块的锁对象是任意对象,同步方法的锁对象是this对象,同步静态方法的锁对象是类的字节码对象。

 

25.volatile相较于synchronized的区别是什么?

答:1.volatile可以直接修饰变量,而synchronize不可以直接修饰变量

       2.volatile只能使用在变量级别(通过直接修饰变量实现),而synchronized既可以变量级别,也可以使用在方法和类级别(通过同步代码块和同步方法实现)。

     3.volatile只能保证变量的内存可见性,不能保证变量的原子性;而sychronized既可以保证变量的内存可见性,也可以保证变量的原子性。

 

为什么volatile关键字不能保证变量的原子性?原因何在?

答:原因是volatile关键字修饰的变量,如果当前值与该变量以前的值相关,则volatile关键字不起作用。例如,有一个共享变量volatile int i = 1;在多个线程中执行了一下操作:i++i = i+1,这时,volatile不能保证i值的内存可见性。

 

为什么说HTTP协议是无状态协议?

答:HTTP协议是无状态协议,指的是HTTP协议不对之前发生过的请求和响应的状态进行保存。

    HTTP协议的无状态性是有其优点的。首先,因为不需要对之前发生过的请求和响应的状态进行保存,所以减少了对内存资源的消耗。然后,无状态性意味着简单,因为简单,所以被广泛的应用在各种场景里。

    当然,HTTP协议的无状态性也是有缺点的。HTTP协议不对之前发生过的请求和响应的状态进行保存,就导致它无法根据之前的状态进行本次的请求处理。典型的例子就是在登录的时候,由于HTTP协议无法对之前登录的状态进行保存,就导致每次跳转到新页面的时候都要求再次登录。

保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了Cookie技术。

 

一个完整的HTTP请求发送成功所经过的过程(越详细越好)。

答:1.浏览器地址栏输入URLwww.baidu.com,浏览器自动默认为http://www.baidu.com:80/,浏览器会自上而下,经过TCP/IP协议栈中的应用层、传输层、网络层、链路层对该URL进行解析。

2.应用层,根据URL,按照HTTP协议规范生成HTTP请求报文,利用DNS(域名解析器)将域名www.baidu.com解析成IP地址。

3.传输层,HTTP请求报文传入传输层,根据URL,确定传输协议采用TCP协议,端口号为80,并将TCP协议、端口号80以标签的形式打在HTTP请求报文上。

4.网络层,HTTP请求报文传入网络层,获取到应用层解析到的IP地址,并将IP地址以标签的形式打在HTTP请求报文上。

5.链路层,HTTP请求报文传入网络层,根据IP地址解析出MAC地址,并将MAC地址以标签的形式打在HTTP请求报文上。至此,HTTP请求报文已具备所要访问服务器的IP地址、MAC地址、端口号以及传输协议TCP。首先,通过TCP协议三次握手建立浏览器客户端与服务器端之间的传输通道,然后将HTTP请求报文发送给服务器。

6.服务器链路层接收到该HTTP请求报文后,至上而下,依次经过链路层、传输层、网络层、应用层,最终应用层获取到HTTP请求报文,并对其进行解析,生成HTTP响应报文,返回给浏览器端。

7.浏览器接收到返回回来的HTTP响应报文信息之后,要对其进行解析处理,并且通过TCP四次握手关闭与服务器之间的连接。(注意:HTTP1.1版本采用连接持久化,即只要任意一端没有明确提出断开连接,则保持TCP连接状态。报文信息中包含首部字段Connection,默认为Connectionkeep-alive,当报文信息中的Connection:close时,关闭连接状态)

 

只是通过IP地址,就可以唯一定位互联网上的一台计算机吗?

答:我认为这种说法不准确。对于互联网而言,要想唯一定位一台计算机,同时需要IP地址和MAC地址。但是对于用户而言,只需要知道IP地址就可以唯一定位到计算机了,这是因为,通过IP地址找到对应的MAC地址,这个过程,是计算机内部实现的,用户看不到。

       IP地址是网络节点的地址,MAC地址是计算机网卡的地址。二者是一对一的关系。通过IP地址只能找到网络节点,但可能有多台计算机共用一个网络节点(路由器的功能),只有再通过MAC地址才能唯一确定这台计算机。

       IP地址和MAC地址的关系就好像门牌号和户主身份证号的关系一样。通过门牌号只能找到这套房子,但是这套房子里可能同时住着好几个人(合租),只要再通过户主身份证号才能唯一确定户主这个人。

 

4. 你知道哪些垃圾回收算法,垃圾回收的时候怎么判断一个对象是否可以被回收?

:常见的垃圾回收算法有标记-清除算法复制算法标记-整理算法分代收集算法

标记-清除算法:是最基础的垃圾回收算法。如其名一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说是最基础的垃圾回收算法,是因为后面几个都是基于它的思路并改其不足而得到的。

缺点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法:为了解决效率问题,一种称为“复制”的算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价未免太高了一点。

现在的商业虚拟机都采用这种收集算法来回收新生代。但并不是按1:1的比例来划分内存空间。我们发现,新生代中的对象98%都是“朝生夕死”的,所以不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将EdenSurvivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认EdenSurvivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%80%+10%),只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

内存的分配担保是指,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

缺点:复制回收算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-整理算法:根据老年代的特点,有人提出了另外一种“标记—整理”算法。标记过程仍然与“标记—清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:当代商业虚拟机的垃圾收集算法都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆内存划分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”算法或者“标记—整理”算法来进行回收。

 

Java中,垃圾回收的时候怎么判断一个对象是否可以被回收?

答:有人会说,用引用计数算法来实现,思路如下:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

这种回答是错误的,引用计数算法实现简单,判定效率也很高,也有一些比较著名的应用案例,例如微软的COM技术、Python语言和在游戏脚本领域被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。但是java虚拟机中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

JavaC#)等主流商用程序语言都是使用可达性分析算法来判定对象是否存活的。这个算法的基本思路如下:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的。

Java语言中,可作为GC Roots的对象包括下面几种:

1. 虚拟机栈中引用的对象。

2. 方法区中类静态属性引用的变量。

3. 方法区中常量引用的对象

4. 本地方法栈中JNI(即一般中的Native方法)引用的对象。

 

Java中,如果垃圾回收器判断一个对象是可以被回收的,是不是就会立即对其进行回收呢?

答:不是。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程,也就是说要通过可达性分析算法对其进行两次判断,如果两次判断的结果都是可回收,那么就会回收。

 


猜你喜欢

转载自blog.csdn.net/lz1170063911/article/details/80261895