java面试题集锦(二)

2016.7.25更新...........................................................................

(12):java中引用的类型

        java中将引用分为强引用、软引用、弱引用、虚引用:

        强引用:只要强引用存在,垃圾回收器就不会对它进行回收;

        软引用:对于软引用对象,在系统发生内存溢出之前,将会把这类引用对象列为回收范围对象进行第二次垃圾回收,如果这次回首之后还没足够的内存,才会抛出内存溢出;

        弱引用:被引用的关联对象只能生存到下一次垃圾回收之前,在下次垃圾回收的时候,不论当前内存释放足够,都会回收掉被弱引用关联的对象;

        虚引用:一个对象有没有虚引用完全不会影响该对象的生存时间,为一个对象设置虚引用的唯一目的就是在该对象被回收之前能够收到一个系统通知而已;

(13):JVM垃圾回收原理

        谈到垃圾回收,需要搞清楚两个问题就可以了,什么样的对象需要回收,怎样进行回收;首先来说说什么样的对象需要回收,Java的内存区域被分为各个部分,对于程序计数器、本地方法栈、虚拟机栈这三个区域来说,因为是属于线程所独享的,随线程的产生而产生,随线程的灭亡而灭亡,因此这部分的内存空间是不需要垃圾回收器关心的,但是对于堆区和方法区来说,情况就不一样了,因为一个接口的多个实现类占用的内存可能是不同的,一个方法的不同分支占用的内存也可能是不同的,他们占用内存的大小是需要在运行的时候才能确定的,这才是垃圾回收器所关心的地方,那么确定哪些对象需要回收之前采用的是引用计数法,但是这种方法有个很大缺点,就是很难解决对象循环引用的问题,HotSpot虚拟机也没采用这种方法,后来出现了可达性分析算法,这种方法通过将一些称为"GC Root"的对象作为起始点,当一个对象到"GC Root"没有任何引用链相连的时候称为对象不可达,那么这些对象就是需要垃圾回收器回收的对象了,重点就是哪些对象可以作为"GC Root"了,java语言规定可以作为"GC Root"的由虚拟机栈中所引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象;解决了要回收哪些对象之后,我们就该聊聊怎样进行回收了,因为方法区中存储的是类信息、静态变量、常量,HotSpot虚拟机将他称为了永久代,也就说明在该区域发生垃圾回收的操作不是很频繁;早期的垃圾回收算法是标记-清除算法,该算法的缺点在于标记清除过程效率不是很高,同时会导致碎片太多,无法存储较大对象而不得不进行频繁的垃圾回收操作,而大家都知道垃圾回收过程会出现"Stop the World"现象,导致当前任务停止等待垃圾回收完成,影响系统运行效率,随后出现的复制算法虽然解决了标记清理效率低以及碎片化严重的问题,但是却造成了内存空间没有完全利用起来的缺点,因此HotSpot虚拟机采用了两者的结合体来实现对堆内存的垃圾回收操作,具体来说他将堆内存分为新生代和老年代,采用分代收集算法来进行垃圾回收操作,新生代的GC操作称为Minor GC,老年代的GC称为Major GC,两者合起来称为Full GC,新生代他的回收操作比较频繁因而采用复制算法进行垃圾回收,老年代因为回收操作不是很频繁,因而采用标记-整理算法进行垃圾回收,新生代又按8:1:1的比例分成了Eden区、From Survivor区、To Survivor区,为了防止多个线程同时分配内存带来的问题,又会将Eden区分为一个或者多个本地线程缓存(TLAB),每个线程占用其中之一,注意一点就是新生代的Survivor区域至少有一个处于空闲状态,刚开始整个新生代都是处于空闲状态的,当我们new对象的时候会将该对象放到Eden区域,这时候两个Survivor区域都是空闲的,当继续new对象出现Eden区域出现空间不足的时候,会触发Minor GC操作,这时候会通过可达性分析算法查看Eden区哪些对象还是存活的,将这些存活的对象存放到From Survivor中,如果From Survivor中存放不下这些存活的对象的话,会直接将多出来的对象存放到分配担保区域,也就是老年代中,同时会将Eden区域清空,这时候From Survivor区域是由对象的,To Survivor区域是空闲的,接着我们继续new对象的时候,同样会将对象放在Eden区域,当Eden区域再次满的时候,同样会触发一个Minor GC操作,这次Minor GC操作将会把Eden区域和From Survivor区域通过可达性分析算法计算出存活的对象移到To Survivor中,如果这时候To Survivor不足以存放下这么多存活对象的话,同样也会直接将多余的对象直接存放到分配担保区域老年代中,接着将Eden区和From Survivor区域清空,这样From Survivor和To Survivor区域交替空闲,并且老年代做分配担保来完成新生代的垃圾回收操作;至于老年代的垃圾回收操作就相对来说简单多了,采用标记整理算法,将存活的对象移到一边,然后直接清理掉端边界意外的内存就好啦;

