一些面试基本知识(JAVA篇二)

进程与线程之间的联系与区别

参考: 操作系统

从操作系统讲起吧

进程
在多任务系中,每一个独立运行的程序就是一个进程,也可以理解为当前正在运行的每一个程序都是一个进程。具体来说,进程由以下方面组成:
1. 至少一个可执行程序,包括代码和初始数据,一般在进程创建时说明。注意可执行程序可以被多进程共享。
2. 一个独立的进程空间 ,在进程创建时由操作系统分配。
3. 系统资源,指在进程创建时及执行过程中,由操作系统分配给进程的系统资源,包括I/O设备、文件等。
4. 一个执行栈区 ,包含运行现场信息,如子程序调用时所压的栈帧、系统调用时所压的栈帧等。

进程创建时,会在用户进程空间定义一个用户栈,用来在用户态运行时保存用户程序现场。在操作系统内核嵌入用户进程中运行的操作系统结构中,系统还会为进程在操作系统核心空间分配一个核心栈,用来保存中断/异常点现场及在进程运行核态程序后的转子现场。逻辑上进程的用户栈和核心栈都是属于一个执行栈区。

操作系统为了管理和控制一个进程必须建立一个表格,描述该进程的存在及状态。这个表格被称为进程控制块(Process Control Block,PCB).存放进程标识、空间、运行状态、资源使用等信息。

进程在它的生命周期中,通常有5种状态:
1. 创建状态
2. 就绪状态
3. 运行状态
4. 阻塞状态
5. 终止状态

image

线程
如果说在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序再并发执行时所付出的时空开销,使OS具有更好的并发性。线程是“进程”中某个单一顺序的控制流,也被称为轻量进程

进程与线程的比较:

  1. 调度。进程是资源拥有的基本单位,线程是调度和分派的基本单位。
  2. 并发性。进程之间可以并发执行,在一个进程中的多个线程之间也可以并发执行。
  3. 拥有资源。 进程可以拥有资源,是系统中拥有资源的一个基本单位。而线程自己不拥有系统资源,但它可以访问其隶属进程的资源。
  4. 系统开销 系统创建进程需要为该进程重新分配系统资源,但创建线程的代价很小。因此多线程的实现多任务并发比多进程实现并发的效率高

线程、多线程相关,对已弃用stop()方法的认识,如何安全的终止一个线程。

参考:http://www.cnblogs.com/absfree/p/5327678.html
这篇文章介绍多线程特别详细,在此总结一些

为啥要多线程?

1、并发并行

在操作系统里了解,“多进程”并不是真正多个进程同时在跑,
而是通过CPU时间分片,快速在进程切换而模拟出多进程。这种情况就是并发

但是现在计算机都是多核CPU了,这个时候能真正实现多个进程并行执行了,这种情况就是并行

综合以上,我们知道,并发是一个比并行更加宽泛的概念。也就是说,在单核情况下,并发只是并发;而在多核的情况下,并发就变为了并行。下文中我们将统一用并发来指代这一概念。

2、阻塞与非阻塞

Unix系统内核提供了一个read函数,用来读取文件的内容。read系统调用默认会阻塞,也就是说系统会一直等待这个函数执行完毕直到它产生一个返回值。

那这个时候,由于输入比较慢,系统不能干等吧,那就让这个进程等着,去调度别的进程来执行。等到磁盘准备好了可以让我们来进行I/O了,它会发送一个中断信号通知操作系统,这时候操作系统重新调度原来的进程来继续执行read函数。这就是通过多进程实现的并发。

3、多进程VS多线程

在上面介绍里知道进程线程的联系和区别了,进程就是一个执行中的程序实例,而线程可以看作一个进程的最小执行单元。线程与进程间的一个显著区别在于每个进程都有一整套变量,而同一个进程间的多个线程共享该进程的数据。

多进程实现的并发通常在进程创建以及数据共享等方面的开销要比多线程更大,线程的实现通常更加轻量,相应的开销也就更小,因此在一般客户端开发场景下,我们更加倾向于使用多线程来实现并发。

然而,有时候,多线程共享数据的便捷容易可能会成为一个让我们头疼的问题,我们在后文中会具体提到常见的问题及相应的解决方案。在上面的read函数的例子中,如果我们使用多线程,可以使用一个主线程去进行I/O的工作,再用一个或几个工作线程去执行一些轻量计算任务,这样当主线程阻塞时,线程调度程序会调度我们的工作线程来执行计算任务,从而更加充分的利用CPU时间片。而且,在多核机器上,我们的多个线程可以并行执行在多个核上,进一步提升效率。

如何使用多线程

