java知识总结

1、面向对象的三个基本特征 封装:隐藏部分对象的属性和实现细节,对数据的访问只能通过对外公公开的接口。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。 继承:让某个类型的对象获得另一个类型的对象的属性的方法。继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 多态:对于同一个行为,不同的子类对象具有不同的表现形式。多态存在的3个条件:1、继承;2、重写;3、父类引用指向子类对象。 举个简单的例子:英雄联盟里面我们按下 Q 键这个动作: 对于亚索,就是斩钢闪 对于提莫,就是致盲吹箭 对于剑圣,就是阿尔法突袭

2、访问修饰符public,private,protected,以及不写时的区别

3、下面两个代码块能正常编译和执行吗?

代码块1编译报错,错误原因是:不兼容的类型;从int转换到short可能会有损失 代码块2正常编译和执行 我们将代码块2进行编译,字节码如下:

可以看到字节码中包含了i2s指令,该指令用于将int转成short。I2s是 int to short的缩写 其实,s1+=1 相当于s1=(short)(s1+1).

4、基础考察,指出下题的输出结果

答案是:false,true 执行integer a = 128,相当于执行:integer a =integer.valueOf(128),基本类型自动转换为包装类的过程称为自动装箱

在integer中引入了integerCache来缓存一定范围的值,integerCache默认情况下范围为: -128~127。本题中的127命中了integerCache,所以c和d是相同对象,而128则没有命中,所以a和b是不同对象。但是这个缓存范围是可以修改的,可能有些人不知道。可以通过JVM启动参数:-XX:AutoBoxCacheMax=

5、用最有效率的方法计算2乘以8 2<<3 进阶:通常情况下,可以认为位运算是性能最高的。但是,其实编译器现在已经“非常聪明了”,很多指令编译器都能自己做优化。所以在实际实用中,我们无需特意去追求使用位运算,这样不仅会导致代码可读性很差,而且某些自作聪明的优化反而会误导编译器,使得编译器无法进行更好的优化。

6、&和&&的区别 &&:逻辑与运算符。当运算符左右两边的表达式都为true,才返回true。同时具有短路性,遇假则假。 &:逻辑与运算符、按位与运算符。 按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1,否则位0。 逻辑与运算符:&在用于逻辑与时,和&&的区别时不具有短路性。所在通常使用逻辑与运算符都会使用&&。而&更多的适用于位运算。

7、string时java基本数据类型吗? 不是,java中的基本数据类型只有8个:int、float、short、long、boolean、double、byte、char,除了基本类型,剩下的都是引用类型。 基本数据类型:数据直接存储在栈上 引用数据类型:数据存储在堆上,栈上只存储引用地址

8、string类可以继承吗? 不行,string类使用final修饰,无法被基础。

9、String和stringBuilder、stringBuffer的区别? String:string的值被创建后不能修改,任何对string的修改都会引发新的string对象的生成。 StringBuffer:跟string相似,但是值可以被修改,使用synchronized来保证线程安全 StringBuilder:stringBuffer的非线程安全版本,没有使用synchronized,具有更高的性能,推荐优先使用。

10、string s = new string(“xyz”)创建了几个字符串对象? 一个或两个。如果字符串常量池已经有”xyz”,则是一个;否则,两个。 当字符串常量池没有”xyz”,此时会创建如下俩个对象: 一个是字符串字面量”xyz”所对应的、驻留在一个全局共享的字符串常量池中的实例,此时该实例也是在堆中,字符串常量池只放引用。 另一个时通过new String()创建并初始化的,内容与”xyz”相同的实例,也是在堆中。

11、string s = “xyz” 和string s = new string(“xyz”)区别? 两个语句都会先去字符串常量池中检查是否已经存在”xyz”,如果有则直接使用,如果没有则会在常量池中创建”xyz”对象。 另外,string s=new string(“xyz”)还会通过new string()在堆里创建一个内容与”xyz”相同的对象实例。所以前者其实理解为被后者所包含。

12、==和equals的区别是什么? ==:运算符,用于比较基础类型变量和引用类型变量。 对于基础类型变量,比较的变量保存的值是否相同,类型不一定要相同。

对于引用类型变量,比较的是两个对象的地址是否相同。

Equals : object类中定义的方法,通常用于比较两个对象的值是否相等。 equals在Object方法中其实等同于==,但是在实际的使用中,equals通常被重写用于比较两个对象的值是否相同。

13、两个对象的hashCode()相同,则equals()也一定为true,对吗? 不对。 当有a.equals(b) == true时,则a.hashCode()==b.hashCode()必然成立, 反过来,当a.hashCode()==b.hashCode()时, a.equals(b)不一定为true.