2016.7.26更新...........................................................................

(14):字节流和字符流的区别

        (1):字节流是以字节为单位来进行IO操作的,java中有字节流有关的最大的两个父类是InputStream和OutputStream两个抽象类;字符流是以字符为单位进行IO操作的,java中与字符流有关的最大的两个父类是Writer和Reader两个抽象类;

        (2):对字节流的操作是不会用到内存缓冲区的,是直接对文件本身进行操作;而对字符流的操作需要用到内存缓冲区,原因在于对字符流的操作本质上是对字节流的操作,所以用到内存作为缓存将数据先暂存到内存中,然后直接从内存中获取,可以避免多次IO操作,提高效率;

        (3):磁盘上的文件都是以字节的形式存在的,而字符值是在内存中才会形成的,也就是记住一点,字符的底层是字节就可以了; 

(15):sleep()和wait()的区别

        (1):两者位于不同的类中,sleep位于Thread类中,wait位于Object类中;

        (2):调用sleep方法之后,线程是不会释放对象锁的,到达指定时间之后会自动苏醒;调用wait方法之后线程会释放对象锁;

        (3):sleep之后不让出系统资源,但是wait之后是会让出系统资源的,其他线程可以占用释放的资源,如果wait之后想要重新获得系统资源,只能通过notify唤醒线程,让他有机会重新获得CPU时间片;

(16):throw和throws的区别

        (1):throw代表动作,表示抛出异常的动作;throws表示一种状态,表示方法可能会抛出异常;

        (2):throw用在方法实现中,throws用在方法声明中;

        (3):throw只能抛出一种异常,而throws可以抛出多个异常;

(17):java多态实现原理

        实现原理靠的是父类或者接口中定义的引用变量可以指向子类或者具体的实现类的实例对象,而程序调用的方法是在运行的时候动态绑定的,即引用变量所指向的具体事例对象的方法也就是当前内存中正在运行的那个对象的方法,而不是引用变量的类型中定义的方法,通俗点讲JVM是通过对象的自动向上转型来实现多态的;

(18):java面向对象的三大特性

        封装、继承、多态

(19):Collection与Collections的区别

        Collection<E>是Java集合框架中的基本接口;Collections是集合框架提供的一个工具类,其中包括大量的静态操作方法;

(20):Java中进行线程同步的方法有哪些呢?

        (1):使用volatile关键字,保证对同一volatile变量的修改操作happens before对他的读操作;

        (2):使用synchronized,可以对一个代码块或者方法上锁,被锁住的地方称为临界区,需要获得对象的monitor才能进入临界区,以后再进入临界区的线程会因为无法获得monitor而被阻塞,直到释放monitor之后其他线程才能进去,注意一点的就是由于等待另一个线程释放monitor而被阻塞的线程是无法被中断的;

        (3):使用ReentrantLock可重入锁,尝试获取锁的线程可以被中断并且可以设置超时参数;