每个进程刚被创建时都只含有一个线程,这个线程通常被称作主线程(main thread)而后随着进程的执行,若遇到创建新线程的代码,就会创建出新线程,而后随着新线程被启动,多个线程就会并发地运行。某时刻,主线程阻塞在一个慢速系统调用中(比如前面提到的read函数),这时线程调度程序会让主线程暂时休眠,调度另一个线程来作为当前运行的线程。每个线程也有自己的一套变量,但相比于进程来说要少得多,因此线程切换的开销更小。

创建新线程

1、通过实现Runnable接口

class MyRunnable implements Runnable {
     ...
    public void run() {
         //这里是新线程需要执行的任务
    }
}

Runnable r = new MyRunnable();
Thread t = new Thread(r);

2、通过继承Thread类

class MyThread extends Thread {
    public void run() {
        //这里是线程要执行的任务
    }
}

创建了一个线程对象后,我们直接对其调用start方法即可启动这个线程:

t.start();

比较: 首先,直接继承Thread类的方法看起来更加方便,但它存在一个局限性:由于Java中不允许多继承,我们自定义的类继承了Thread后便不能再继承其他类,这在有些场景下会很不方便;实现Runnable接口的那个方法虽然稍微繁琐些,但是它的优点在于自定义的类可以继承其他的类。

stop方法为什么不安全?
其实stop方法天生就不安全,因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致,我们还是拿银行转账作为例子,我们还是从A账户向B账户转账500元,我们之前讨论过,这一过程分为三步,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性。

如何正确的终止一个线程?
我们可以采用设置一个条件变量的方式,run方法中的while循环会不断的检测flag的值,在想要结束线程的地方将flag的值设置为false就可以啦!

HashSet VS HashMap VS HashTable && LIST/SET/MAP && SynchronizedMap和ConcurrentHashMap

参考: http://www.cnblogs.com/ywl925/p/3865269.html 这篇博客详细介绍了Hash一系列的对比,值得一看!

HashMap的实现原理和底层数据结构 http://blog.csdn.net/u011202334/article/details/51496381

SynchronizedMap和ConcurrentHashMap的深入分析 http://blog.sina.com.cn/s/blog_5157093c0100hm3y.html

关于这一部分,在JAVA篇一就出现过了,但是太容易搞混了,所以又来一个加深记忆好了。

HashSet和HashMap区别

(1)HashSet是set的一个实现类,hashMap是Map的一个实现类,同时hashMap是hashTable的替代品(为什么后面会讲到).

(2)HashSet以对象作为元素,而HashMap以(key-value)的一组对象作为元素,且HashSet拒绝接受重复的对象.HashMap可以看作三个视图:key的Set,value的Collection,Entry的Set。 这里HashSet就是其实就是HashMap的一个视图。

HashSet内部就是使用Hashmap实现的,和Hashmap不同的是它不需要Key和Value两个值。

HashMapHashSet
HashMap实现了Map接口HashSet实现了Set接口
HashMap储存键值对HashSet仅仅存储对象
使用put()方法将元素放入map中使用add()方法将元素放入set中
HashMap中使用键对象来计算hashcode值HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap比较快,因为是使用唯一的键来获取对象HashSet较HashMap来说比较慢

HashTable和HashMap区别

第一,继承不同。

public class Hashtable extends Dictionary<> implements Map<>
public class HashMap  extends AbstractMap<> implements Map<>

具体可见Java API.

第二

Hashtable 中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

第三

Hashtable中,key和value都不允许出现null值。

在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

第四,两个遍历方式的内部实现上不同。

Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

第五

哈希值的使用不同,HashTable直接使用对象的hashCode,如下:

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新计算hash值。

而HashMap重新计算hash值,而且用与代替求模:

第六

Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

List 接口对Collection进行了简单的扩充,它的具体实现类常用的有ArrayList和LinkedList。你可以将任何东西放到一个List容器中,并在需要时从中取出。ArrayList从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快,而LinkedList的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。在具体应用时可以根据需要自由选择。前面说的Iterator只能对容器进行向前遍历,而 ListIterator则继承了Iterator的思想,并提供了对List进行双向遍历的方法。

Set 接口也是 Collection的一种扩展,而与List不同的时,在Set中的对象元素不能重复,也就是说你不能把同样的东西两次放入同一个Set容器中。它的常用具体实现有HashSet和TreeSet类。HashSet能快速定位一个元素,但是你放到HashSet中的对象需要实现hashCode()方法,它使用了前面说过的哈希码的算法。而TreeSet则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的,这就用到了集合框架提供的另外两个实用类Comparable和Comparator。一个类是可排序的,它就应该实现Comparable接口。有时多个类具有相同的排序算法,那就不需要在每分别重复定义相同的排序算法,只要实现Comparator接口即可。集合框架中还有两个很实用的公用类:Collections和 Arrays。Collections提供了对一个Collection容器进行诸如排序、复制、查找和填充等一些非常有用的方法,Arrays则是对一个数组进行类似的操作。

