2020Java面试试题及答案(基础部分)

与世无争的帅哥 提交于 2020-11-01 14:44:00

世人慌慌张张,不过图碎银几两。偏偏这碎银几两,能解世间万种慌张。最近实在揭不开锅了,有了换工作的想法,网上整理了部分Java基础面试题,好东西要拿出来分享。好了不多说了进入正题。

如果你不想再体验一次自学时找不到资料,没人解答问题,坚持几天便放弃的感受的话,可以加我们的扫描二维码,各种面试资料简历模板免费送!

 

1、面向对象的三个特征

封装(即包装或隐藏):

封装从字面上来理解就是包装的意思,专业点就是信息隐藏。它指的是将对象的数据、状态信息和对数据的操作方法及细节隐藏在对象内部,
不允许外部程序直接访问对象的内部信息或直接对对象进行操作,而是向外提供一些安全可靠的方法供外部程序对对象进行安全的访问和操作。

好处:
隐藏信息和实现细节
可以对成员进行更精确的控制,限制对对象的不合理访问
便于修改,提高代码的可维护性
良好的封装能够减少耦合

继承:

使用现有的类的所有功能,并无须重新编写原来的这些类的基础上对这些功能进行扩展(可以增加新的数据或新的功能)。

特点:
子类拥有父类非private的属性和方法
子类可以拥有自己属性和方法,即子类可以对父类进行扩展
子类可以重写父类已实现的方法

多态:

所谓多态就是一个实例的相同方法在不同的情形下有不同的表现形式,即程序中定义的引用变量所指向的具体类型和通过引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

实现多态主要有以下三种方式:
实现接口
继承父类重写方法
同一类中进行方法重载

2、接口和抽象类

抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。java类可以继承抽象类,也可以实现接口,继承是一个"是不是"的关系,而接口实现则是"有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系。

接口:

可将接口理解为统一定义的“协议”或者是“行为规范”。例如两个开发者,开发时间完全不一致,那么需要两个人的配合开发,则需要一个人将接口写好,定义好其中所有的变量命名规范、函数定义规范。具体实现类的开发人员则只需要按照接口实现相应功能即可。

抽象类:

如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。

抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。
对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,
那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

3、父类的静态方法能否被子类重写

java的静态方法不能被重写

静态成员(方法或属性)是类的成员存放在栈中,类可以直接调用(是属于类的静态成员,当然对象也可以调用,只是说你可以使用而已)。实例成员是对象的成员,存放在堆中,只能被对象调用。 重写的目的在于根据创造对象的所属类型不同而表现出多态。因为静态方法无需创建对象即可使用。没有对象,重写所需要的“对象所属类型” 这一要素不存在,因此无法被重写。

重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏。静态方法不能重写但能重定义。

4、什么是不可变对象

所谓的不可变类是指这个类属性是被final修饰的,并不是指该类是被final修饰的。类的实例一但被创建,实例的属性值将不能被改变。Java中的8个包装类和String类都是不可变类。

不可变类的设计方法:
类添加final修饰符,保证类不被继承。
保证所有成员变量必须私有,并且加上final修饰
不提供改变成员变量的方法,包括setter
通过构造器初始化所有成员,进行深拷贝(deep copy)
在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

5、静态变量与实例变量的区别

被static关键字修饰的变量,叫类变量或者静态变量。没有static修饰,为实例变量。

类的静态变量在内存中只有一个,java虚拟机在加载类的过程中为静态变量分配内存,静态变量位于方法区,被类的所有实例共享。静态变量可以直接通过类名进行访问,其生命周期取决于类的生命周期。类变量是所有对象共有,其中一个对象将它的值改变了,其他对象得到的就是改变后的结果。

而实例变量取决于类的实例。每创建一个实例,java虚拟机就会为实例变量分配一次内存,实例变量位于堆区中,其生命周期取决于实例的生周期。而实例变量则属对象私有,某一个对象将其值改变,不影响其他对象。

6、Java中创建对象的5种方式

使用new关键字
使用Class类的newInstance方法
使用Constructor类的newInstance方法
使用clone方法
使用反序列化

7、switch中能否使用string做参数

在idk 1.7之前,switch只能支持byte, short, char, int或者其对应的封装类以及Enum类型。从idk 1.7之后switch开始支持String。