(21):TreeMap, LinkedHashMap, HashMap的区别

        HashMap与LinkedHashMap的区别主要体现在LinkedHashMap保存了记录的插入顺序,在使用迭代器遍历LinkedHashMap的时候,先得到的记录肯定是先插入的,但是在遍历的时候会比HashMap慢点,除了一种特殊情况,就是HashMap容量很大,但是实际数据较少,遍历的时候速度可能会快于HashMap,因为LinkedHashMap的遍历速度只与实际数据的个数有关系,与Map的容量是没有关系的,而HashMap的遍历速度是与他的容量有关系的;

        HashMap和TreeMap的区别主要体现在:TreeMap能够把他保存的记录根据键值排序,默认是按照键值的升序排序,也可以指定排序的比较器,当用迭代器遍历TreeMap的时候得到的记录是排过序的;

(22):Java中堆和栈的区别

        (1):Java堆是线程共享的,他的空间回收需要垃圾回收器的参与,Java栈是线程独享的,他会随线程的产生而产生,随线程的灭亡而灭亡,这部分内存空间的回收不需要垃圾回收器的参与;

        (2):Java堆中存放的是对象实例,Java栈中存放的是局部变量表、操作数栈、动态链接、方法出口信息;

(23):ArrayList与Vector的区别

        ArrayList与Vector都是采用动态数组的方式实现的,两者的最大区别在于,ArrayList是非线程安全的,要想在多线程下使用它,需要使用他的升级版本CopyOnWriteArrayList,而Vector是线程安全的,它内部的众多方法前面都有synchronized修饰,用于保证同一时刻只有一个线程可以访问他;

(24):简单介绍下Java反射

        我们平常要想获得对象的话,往往是通过new的方式实现的,这个对象的获得实际上是JVM虚拟机帮我们解析Class字节码文件在运行的时候构建出来的,而反射是指我们在运行时根据一个类的Class对象来获得他的定义信息,比如类的方法、属性、父类等信息的机制,我们知道javac会将.java文件编译成.class文件,这个.class中就包含类的一些定义信息了,比如父类、接口、构造器等等,.class文件在运行的时候会被ClassLoader类加载器加载到java虚拟机中,当一个.class文件被加载后,JVM会为之生成一个Class对象,我们在程序中的new操作实际上是根据相应的Class对象构造出来的,确切的讲,这个Class对象实际上是java.lang.Class<T>泛型类的实例,因为Class类没有提供公有类型的构造器,所以我们一般是通过Class.forName的方式获得他的,有了Class对象之后,我们便可以利用该Class对象获得构造器,具体可以通过getConstructors获得所有public类型的构造器,通过getDeclaredConstructor获得所有类型的构造器,包括public/private类型的,然后调用这些构造器的newInstance方法便可以创建一个该类型的实例出来了,通过同样的方法,我们也可以通过Class对象获得该类的方法、属性、父类、接口等信息,此外我们可以通过将方法调用setAccessible(true)使得某个private方法能够通过反射访问到,正是因为setAccessible的存在,导致了反射操作会破坏单例模式;

(25):Java深拷贝与浅拷贝

        浅拷贝:会创建一个新的对象,这个对象有着和原始对象属性值的一份精确拷贝,如果属性值是基本类型的话,拷贝的就是基本类型的值;如果属性是引用类型的话,拷贝的是内存地址,因此如果一个对象改变了这个地址里面的值的话也会影响到这个拷贝的对象;

        深拷贝:不仅仅会对对象属性中的基本数据类型进行复制操作,也会对引用类型进行复制操作,这里的复制将不再仅仅是拷贝内存地址了,而将创建的是完完全全的新对象,只不过这个对象里面的值和之前对象里面的值一样而已了;

        我们平常见到的克隆实际上是浅拷贝的,如果想要实现真正意义上的拷贝,我们需要将对象中的所有引用类型全部实现Cloneable接口里面的clone方法,这点对于类结构比较复杂的操作来说未免太麻烦了,一个有效的解决方案就是使用序列化的方式实现,序列化创建出来的对象将是和原先对象属性值完全相同的对象,注意一点的是,序列化也会破坏单例模式;