Map 是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个Map,依次类推,这样就可形成一个多级映射。对于键对象来说,像Set一样,一个Map容器中的键对象不允许重复,这是为了保持查找结果的一致性;如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。Map有两种比较常用的实现: HashMap和TreeMap。HashMap也用到了哈希码的算法,以便快速查找一个键,TreeMap则是对键按序存放,因此它便有一些扩展的方法,比如firstKey(),lastKey()等,你还可以从TreeMap中指定一个范围以取得其子Map。键和值的关联很简单,用pub (Object key,Object value)方法即可将一个键与一个值对象相关联。用get(Object key)可得到与此key对象所对应的值对象。

HashMap和Hashtable的实现原理

HashMap和Hashtable的底层实现都是数组+链表结构实现的,这点上完全一致。

添加、删除、获取元素时都是先计算hash,根据hash和table.length计算index也就是table数组的下标,然后进行相应惭怍。

加载因子 是表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.

HashMap的实现原理

- HashMap的创建
HashMap默认初始化时会创建一个默认容量为16的Entry数组,默认加载因子为0.75,同设置临界值为16*0.75

- HashMap的put()方法

HashMap会对null值key进行特殊处理,总是放在table[0]位置上的

put过程是先计算hash,然后通过hash与table.length取模计算Index值,然后将key放到table[index]位置,当table[index]已存在其他元素时,会在table[index]位置形成一个链表,将新添加的元素放在table[index],原来的元素通过Entry的next进行连接,这样以链表形式解决hash冲突问题,当元素数量达到临界值(capacity*factor)时,则进行扩容,是table数组长度变为table.length *2。

   int hash = hash(key.hashCode());//计算hash
int i = indexFor(hash, table.length);//计算在数组中的存储位置

- HashMap的get()方法

同样当key为null时会进行特殊处理,在table[0]的链表上查找key为null的元素
get的过程是先计算hash然后通过hash与table.length取摸计算index值,然后遍历table[index]上的链表,直到找到key,然后返回key的value值

containsKey和containsValue
containsKey方法是先计算hash然后使用hash和table.length取摸得到index值,遍历table[index]元素查找是否包含key相同的值

SynchronizedMap和ConcurrentHashMap

在多线程环境中,需要手动实现同步机制。因此,在Collections类中提供了一个方法返回一个同步版本的HashMap用于多线程的环境:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {   
    return new SynchronizedMap<K,V>(m);   
 }  

该方法返回的是一个SynchronizedMap 的实例。SynchronizedMap类是定义在Collections中的一个静态内部类。它实现了Map接口,并对其中的每一个方法实现,通过synchronized 关键字进行了同步控制。

java5中新增了ConcurrentMap接口和它的一个实现类ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不同的锁机制。
- Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;
- ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。默认有16个段上锁,这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

上面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作时才需要锁住整个hash表。

线程中sleep()和wait()有啥区别?各有什么含义?

参考;http://www.cnblogs.com/lancidie/archive/2011/01/24/1943401.html

1、继承不同

sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行

Thread.sleep(1000);

wait()是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait()方法后会将调用者的线程挂起,直到其他线程调用同一个对象的notify()方法才会重新激活调用者。

try{
obj.wait();//suspend thread until obj.notify() is called
}
catch(InterrputedException e) {
}

2、同步锁释放不同

sleep()不释放同步锁,wait()释放同步锁.

Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

可以简单认为和锁相关的方法都定义在Object类中,因此调用Thread.sleep是不会影响锁的相关行为。

举个例子:

sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.好比如说,我要做的事情是 “点火->烧水->煮面”,而当我点完火之后我不立即烧水,我要休息一段时间再烧.对于运行的主动权是由我的流程来控制.

而wait(),首先,这是由某个确定的对象来调用的,将这个对象理解成一个传话的人,当这个人在某个线程里面说”暂停!”,也是 thisOBJ.wait(),这里的暂停是阻塞,还是”点火->烧水->煮饭”,thisOBJ就好比一个监督我的人站在我旁边,本来该线 程应该执行1后执行2,再执行3,而在2处被那个对象喊暂停,那么我就会一直等在这里而不执行3,但正个流程并没有结束,我一直想去煮饭,但还没被允许, 直到那个对象在某个地方说”通知暂停的线程启动!”,也就是thisOBJ.notify()的时候,那么我就可以煮饭了,这个被暂停的线程就会从暂停处 继续执行.

其实两者都可以让线程暂停一段时间,但是本质的区别是一个线程的运行状态控制,一个是线程之间的通讯的问题