14、什么是反射 反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为反射机制。

15、深拷贝和浅拷贝区别是什么? 数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。

浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。

深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。

深拷贝相比于浅拷贝速度较慢且花销较大。

16、并发和并行有什么区别? 并发:两个或多个事件在同一时间间隔发生。 并行:两个或者多个事件在同一时刻发生。 并行是真正意义上,同一时刻做多件事情,而并发在同一时刻只会做一件事情,只是可以将事件切碎,交替做多件事情。

17、构造器是否可被重写? constructor不能被override(重写),但是可以overload(重载),所以你可以看到一个类中有多个构造函数的情况。

18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 值传递。Java中只有值传递,对于对象参数,值的内容是对象的引用。

19、java静态变量和成员变量的区别

成员变量存在于堆内存中。静态变量存在于方法区中。 成员变量与对象共存亡,随着对象创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而加载,随着类的消失而消失。 成员变量所属于对象,所以也成为实例变量。静态变量所属于类,所以也称为类变量。 成员变量只能被对象所调用。静态变量可以被对象调用,也可以被类名调用。

20、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用? 区分两种情况,发出调用时是否显示创建了对象实例。 没有显示创建对象实例,不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。因此通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。

显示创建对象实例:可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用。

21、初始化考察,请指出下面程序的运行结果

执行结果:ABabab,两个考察点: 静态变量只会初始化(执行)一次 当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器->子类非静态变量(非静态代码块)-> 子类构造器。

22、重载和重写的区别? 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 重载:一个类中有多个同名的方法,但是具有不同的参数列表(参数类型不同、参数个数不同或者二者都不同) 重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变, 核心重写,重写的好处在于子类可以根据需要,定义特定于自己的行为。

23、抽象类和接口有什么区别? 抽象类只能单继承,接口可以多实现。 抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是public static final) 抽象类中可以包含非抽象的方法,在jdk1.7之前接口中的所有方法都是抽象的,在jdk1.8之后,接口支持非抽象方法:default方法、静态方法等。Jdk1.9支持私有方法、私有静态方法。 抽象类中的方法类型可以是任意修饰符,jdk1.8之前接口中的方法只能是public类型,jdk1.9支持private类型。

24、Error和Exception有什么区别 Error和Exception都是Throwable的子类,用于表示程序出现了不正常的情况。区别在于: Erro表示系统级的错误和程序不必处理的异常,是恢复不是不可能但是很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。 Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。

25、java中的final关键字有哪些用法? 修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract和final。 修饰方法:该方法不能被子类重写。 修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。

26、阐述final、finally、finalize的区别 Final如上所示 finally是对java异常处理机制的最佳补充,通常配合try、catch使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到finally中,可以大大降低程序出错的几率。 finalize:object中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。Finalize()方法仅作为了解即可,在java9中该方法已经被标记为废弃,并添加新的java.lang.cleaner,提供了更灵活和有效的方法来释放资源。这也侧面说明了,这个方法的设计是失败的,因此更加不能去使用它。

27、try、catch、finally考察,请指出下面程序的运行结果

28、jdk1.8之后有哪些新特性? 接口默认方法:jdk1.8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可 lambda表达式和函数式接口:lambda表达式本质是一段匿名内部类,也可以是一段可以传递的代码。Lambda允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用lambda表达式使代码更加简洁,但是也不要滥用,否则会有可读性差等问题,建议使用最好不要超过三行。 Stream流:用函数式编程方法在集合类上进行复杂操作的工具,配合lambda表达式可以方便的对集合进行处理。Jdk1.8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射等操作。使用stream流对集合数据进行操作,就类似于使用sql执行的数据库查询。也可以使用stream来并执行操作。提供了一种高效且易于使用的处理数据的方式。 方法引用:方法引用提供了非常有用的语法,可以直接引用已有java类或对象的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简介,减少冗余代码。 日期API;jdk1.8引入了新的日期时间API改进了日期时间的管理。 optional类:著名的nullPointerException是引起系统失败最常见的原因。很久以前Google Guava项目引入了Optional作为解决空指针异常的一种方式,不赞成代码被null检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optional现在是jdk1.8库的一部分。 新工具:新的编译工具,如:Nashorn引擎jjs、类依赖分析器jdeps。