(26):Java动态代理实现原理

        代理模式主要用于一些权限方面的限制功能,比如服务端并不想让某些用户直接访问自己,那么就可以抽象出来一个代理用户,其他用户想要访问自己的时候首先需要通过这个代理用户的检查,看他有没有资格访问服务端,有的话才会放行;代理分为静态代理和动态代理,静态代理是我们自己实现的而动态代理是在程序的运行期间动态生成的;在代理模式中最关键的就是需要代理和服务真正提供者有相同的功能,代理中有真正服务的对象,实际上代理中的操作还是真正的服务对象在操作,实现这点要求有两种方式:一种是代理和真正服务提供者实现相同的接口,这也就是JDK为我们实现动态代理所采用的方式,一种是代理类继承自真正服务提供者类,并且可以重写里面的方法,这也就是CGLIB实现动态代理所采用的方式;我们只看JDK实现动态代理的原理,在我们平时的使用中,只需要通过Proxy.newProxyInstance方法创建一个代理类实例,接着使用这个代理类实例就可以进行真正的操作了,其实在创建代理类实例的时候我们会传入一个实现了InvocationHander接口的对象,该对象实现了接口中的invoke方法,并且该对象中是有一个真正服务提供者的引用的,我们在调用代理类的某些方法的时候实际上调用的是实现了InvocationHandler接口的类实例的invoke方法,该invoke方法会通过接口中的真正服务提供者的引用来调用真正服务者的方法,简单点的话,我们可以这样理解,动态代理实际上就是在代理和真正服务者之间又增添了一个代理一样,这个代理就是实现了InvocationHandler接口的对象,我们可以将其称为中介类,动态代理实际上就是两组静态代理的组合,我们调用代理类方法的时候,实际上该代理类会去调用中介类的invoke方法,这个方法会通过反射的方式调用中介类中的真正服务类的方法,这就是Java动态代理的实质;

(27):简单介绍下单例模式及其注意点

        单例模式从类加载的时候是否会创建实例的角度可以分为恶汉式和懒汉式,恶汉式是不会加载的,只有在用的时候通过显式的方法调用才会创建实例,懒汉式是会加载的,在类被加载的时候就会被创建出来的;

        我们常见的单例模式实现方式是双重校验锁方式,具体实现代码见下:

class SingletonMode 
{
	private static SingletonMode instance; 
	private SingletonMode(){}
	
	public SingletonMode getInstance()
	{
		if(instance == null)
		{
			//这里可能对于不同的线程存在不同时间长度的耗时操作
			synchronized (SingletonMode.class) {
				if(instance == null)
					instance = new SingletonMode();
			}
		}
		return instance;
	}
}

        这里我们有几点需要注意一下:

        (1):单例模式中的构造函数是private类型的,也正是因为是private类型,所以才能保证外界无法通过new关键字创建实例;

        (2):在上面的单例模式中,我们采用的是恶汉式,也就是说只有你显式的调用getInstance方法才有可能创建SingletonMode实例;

        (3):上述方法中,我们对instance是否为null进行了两次判断,很多人都觉得进行一次判断就可以了,其实不是这样的,假如现在有两个线程,线程1和线程2,线程1执行getInstance方法的时候,会发现instance的值是null,那么他会进入if语句块中,接着开始执行一些耗时操作了,但是此时呢,线程2也同样调用了getInstance方法,因为线程1一直在执行耗时操作,所以并没有执行synchronized语句块,那么此时instance的值是null的,同样也会进入if语句块中,但是线程2不需要执行耗时操作,那么他就会获得SingletonMode的对象锁,如果不存在第12行通过if判断instance是否为null的话,则直接创建了instance对象出来,执行结束之后释放掉了SingletonMode对象锁,接着线程1执行完耗时代码之后,同样也会获得SingletonMode对象锁,那么不存在第12行代码的话,他同样也会创建一个instance值出来,这显然是违背了单例模式的,所以我们通常采用双重校验锁的方式,加入了第12行代码;

        (4):在一些情况下可能会破坏单例模式,比如序列化反序列化的过程中,具体解决方法是在单例类中实现readResolve方法,该方法是private类型的,在该方法中返回instance实例就可以了;反射也会破坏单例模式,因为虽然我们把单例类的构造方法设置成了private类型,但是我们可以通过setAccessible方法将该构造函数设置成外界可以访问,这样外界可以随便访问了,自然也就不是单例了,解决措施是在第二次创建的时候抛出已经存在实例的异常,java中就是这么做的;

        (5):在<Effective java>中提到了使用枚举来实现单例模式,这种方法是可以防止序列化反序列化以及反射破坏单例的,也是这本书的作者多提倡我们使用的单例模式方法;