3、使用方式不同

sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

使用wait()方法,必须捕获异常。

abstract和interface的区别

接口和抽象类有什么区别 http://www.cnblogs.com/yongjiapei/p/5494894.html

抽象类和接口的区别与联系 http://blog.csdn.net/u010456903/article/details/45868949

Java抽象类与接口的区别 http://www.importnew.com/12399.html

参考:http://www.cnblogs.com/felixzh/p/5938544.html

抽象方法

在了解抽象类之前,先来了解一下抽象方法。抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:

abstract void fun();

抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类 必须在类前用abstract关键字修饰。

因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。

[public] abstract class ClassName {
    abstract void fun();
}

从这里可以看出,抽象类就是为了继承而存在的。试想你定义了一个抽象类,却不继承它,那就白建了,因为不能用它来做任何事情。

对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。注意,抽象类和普通类的主要有三点区别:
1. 抽象方法必须为public或者protected。
2. 抽象类不能创建对象
3. 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。

在其他方面,抽象类和普通的类并没有区别。

接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。

抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。

比如,男人,女人,这两个类,他们的抽象类是人。说明,他们都是人。

人可以吃东西,狗也可以吃东西,我们可以把“吃东西”定义为一个接口,然后让各类去实现它。

所以,在JAVA中,一个类只能继承一个类(抽象类)(正如人不可能是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)

抽象类的特点
1. 抽象类有自己的成员变量,有一个或多个抽象方法,当然也可以有非抽象方法。(抽象类的另一个定义可以是:含有抽象方法的类叫做抽象类。)
2. 抽象类不能被实例化,但是他有自己的构造器,构造器的作用是在子类在实例化的时候会默认调用父类(抽象类)的构造器
3. 抽象类的子类必须为抽象方法定义,否则,该子类也是抽象类。

接口的特点
接口是一个极度抽象的类,它只有方法的声明,没有任何方法的实现。(也就是说它只知道应该做什么,至于具体怎么去做他不管)
1. 接口中的成员变量默认都是static和final类型的。成员变量在定义的时候必须直接初始化他。
2. 接口中的方法默认都是abstract类型的。
3. 接口中的成员变量和成员方法的访问权限都是public类型
4. 接口可继承接口,并可多继承接口,但类只能单根继承。
5. 一个类可以实现多个接口,多个接口名之间用逗号间隔。

抽象类和接口的区别

参数抽象类接口
默认的方法实现可以有默认的方法实现完全抽象的,不存在方法的实现
实现子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现子类使用implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器抽象类可以有构造器接口不能有构造器
与正常JAVA类的区别除了不能实例化抽象类以外,没有区别接口是完全不同的类型
访问修饰符抽象方法可以有public、protacted和default这些修饰符接口方法默认修饰符是public,不可使用其他修饰符
main方法抽象方法可以有main方法并且可以运行接口没有main方法,不能运行
多继承抽象类可以继承一个类和实现多个接口接口只可以继承一个或者多个接口
添加新方法如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

什么时候使用抽象类和接口

  • 如果想拥有一些方法并且让它们中的一些有默认实现,那就使用抽象类
  • 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

ClassLoader

参考 http://blog.csdn.net/xyang81/article/details/7292380

基本概念

与C/C++编写的程序不同,JAVA程序并不是一个可执行文件,而是由许多独立的类文件组成,每一个文件对应一个JAVA类。此外,这些类文件并非全部装入内存,而是根据程序需要逐渐载入。ClassLoader是JVM实现的一部分,ClassLoader包括bootstrap classloader(启动类加载器),ExtClassLoader(扩展类加载器)和AppClassLoader(系统类加载器)。

bootstrap classloader

在JVM运行的时候加载JAVA核心的API,以满足JAVA程序最基本的需求,其中就包括后两种ClassLoader。

ExtClassLoader

加载JAVA的扩展API,也就是/lib/ext中的类

AppClassLoader

用来加载在用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由该ClassLoader进行加载。

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

ClassLoader加载流程

当运行一个程序的时候,JVM启动,运行bootstrap classloader,加载JAVA核心API,同时加载另两个ClassLoader。然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class.这是最基本的加载流程。

ClassLoader加载原理

双亲委托模式

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

。。。
try{
if(parent != null){
c = parent.loadClass(name,false);
}eles {
c = findBootstrapClass(name);
}
}catch (ClassNotFoundException e){
c = findClass(name);
}
。。。

从这段代码就可以看出双拼委托模式了,为啥叫双亲呢?因为有两个爸爸,一个是它继承的爸爸,另一个就是bootstap爸爸。因为bootsrap不是继承自ClassLoader的,而是写在底层的。

为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值