switch中的参数不支持其他类型,例如long类型

8、String对象的intern()方法

intern()方法会首先从常量池中查找是否存在该常量值,如果常量池中不存在则现在常量池中创建,如果已经存在则直接返回。

9、java当中的四种引用

强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在GC上:

强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。

弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。

虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
为什么要有不同的引用类型?

在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。

10、==和eqauls()的区别

==是运算符,用于比较两个值是否相等,而equals是Object类的方法,用于比较两个对象是否相等。默认Object类的equals方法是比较两个对象的内存地址,此时和==的结果一样,如果需要比较对象内容,需要重写equal方法。

11、equals()和hashcode()的区别

hashCode()是Object类的一个方法,返回一个哈希值。
如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的哈希值。
如果两个对象根据eqaul()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的)。

12、a=a+b与a+=b有什么区别

+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。
+=操作符会自动对右边的表达式结果强转匹配左边的数据类型。

在两个变量的数据类型一样时:a+=b 和a=a+b 是没有区别的。但是当两个变量的数据类型不同时,就需要考虑一下数据类型自动转换的问题,也就是涉及到精度了。

13、& 和 &&的区别

&(按位与)是位运算符,只能操作整数类型的变量或值
&&(逻辑与)是逻辑运算符,只能操作布尔boolean类型的变量或值

逻辑运算符具有短路特性,而&不具备短路特性。

14、一个java文件内部是否可以有其他非内部类

只能有一个public公共类,但是可以有多个default修饰(即缺省,什么也不写)的类。当有public类时,java文件必须与public类的类名相同。

15、如何正确的退出多层嵌套循环

使用标签和break,并且应在所在循环的外层循环之前定义标签:
        flag:
        for (...){
            for(...){
                if(...){
                    ...;
                    break flag;
                }
            }
        }

 

16、final、finally和finalize的区别

final修饰符(关键字):
被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,即不能方法重写。

finally:
是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。

finalize:
是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。 

17、深拷贝和浅拷贝的区别是什么

浅拷贝:
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。“里面的对象”会在原来的对象和它的副本之间共享。简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。

深拷贝:
深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

Java中要想自定义类的对象可以被复制,自定义类就必须实现Cloneable中的clone()方法如果想要clone()得到的新对象的修改不会影响被复制的对象的字段时,我们就需要实现深复制(deep copy)。

18、static都有哪些用法

修饰静态变量和静态方法:也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享。
修饰静态块:static块多用于初始化操作。
修饰内部类:称之为静态内部类。
静态导包:可以用来指定导入某个类中的静态资源,并且不需要使用类名。

19、final有哪些用法

被final修饰的类不可以被继承。
被final修饰的方法不可以被重写。
被final修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。
被final修饰的方法,JVM会尝试将其内联,以提高运行效率。
被final修饰的常量,在编译阶段会存入常量池中。

20、各基本(数值)类型数据占多少字节

类型          占用字节        占用位数
byte          1              8
short         2              16
int           4              32
long          8              64
float         4              32
double        8              64
char          2              16
boolean       1              8

21、int和Integer的区别

Integer是int的包装类型,在拆箱和装箱中,二者自动转换。int是基本类型,直接存数值,而integer是对象,用一个引用指向这个对象。

22、String, StringBuffer和StringBuilder区别

String:不可变字符串;频繁操作时,每次都需新开辟内存,极易造成内存浪费
StringBuffer:可变字符串、效率低、线程安全;执行速度慢
StringBuilder:可变字符串、效率高、线程不安全;执行速度快

