Java 面试题基础概念收集(高级)

时间:2023-12-29 19:28:44

JVM垃圾回收:

GC又分为 minor GC 和 Full GC (也称为 Major GC )。Java 堆内存分为新生代和老年代,新生代中又分为1个 Eden 区域 和两个 Survivor 区域。

那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。

GC主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。

例如新生代采用了复制算法,老年代采用了标记整理法。在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。

Callable和Runnable

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值

FutureTask是一个具体的实现类,ThreadPoolExecutor的submit方法返回的就是一个Future的实现,这个实现就是FutureTask的一个具体实例,FutureTask帮助实现了具体的任务执行,以及和Future接口中的get方法的关联。

execute和submit

submit返回的最终是FutureTask对象

submit内部调用execute

submit有返回值

submit方便exception处理

线程池流程:

1、如果线程池的当前大小还没有达到基本大小(poolSize < corePoolSize),那么就新增加一个线程处理新提交的任务;
2、如果当前大小已经达到了基本大小,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
3、如果队列容量已达上限,并且当前大小poolSize没有达到maximumPoolSize,那么就新增线程来处理任务;
4、如果队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。

HashMap的put流程:

当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。

若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。

如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。

如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头(最先保存的元素放在链尾),新元素设置为Entry[0],其next指针指向原有对象,即原有对象为Entry[1]

HashMap扩容:

初始容量,加载因子。

这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,

加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;

如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

ConcurrentHashMap原理:

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。

Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。

一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,

一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,

每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

CopyOnWrite原理:

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象 。

如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。

  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如ConcurrentHashMap.

  数据一致性问题CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

链表和数组的区别:

首先,二者都属于数据结构的范畴。
数组一旦初始化,长度就不能改变。链表长度可以改变,可以动态的增加节点数据,操作比较灵活;
数组是可以有一维二维三维。。。属于非线性结构,链表是线性结构;
数组访问元素依靠下角标,比如查找第n是数据,直接arrn-1,时间复杂度是O(1)。链表访问元素得从头开始依次查找,根据引用找到下一个节点,时间复杂度是O(n);
数组插入删除中间数据比较麻烦,时间复杂度是O(n)。链表插入删除比较方便,时间复杂度是O(1);
所以如果需要快速访问数据并且涉及很少插入删除操作那么就选用数组。如果涉及比较多的删除插入那么选用链表;
数组在栈内存中存储引用,在堆内存中存储对象。链表存储在堆内存中,由于存储在堆内存中,需要手动分配内存;
数组在内存中是连续存储的,链表不是连续的。

Spring如何控制事务?

1.IOC控制反转:控制权由对象本身转向容器,由容器对bean对象进行控制。
2.AOP面向切面编程:把具体的类创建对应的代理类,通过代理类来对具体类进行操作。
spring是一个容器,通过spring这个容器来对对象进行管理,根据配置文件来实现spring对对象的管理。

spring的事务是通过“声明式事务”的方式对事务进行管理,即在配置文件中进行声明,通过AOP将事务切面切入程序。最大的好处是大大减少了代码量,

比如:

Transaction tx=session.getTransaction();
session.beginTransaction();
tx.commit();
tx.rollback();

SpringMVC原理:

http://maosheng.iteye.com/blog/2270687

http://www.cnblogs.com/lanxuezaipiao/p/3371224.html

http://ifeve.com/falsesharing/