(28):关于String的intern关键字

        JDK1.6和JDK1.7中intern关键字是有区别的,原因在于JDK1.7中将常量池移出了堆内存中;

        在JDK1.6中,调用String的intern方法的时候,会去常量池中查看是否有与当前String值相同的String存在,有的话则直接返回常量池中的这个引用就可以了,如果不存在的话,则会在常量池中创建一个和这个String相同的String出来;

        在JDK1.7中,在调用String的intern方法的时候,首先会去常量池中查看有没有和当前String值相同的String存在,存在的话直接返回常量池中这个String值的引用就可以了,如果不存在的话,则不会像JDK1.6那样在常量池也创建一个和当前String相同的String出来,而是会将原始堆中当前String的引用存储在常量池中而已;

(29):synchronized与volatile的区别:

        并发编程有三大特性,原子性、可见性和有序性,当然在java中的并发编程也应该是满足这三个特性的,针对原子性来说,java语言本身对于基本数据类型的变量读取和赋值操作是原子性的,但有一点需要注意,在32位平台下,对64位数据的读取和赋值是通过两个操作来完成的,不能保证其原子性比如double类型,对于更大范围的原子性,java是通过synchronized或者Lock来实现的,原因在于synchronized与Lock能够保证同一时刻只有一个线程可以访问代码块,但是volatile是不能够确保原子性的;对于可见性,java中是使用volatile关键字来达到目的的,他会保证变量被修改的值立即被更新到内存中,当有其他线程需要读取的时候,直接从内存中读取就好了,取到的值将是新值,但是对于普通变量是不能够保证可见性的因为普通变量被修改后什么时候写入内存的时间是不固定的,其他线程去读取该变量值的时候可能读到的是旧值,具体的原因在于java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的本地内存(这里的本地内存可以类比为CPU与主存之间的高速缓存),线程的本地内存中存放着该线程所使用到的主内存变量的拷贝,线程对变量的所有操作都必须在本地内存中进行,而不能直接读取主内存中的变量,不同线程之间无法直接访问对方本地内存中的变量,线程间变量值的传递均需要主内存的参与,因此在多线程情况下如果两个线程共享某一变量,一个线程可能还未在另一个线程写入新值就读出里面的内容,导致了内容不一致,而使用volatile修饰的变量会强制将修改的值直接写入主内存中而不是线程自己的本地内存;对于有序性的话,synchronized和Lock是可以保证的,因为每一时刻都只能有一个线程访问同步代码,自然能够保证有序性,而volatile能够保证一定的"有序性";

        首先应该知道的一点,正是因为编译器为了加快程序的运行速度,对变量的写操作首先是在自己的本地内存中进行的,而这部分内容自己自己知道,其他线程根本没法感知,因而要写入主内存,正是因为这一过程被分成两个步骤导致了线程中共享变量的不同步,而volatile就是要求线程对该修饰符修饰的变量的操作直接写入主内存中,将两步操作直接变为一步完成;

        (1):volatile本质是告诉JVM当前变量在寄存器中存储的值是不确定的,需要直接从主存中读取;synchronized则是锁住当前变量,只有当期线程可以访问该变量,其他访问该变量的线程只能阻塞;

        (2):volatile只适用于变量级别,而synchronized适用于方法、变量中;

        (3):volatile仅能保证对变量修改保证可见性,但是不能保证原子性;synchronized对变量的修改则可以保证可见性和原子性;

        (4):volatile不会造成线程阻塞,而synchronized可能会造成线程阻塞;

        (5):volatile标记的变量不会被编译器优化,而synchronized标记的变量可能被编译器优化;