29、wait()和sleep()方法的区别 来源不同:sleep()来自Thread类,wait()来自Object类。 对于同步锁的影响不同:如果当前线程持有同步锁,那么sleep是不会让线程释放同步锁的。Wait()会释放同步锁,让其他线程进入synchronized代码块先执行。 使用范围不同:sleep()可以在任何地方使用。Wait()只能在同步控制方法或者同步控制块里面使用,否则会抛IllegalMonitorStateException。 恢复方式不同:两者会暂停当前线程,但是在会附上不太一样。Sleep()在时间到了之后会重新恢复;wait()则需要其他线程调用同一对象的notify()/nofityAll()才能恢复。

30、线程的sleep()方法和yield()方法有什么区别? 线程执行()sleep()方法后进入超时等待(TIMED_WAITING)状态,而执行yield()方法后进入就绪(READY)状态。 Sleep()方法给其他线程运行机会时候不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。

31、线程的join()方法是干啥用的? 用于等待当前线程终止。如果一个线程A执行了threadB.join()语句,其含义是:当前线程A等待threadB线程终止之后采从threadB.join()返回继续往下执行自己的代码。

32、编写多线程程序有几种实现方式? 继承Thread类 实现Runnable接口 实现Callable接口 其中,Thread其实也是实现了Runnable接口。Runnable和Callable的主要区别在于是否有返回值。

33、Thread调用start()方法和调用run()方法的区别。 Run():普通的方法调用,在主线程中执行,不会新建一个线程来执行。 Start():新启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到COU时间片,就开始执行run()方法。

34、线程的状态流程

一个线程可以处于以下状态之一: NEW:新建但是尚未启动的线程处于此状态,没有调用start()方法。 RUNNABLE:就绪(READY)和运行中(RUNNING)两种状态。线程调用start()方法会进入就绪(READY)状态,等待获取CPU时间片。如果成功获取到CPU时间片,则会进入运行中(RUNNINGN)状态。 BLOCKED:线程在进入同步方法/同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。 WAITINGl:无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于Object.wait(),需要等待另一个线程执行Object.notify()或notifyAll();对于Thread.jogin(),则需要等待指定的线程终止。 TIMED_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。 TERMINATED:执行完毕已经推出的线程处于此状态。 线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。

35、synchronized和lock的区别 1、Lock是一个接口;synchronized是java中的关键字,synchronized是内置的语言实现; 2、Lock在发生异常时,如果没有主动通过unLock()去释放锁,很可能会造成死锁现象,因此使用Lock时需要在finally块中释放锁;synchronized不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生; 3、Lock的使用更加灵活,可以有响应中断、有超时时间等;而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,直到获取到锁。 4、在性能上,随着近些年synchronized的不断优化,Lock和synchronized在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用synchronized,除非synchronized无法满足需求时,则可使使用Lock。 36、synchronized各种加锁场景的作用范围 1、作用于非静态方法,锁住的是对象实例(),每一个对象实例有一个锁。

2、作用于静态方法,锁住的是类的class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。

3.作用于Lock.class,锁住的是Lock的Class对象,也是全局只有一个。

4、作用于this,锁住的是对象实例,每一个对象实例有一个锁

5、作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

36、如何检测死锁? 死锁的四个必要条件: 互斥条件:进程所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。 请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。 不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。 环路等待条件:存在一种进程资源的循环等待链,锁中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{P1,P2,...Pn},其中Pi等待的资源被P(i+1)占有(i=0,1,...,n1),Pn等待的资源被P0占有,如下图所示

37、怎么预防死锁 预防死锁的方式就是打破四个必要条件中的任意一个即可。 1、打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,”互斥”条件是无法破坏的因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。 2、打破请求和保持条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。每个进程提出资源申请前,必须先释放它先前说占有的资源。 3、打破不可剥夺条件:当进程占有某些资源后有进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。 4、打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用序号递增的形式申请资源。

38、为什么要使用线程池?直接new个线程不是很舒服? 如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性。 如果合理的使用线程池,可以带来以下几个好处: 1、降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。 2、提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 3、增加线程的可管理性。线程是稀缺资源使用线程池可以进行统一分配,调优和监控。

39、线程池的核心属性有哪些? threadFactory(线程工厂):用于创建工作线程的工厂。 corePoolSize(核心线程数):当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。 WorkQueue(队列):用于保留任务并移交给工作线程的阻塞队列。 MaximumPoolSize(最大线程数):线程池允许开启的最大线程数。 Handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:线程池运行状态不是running;线程池已经达到最大线程数,并且阻塞队列已满。 KeepAliveTime(保持存活时间):如果线程池当前线程数超过corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被终止。

40、线程池的运作流程