(1)如果要操作少量的数据用 String;
(2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
(3)单线程操作字符串缓冲区下操作大量数据 StringBuilder(推荐使用)。

StringBuffer和StringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。

StringBuilder类在Java5中被提出,它和StringBuffer之间的最大不同在于StringBuilder的方法不是线程安全的(不能同步访问)。

由于StringBuilder相较于StringBuffer有速度优势,所以多数情况下建议使用StringBuilder类。
然而在应用程序要求线程安全的情况下,则必须使用StringBuffer类。

23、java当中使用什么类型表示价格比较好

由于float和double所表示的浮点数是近似值,不是精确的值,计算时易出现精度丢失的问题,因此,二者不适合作为价格的数据类型。Java语言提供了另外一种数据类型BigDecimal,可以表示精确的浮点数,适合用作财务计算的数据类型。但是需要注意的是,在使用BigDecimal的时候,BigDecimal有多个重载的构造方法能表示精度的值,只有用参数为String类型的构造方法才能表示。

24、float与double精度丢失问题

出现精度丢失的根本原因是计算机使用二进制01无法准确表示某些带小数位的十进制数据。BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数。避免了小数的出现,就不会有精度问题了。

25、将int强转为byte类型会产生什么问题

byte是用1个字节8位来存储,只能表示-128~127之间的数,int是用4个字节32位来存储,当由int强制类型转化为byte时,系统就采取了截取int后8位,当int值超出byte范围时,就会出现数字越界。

26、常用垃圾回收算法

标记-清除
标记-复制
标记-整理
分代回收

27、如何判断一个对象是否应该被回收

这就是所谓的对象存活性判断,常用的方法有两种:1.引用计数法; 2.对象可达性分析。由于引用计数法存在互相引用导致无法进行GC的问题,所以目前JVM虚拟机多使用对象可达性分析算法。

28、简单的解释一下垃圾回收

GC线程是jvm的一个守护线程,会在必要的时候不定时的进行垃圾回收(回收内存中的对象),释放资源、清除内存记录碎片,保证程序正常运行。

垃圾回收机制最基本的做法是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成3个世代:年轻、年老和永久。内存的分配是发生在年轻世代中的。当一个对象存活时间足够长的时候,它就会被复制到年老世代中。对于不同的世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。

29、调用System.gc()会发生什么

程序中主动执行GC:System.gc(),只是告诉JVM尽快GC一次,但不会立即执行GC。Java的GC是由JVM自行调动的,在需要的时候jvm才会执行GC。

30、进程和线程的区别

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

31、守护线程与非守护线程

Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。
守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,JVM就会退出,因为没有如果没有了被守护者,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,JVM就不会退出。

虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。

另外有几点需要注意:
1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
2、在守护线程中产生的新线程也是守护线程。
3、不是所有的应用都可以分配给 Daemon 线程来进行服务,比如读写操作或者计算逻辑,因为在 Daemon 线程还没来的及进行操作时虚拟机可能已经退出了

32、什么是多线程上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

33、创建两种线程的方式,他们有什么区别

通过实现Runnable或者通过继承Thread类。相比继承Thread,实现Runnable接口可能更优,原因有二:

Java不支持多继承。因此扩展Thread类就代表这个子类不能扩展其他类。而实现Runnable接口的类还可能扩展另一个类。

类可能只要求可执行即可,因此继承整个Thread类的开销过大。

34、Thread类中的start()和run()方法有什么区别

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

35、怎么检测一个线程是否持有对象监视器

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。

36、Runnable和Callable的区别

相同点:
两者都是接口;(废话)
两者都可用来编写多线程程序;
两者都需要调用Thread.start()启动线程;

不同点:
两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果,当不调用此方法时,主线程不会阻塞。

37、哪些方法回阻塞线程

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)

sleep():
sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。

suspend() 和 resume():
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。

yield():
yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。

wait() 和 notify():
两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。

38、什么情况,线程会进入阻塞状态

线程调用sleep()方法主动放弃CPU执行权(但不释放同步锁)
线程调用了一个阻塞式IO方法,在该方法返回之前,线程被阻塞
线程试图获取一个同步监视器,但同步监视器正被其他线程持有
线程正在等待某个通知(notify)
程序调用了线程的suspend()方法将该线程挂起

39、什么情况,线程进入就绪状态,等待被CPU重新执行

调用sleep()方法的线程经过了指定的时间
线程调用的阻塞式IO方法已经返回
线程成功的获取了试图获取的同步监视器
线程正在等待某个通知,其他线程发出了一个通知
处于挂起状态的线程被调用了resume()恢复方法

40、wait(),notify()和suspend(),resume()之间的区别

wait()释放CPU执行权,也释放同步锁,使得其他线程可以使用同步控制块或者方法。

sleep()释放CPU执行权,但不释放同步锁;

suspend()释放CPU执行权,但不释放同步锁;

suspend(),resume()属于Thread 类,但wait(),notify()却直接属于Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的wait()方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致从调用该对象的wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

