说一说你对双亲委派模型的理解
参考回答
【得分点】
三层类加载器、双亲委派流程
【参考答案】
标准回答
双亲委派模型依赖于三层类加载器:
-
启动类加载器:这个类加载器负责加载存放在
<JAVA_HOME>\lib
目录(lib:库),或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。 -
扩展类加载器:这个类加载器负责加载
<JAVA_HOME>\lib\ext
目录中(ext:拓展),或者被java.ext.dirs系统变量所指定的路径中所有的类库。 -
应用程序类加载器:这个类加载器负责加载用户类路径(classpath)上所有的类库。
双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层都是如此。因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
使用双亲委派模型的好处是,Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
加分回答
双亲委派模型主要有3种“被破坏”的情况:
- 双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前
双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了兼容这些已有代码,只能在之后的ClassLoader中添加一个protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
- 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
一个典型的例子便是JNDI服务,JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,JNDI服务就可以使用它去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。
- 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的
这里所说的动态性指的是一些非常热门的名词:代码热替换、模块热部署等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用。
在这个领域,IBM公司主导的OSGi 提案是业界事实上的Java模块化标准。OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
说一说你对Spring AOP的理解
参考回答
【得分点】
概念、作用、实现
【参考答案】
标准回答
AOP是一种编程思想,是通过预编译方式和运行期动态代理的方式,在不修改源代码的情况下实现给程序动态统一添加功能的技术。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
AOP技术利用一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为切面。所谓的切面,简单来说就是与业务无关,却为业务模块所共同调用的逻辑,将其封装起来便于减少系统的重复代码,降低模块的耦合度,有利用未来的可操作性和可维护性。
利用AOP可以对业务逻辑各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。
AOP可以有多种实现方式,而Spring AOP支持如下两种实现方式。
· JDK动态代理:这是Java提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。
· CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。
JDK动态代理需要接口,CGLib动态代理不需要接口
加分回答
AOP不能增强的类:
-
Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。
-
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。
AOP面向切面编程,将代码中重复的部分抽取出来,使用动态代理技术,在不修改源码的基础上对方法进行增强。如果目标对象实现了接口,默认采用JDK动态代理,也可以强制使用CGLib,如果目标对象没有实现接口,采用CGLib的方式。常用的场景包括权限认证、自动缓存、错误处理、日志、调试和事务等
Java中有哪些集合类
参考回答
【得分点】
Set、Quque、List、Map
【参考答案】
标准回答
Java中的集合类分为4大类,分别由4个接口来代表,它们是Set、List、Queue、Map。其中,Set、List、Queue、都继承自Collection接口。
· Set代表无序的、元素不可重复的集合。
· List代表有序的、元素可以重复的集合。
· Queue代表先进先出(FIFO)的队列。
· Map代表具有映射关系(key-value)的集合。
Java提供了众多集合的实现类,它们都是这些接口的直接或间接的实现类,其中比较常用的有:HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。
加分回答
上面所说的集合类的接口或实现,都位于java.util包下,这些实现大多数都是非线程安全的。虽然非线程安全,但是这些类的性能较好。如果需要使用线程安全的集合类,则可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。
java.util包下的集合类中,也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。
从Java 5开始,Java在java.util.concurrent(JUC)包下提供了一批支持高效并发的集合类,这些类既实现了线程安全,又保证了可靠的性能。这些集合类按照名字可以分为两大类,它们分别以Concurrent开头或CopyOnWrite开头。
以Concurrent开头的集合类,在多线程读取数据时则没有加锁,以提高读取的性能。在多线程写入数据时加锁,来保证所有写入操作是线程安全的。这些类都采取了比较复杂的算法,在加锁时不会锁住整个集合,从而保证并发写入时具有更好的性能。
以CopyOnWrite开头的集合类,采用写时复制技术来实现并发写操作,它复制的是底层的数组。当线程读取集合中的数据时,它直接读取集合本身的数据,不会加锁。当线程向集合写入数据时,该集合会在底层复制一份新的数组,然后对这个新数组执行写入操作。由于所有写入操作都是对数组的副本的操作,所以它是线程安全的。
介绍一下分代回收机制
参考回答
【得分点】
新生代收集、老年代收集、混合收集、整堆收集
【参考答案】
标准回答
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则。而分代收集理论,建立在如下三个分代假说之上,即弱分代假说、强分代假说、跨代引用假说。依据分代假说理论,垃圾回收可以分为如下几类:
-
新生代收集:目标为新生代的垃圾收集。
-
老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。
-
混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
-
整堆收集:目标为整个堆和方法区的垃圾收集。
JVM对自己的内存进行了划分5个区域,分别是
方法栈:所有的方法运行的时期,进行的内存;
堆(heap):存储的是容器和对象。
寄存器:内存和CPU之间;
本地方法栈:JVM调用了系统中的功能;
方法和数据共享:运行时期class文件,进行的地方;
垃圾分代回收机制针对的是堆内存。
Java中的每种数据类型大小都是确定的,所以所有的内存是由Java自己进行分配,这也就意味着内存的管理和回收也是由JVM自己进行。在Java中一旦产生内存问题导致程序员无法处理。理论上在正常情况下Java中的堆内存是足够使用的,当堆内存使用的负荷量超过一定限度(一般情况下是70%)的时候,会启动垃圾回收器(Garbage Collector — GC)进行堆内存的回收释放。
分代是指堆内存又分为新生代(Young Generation)与老生代(Old Generation),新生代又分为伊甸园区(eden)与幸存区(survivor),幸存区由from space与to space两块相等的内存区域构成。
eden:from:to = 8:1:1
说一说你对volatile关键字的理解
标准回答
volatile用于保证内存的可见性,可以将其看做是轻量级的锁,它具有如下的内存语义:
写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存中。
读内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。
其中,JMM是指Java内存模型,而本地内存只是JMM的一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。在本文中,大家可以将其简单理解为缓存。
volatile只能保证单个变量读写的原子性,而锁则可以保证对整个临界区的代码执行具有原子性。所以,在功能上锁比volatile更强大,在可伸缩性和性能上volatile更优优势。
加分回答
volatile的底层是采用内存屏障来实现的,就是在编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障就是一段与平台相关的代码,Java中的内存屏障代码都在Unsafe类中定义,共包含三个方法:LoadFence()、storeFence()、fullFence()。