41、线程池有哪些拒绝策略? AbortPolicy::中止策略。默认的拒绝策略,直接抛出RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。 DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。 CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行人物的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有实际按来处理完正在执行的任务。

42、List、Set、Map三者的区别 List<对付顺序的好帮手>:List接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。 Set(注重独一无二的性质):不允许重复的集合,不会有多个元素引用相同的对象。 Map(用Key来搜索的专业户):使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是string类型,但也可以是任何对象。

43、ArrayList和LinkedList的区别。 ArrayList底层基于动态数组实现,LinkedList底层基于链表实现。 对于按Index索引数据(get/set方法):ArrayList通过Index直接定位到数组对应位置的节点,而LinkedList需要从头节点或尾系欸但开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList。 对于随即插入和删除:ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的next或prev属性即可,因此在效率上LinkedList优于ArrayList。 对于顺序插入和删除:由于ArrayList不需要移动节点,因此在效率上比LinkedList更好。这也是为什么在实际使用中ArrayList更多,因为大部分情况下我们的使用都是顺序插入。

44、ArrayList和Vector的区别 Vector和ArrayList几乎一致,唯一的区别是Vector在方法上使用了synchronized来保证线程安全,因此在性能上ArrayList具有更好的表现。 有类似关系的还有:stringBuilder和stringBuffer、hashMap和hashtable。

45、介绍下HashMap的底层数据结构 Jdk1.8之前是由“数组+链表”组成,jdk1.8使由“数组+链表+红黑树”组成。

46、为什么要改成“数组+链表+红黑树”? 主要是为了提升在hash冲突严重时(链表过长)的查找性能,使用链表的查找性能是O(n),而使用红黑树是O(logn)。

47、那在什么时候用链表?什么时候用红黑树? 对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(或阈值8):如果此时数组长度大于等于64,则会触发链表节点转红黑树节点,而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。 对于移除,当同一个索引位置的节点在移除后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。

48、HashMap的默认初始容量是多少?HashMap的容量有什么限制吗? 默认初始容量是16。HashMap的容量必须是2的N次方,HashMap会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传9,容量为16。

49、HashMap的插入流程是怎么样的?

50、HashMap的扩容流程是怎么样的?

51、除了HashMap,还用过哪些Map,在使用时怎么选择?

52、HashMap和Hashtable的区别? HashMap允许key和value为null,Hashtable不允许。 HashMap的默认初始容量为16,Hashtable为11。 HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。 HashMap时非线程安全的,Hashtable是线程安全的。 HashMap的hash值重新计算过,Hashtable直接使用hashCode。 HashMap去掉了Hashtable中的contains方法。 HashMap继承自AbstractMap类,Hashtable继承自Dictionary类。

53、java内存结构(运行时数据区)

程序计数器:线程私有。一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。 Java虚拟机栈:线程私有。它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出战的过程。 本地方法栈:线程私有。本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 Java堆:线程共享。对大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 方法区:与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即时编译器编译后的代码(字节码)等数据。方法区是JVM规范中定义的一个概念,具体放在哪里,不同的实现可以放在不同的地方。 运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外。

上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”hello”这个字面量是放在堆中。

54、什么是双亲委派模型? 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

55、java虚拟机中有哪些类加载器? 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.LauncherAppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器:用户自定义的类加载器。

56、类加载的过程 类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。 加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表着各类的java.lang.Class对象。 验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值”通常情况”下是数据类型的零值。 解析:将常量池内的符号引用替换为直接引用。 初始化:到了初始化阶段,才真正开始执行类中定义的java初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

57、介绍下垃圾收集机制(在什么时候,对什么,做了什么)? 在什么时候? 在触发GC的时候,具体如下,这里只说常见的Young GC和Full GC。 触发Young GC:当新生代中的Eden区没有足够空间进行分配时会触发Young GC。 触发Full GC: 当准备要触发一次Young GC时,如果发现统计数据说之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Young GC而是转为触发Full GC。(通常情况) 如果由永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次Full GC。 System.gc()默认也是触发Full GC。 Heap dump 带GC默认也是触发Full GC。 CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生。

对什么? 对那些JVM认为已经”死掉”的对象。即从GC Root开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。

做了什么? 对这些JVM认为已经”死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。

58、GC Root有哪些? 在java语言中,可作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象。 方法区中类静态属性引用的对象。 方法区中 常量引用的对象。 本地方法栈中JNI(即一般说的Native方法)引用的对象。

96、垃圾收集有哪些算法,各自的特点? 标记 - 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

标记 - 整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。


本文章使用limfx的vscode插件快速发布