suspend(),resume()方法都可在任何位置调用,但wait(),notify()方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。

关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。

41、死锁产生的原因及四个必要条件

产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

42、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁,该方法或块的上锁对象就是调用这一对方法的对象。

43、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

44、wait()与sleep()的区别

sleep()来自Thread类,和wait()来自Object类。调用sleep()方法的过程中,线程不会释放对象锁。而调用wait方法线程会释放对象锁。

sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()或者notifyAll()使用。

45、为什么wait, nofity和nofityAll这些方法不放在Thread类当中

一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。

简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

46、synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

47、FutureTask是什么

这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

48、一个线程如果出现了运行时异常怎么办

如果这个异常没有被捕获的话,这个线程就停止执行了。
另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。

49、如何在两个线程间共享数据

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

50、什么是线程局部变量ThreadLocal

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,应用就存在内存泄露的风险。

51、ThreadLoal的作用是什么

简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

52、生产者消费者模型的作用是什么

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要受到相互的制约。

53、为什么要使用线程池

系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统交互。在这种情况下,使用线程池能够很好的提高性能,尤其程序中需要创建大量生存期很短的线程时,更应该使用线程池。

使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数能控制系统中最大并发线程数不超过最大线程数。

避免频繁地创建和销毁线程,达到线程对象的重用。

另外,使用线程池还可以根据项目灵活地控制并发的数目。

54、java中用到的线程调度算法是什么

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

饥饿产生的原因:
(1)其它线程吞噬所有的 CPU 时间。
(2)线程被长期堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

饥饿怎么处理:
系统调度依据优先级来选择线程,但是如果优先级较低的线程长期不能得到执行,比如(2~4秒),系统就会认为该线程处于“饥饿”状态,就会临时提高该线程的优先级使得其可以执行一到两次。实际应用中,我们应当确保优先级高的程序能够快速执行,然后恢复到睡眠或者挂起状态,以保证最大的实时性,而让优先级低的程序在大多时候可以调度执行。

55、Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

56、什么是乐观锁和悲观锁

乐观锁:
乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

悲观锁:
悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

57、ArrayList和LinkedList的区别

最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
1. ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构;

2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;

3. 对于添加和删除操作add和remove,LinkedList要比ArrayList快,因为ArrayList要移动数据。

58、Array与ArrayList的区别

(1)数组Array类可以支持一维、二维和多维,而ArrayList类相当于一维数组,不支持多维数组

(2)数组存储的元素类型必须一致,而ArrayList可以存储不同类型的元素

(3)数组Array类在创建时必须指定大小且是固定的,不能随意更改,ArrayList类创建时可以不指定大小,使用过程中容量可以根据需要自动扩充。

(4)数组Array和ArrayList创建时语法格式不同。

(5)数组Array对象在获得元素个数时通过数组的Length属性,ArrayList对象在获得元素个数时通过集合的Count属性。

(6)数组为元素赋值时可以通过创建时初始化值或给单个元素赋值,ArrayList对象只能通过Add、Insert等方法为元素赋值。

虽然数组和ArrayList对象之间有很多区别,但是他们之间还是可以互相转化的。例如,可以在创建ArrayList对象时,把数组元素添加到ArrayList对象中;也可以通过ArrayList的CopyTo方法将ArrayList对象元素复制到数组中。

59、ArrayList和HashMap默认大小

ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。

60、JDK、JRE、JVM关系是什么

JDK(Java Development Kit)即为Java开发工具包,包含编写Java程序所必须的编译、运行等开发工具以及JRE。开发工具如:用于编译java程序的javac命令、用于启动JVM运行java程序的java命令、用于生成文档的javadoc命令以及用于打包的jar命令等等。

JRE(Java Runtime Environment)即为Java运行环境,提供了运行Java应用程序所必须的软件环境,包含有Java虚拟机(JVM)和丰富的系统类库。系统类库即为java提前封装好的功能类,只需拿来直接使用即可,可以大大的提高开发效率。

JVM(Java Virtual Machines)即为Java虚拟机,提供了字节码文件(.class)的运行环境支持。

简单说,就是JDK包含JRE包含JVM。

61、HashMap的工作原理