(30):synchronized与Lock以及Atomic的区别

        synchronized是在JVM层面上实现的,在代码执行时出现异常后,JVM会自动释放锁定,但是Lock不可以,Lock是通过代码实现的,要保证锁一定会被释放,就必须将unLock()放到finally{ }语句块中;

        在资源竞争不是很激烈的情况下,synchronized的性能要优于ReentrentLock,但是当资源竞争激烈的情况下,synchronized的性能会下降,而ReentrantLock能够维持常态;

        在资源竞争不是很激烈的情况下,synchronized的性能要优于Atomic,但在资源竞争激烈的时候,Atomic能够保证常态,并且性能也好于ReentrantLock,有一个缺点就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多余一个同步无效,因为他不能在多个Atomic之间同步;

(31):synchronized和ReentrantLock的区别:

        Reentrant除了synchronized有的功能之外,还多了几个高级功能:

       synchronized会在进入同步块的前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令时会尝试获取对象的锁,monitorenter指令执行时会让对象的锁计数加1,monitorexit指令执行时会让对象的锁计数减1;

        (1):中断锁:在持有锁的线程长时间不释放锁的时候,等待锁的线程可以选择放弃等待,tryLock(long timeout,TimeUnit unit),当然对于中断锁可以选择忽略或者响应;

        (2):公平锁:按照申请锁的顺序来依次获得锁,而synchronized是非公平锁,公平锁可以通过ReentrantLock的构造函数实现,new ReentrantLock(boolean fair);

        (3):绑定多个Condition:通过多次new Condition可以获得多个Condition,可以简单的实现比较复杂的线程同步功能;

(32):Java中的final关键字

        final表示"不可修改"的意思,可以用来修饰非抽象类、非抽象方法和变量;

        final修饰非抽象类:表示该类不可以被继承,没有子类,final类中的方法默认是final类型的;

        final修饰非抽象方法:表示该方法不可以被子类重写,但可以被继承;

        final修饰成员变量:表示常量,只能被赋值一次,赋值后不再改变;

        final不能修饰构造方法,父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的;

(33):Java中的static关键字

        static表示"全局"或者"静态"的意思,可以用来修饰成员变量和成员方法,也可以形成static代码块,但是java中没有全局变量的概念,被static修饰的成员变量或者方法是独立于该类的任何对象的,也就是说他不依赖于类特定的实例,被所有的实例共享,只要这个类被加载,虚拟机就可以根据类名在运行时数据区中的方法区中找到他们了,被static修饰的成员变量或者成员方法可以通过类名访问,注意一点的就是在static修饰的成员方法只能访问所属类的静态成员变量与成员方法,不能访问所属类的实例变量和实例方法;static默认情况下是不能修饰类的,但他可以修饰内部类,默认情况下不用static修饰的内部类会隐式包含有外部类的引用,但是静态内部类中不包含外部类的引用;

(34):start()方法和run()方法的区别

        用过java线程的都应该知道这两个方法了,一般我们是通过start方法来开启线程的,但是最终执行的还是线程中的run方法了,因为run方法一般都是public,所以外界也可以直接调用run方法了,两者有什么区别呢?

        确切的讲,调用start方法是会出现多线程的调用start方法之后线程处于可运行的状态,只要CPU给他时间片了,他便可以执行run方法了,但是调用run方法仍然还是只有一个主线程,和普通的方法调用是没什么区别的,只不过名字是run而已了;

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值