特性:
基于hashing(散列算法)原理
以键值对的方式存储数据
可以接受null键值和值
非线程安全的
速度快,执行效率高

工作原理:
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

当两个对象的hashcode相同会发生什么:
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

如果两个键的hashcode相同,你如何获取值对象:
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。如果有两个值对象储存在同一个bucket,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper包装类作为键是非常好的选择。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办:
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

重新调整HashMap大小存在什么问题:
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

为什么String, Interger这样的wrapper类适合作为键:
 String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

我们可以使用自定义的对象作为键吗:
可以使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

我们可以使用CocurrentHashMap来代替Hashtable吗:
我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

62、set中的元素为什么不能重复

Set接口的实现类中的元素使用Map保存的,并且元素的值是保存在Map的Key中的,因为Map的Key不能重复,所以用它保存的Set元素当然也就不能是重复的。

例如HashSet中元素就存在HashMap的Key中

63、遍历list的时候为什么不能修改呢

使用for循环、增强for循环foreach、Iterator遍历集合时,不能对集合做添加和删除操作:
1.在遍历时往数组增加数据,会导致遍历不完整(因为增加了新成员长度变了),或者死循环(因为总是有新的进去)
2.在遍历时删除数据,则会导致数组访问越界(因为长度缩短了,指针指向了一个已经标示为空的区域)

否则会引发ConcurrentModificationException异常

64、Java支持的数据类型有哪些?什么是自动拆装箱

基本数据类型:
整数值型:byte,short,int,long,
字符型:char
浮点类型:float,double
布尔型:boolean
整数默认int型,小数默认是double型。Float和long类型的必须加后缀。

首先知道String是引用类型不是基本类型,引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。引用类型包括类、接口、数组等。String类还是final修饰的。而包装类就属于引用类型,自动装箱和拆箱就是基本类型和引用类型之间的转换,至于为什么要转换,因为基本类型转换为引用类型后,就可以new对象,从而调用包装类中封装好的方法进行基本类型之间的转换或者toString(当然用类名直接调用也可以,便于一眼看出该方法是静态的),还有就是如果集合中想存放基本类型,泛型的限定类型只能是对应的包装类型。

65、什么是值传递和引用传递

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。

引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。

一般认为,java内的传递都是值传递, java中实例对象的传递是引用传递 。

66、java中构造方法及其作用

构造方法作用就是对创建的对象进行初始化。 如果没有定议任何构造方法,程序会取一个不带任何参数的构造函数,创建类的对像时只能用不带参数的方法。

构造方法的特点:

1:方法名称和类同名

2:不用定义返回值类型

3:不可以写retrun语句

4:构造方法可以被重载

67、用最有效率的方法计算2乘以8

2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)

68、单例模式(饿汉和懒汉模式)和工厂模式

饿汉式单例类:在类初始化时,已经自行实例化

懒汉式单例类:在第一次调用的时候实例化

69、什么是迭代器(Iterator)

Iterator接口提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的迭代方法。迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调用集合的remove(Object Obj)删除,可以通过迭代器的remove()方法删除。

使用迭代器访问集合时,集合里的元素不能被更改,否则会引发异常

70、Iterator和ListIterator的联系和区别

1.ListIterator有add()方法,可以向List中添加对象,而Iterator不能。

2.ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

3.ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator 没有此功能。

4.都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。

5.Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。

71、HashMap和Hashtable有什么区别

1、HashMap是非线程安全的,HashTable是线程安全的。
2、HashMap的键和值都允许有null值存在,而HashTable则不行。
3、因为线程安全的问题,HashMap效率比HashTable的要高。
4、Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。

72、HashMap结构

Node<K,V>:链表节点,包含了key、value、hash、next指针四个元素。
table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体(table数组的大小,缺省值为16)。
size:记录了放入HashMap的元素个数。
loadFactor:负载因子(缺省值为0.75)。
threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

73、HashMap什么时候扩容

当向容器添加元素的时候,会判断当前容器(table数组)的元素个数,如果大于等于阈值,即当前数组的长度乘以加载因子(缺省值为0.75)的值的时候,就要自动扩容啦。

就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

74、ConcurrentHashMap的原理

底层采用分段的数组+链表实现,线程安全

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。)

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁

扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,分段扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。

75、ConcurrentHashMap怎么保证线程安全

ConcurrentHashMap是使用了锁分段技术来保证线程安全的:
首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。

76、极高并发下HashTable和ConcurrentHashMap哪个性能更好

ConcurrentHashMap是使用了锁分段技术来保证线程安全,将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶,效率提升16倍。

77、java中四种修饰符的限制范围

访问权限        本类   本包   不同包子类  不同包非子类
public          √     √      √          √
protected       √     √      √
default         √     √
private         √

78、Object类中的常用方法

equale()用于确认两个对象是否相同。

hashCode()用于获取对象的哈希值。

toString()用于将任何一个对象转换成字符串返回。

wait(),wait(long),wait(long,int),notify(),notifyAll():
这几个函数体现的是Java的多线程机制,在使用的时候要求在synchronize语句中使用。
wait()用于让当前线程失去操作权限,释放锁,当前线程进入等待序列。
notify()用于随机通知一个持有对象的锁的线程获取操作权限。
notifyAll()用于通知所有持有对象的锁的线程获取操作权限。
wait(long) 和wait(long,int)用于设定下一次获取锁的距离当前释放锁的时间间隔。

finalize():
当gc回收一个对象的时候,主动会调用这个对象的finalize方法。

79、动态代理的两种方式以及区别

JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。

1.JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高;
2.JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法,CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。

80、Java中实现序列化的两种方式

方法一:
实现Serializable(可序列化)接口。
一个对象想要被序列化,那么它的类就要实现此接口,这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。

序列化的时候的一个关键字:transient(临时的)。它声明的变量实行序列化操作的时候不会写入到序列化文件中去。

方法二:
实现Externalizable(可外部化的)接口:
他是Serializable接口的子类,有时我们不希望序列化那么多,可以使用这个接口,这个接口的writeExternal()和readExternal()方法可以指定序列化哪些属性;

一个类中我们只希望序列化一部分数据,其他数据都使用transient修饰的话显得有点麻烦,这时候我们使用externalizable接口,指定序列化的属性。
public class Person implements Externalizable {

    private String name;                      // 声明name属性
    private int age;                          // 声明age属性

    public Person() {
    }                        // 必须定义无参构造

    public Person(String name, int age) {    // 通过构造方法设置属性内容
        this.name = name;
        this.age = age;
    }

    public String toString() {                 // 覆写toString()方法
        return "姓名:" + this.name + ";年龄:" + this.age;
    }

    // 覆写此方法,根据需要可以保存属性或具体内容,序列化时使用
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(this.name);         // 保存姓名属性
        out.writeInt(this.age);            // 保存年龄属性
    }

    // 覆写此方法,根据需要读取内容,反序列化时使用
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();  // 读取姓名属性
        this.age = in.readInt();             // 读取年龄
    }


}

81、Java的类加载过程

一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译、运行:

编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。

运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。

而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

Java的类加载过程:
加载-->链接(验证-->准备-->解析)-->初始化

加载
**************
加载:把class字节码文件从各个来源通过类加载器装载入内存中

这里有两个重点:
字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译

类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。

为什么会有自定义类加载器:
一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。

另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

验证
**************
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

准备
**************
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456,那么该阶段tmp的初值就是456

解析
**************
将常量池内的符号引用替换为直接引用的过程
两个重点:
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法、实例变量的直接引用则是从实例的头指针开始算起到这个实例方法、实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

初始化
**************
这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

总结:
类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载。

82、Java类的生命周期

Java类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

83、Java有没有主动触发GC的方式

没有

程序中主动执行GC:System.gc(),只是告诉JVM尽快GC一次,但不会立即执行GC。Java的GC是由JVM自行调动的,在需要的时候jvm才会执行GC。

84、对ThreadLocal的理解

ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。

ThreadLocal不能代替线程同步机制,通常建议:
如果多个线程之间需要共享资源,以达到线程之间通讯的功能,就使用同步机制;
如果仅仅需要隔离多个线程之间对资源共享的冲突,避免多个线程对共享资源的竞争,则使用ThreadLocal。

这些面试题都整理成了一个PDF文档,欢迎扫描二维码,加好友领取Java面试题资料。


这些面试题都整理成了一个PDF文档,欢迎扫描二维码,加好友领取Java面试题资料。


 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!