Advertisement

Java八股文——面试必问

阅读量:
Java一次编写到处运行

JVM(Java Virtual Machine),存储了以 .java 标识符命名的源代码文件。通过编译器将这些原始代码转换为 .class 格式的 bytecode 文件。然后将这些编译后的 bytecode 文件分配至各平台对应的 Java 虚拟机进行运行。

Java的数据类型

涵盖基本数据类型与引用数据类型的范畴内

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0Emwjuf-1639014037502)(C:\Users\86130\AppData\Roaming\Typora\typora-user-images\image-20211204153047326.png)]

Java语言的编码方案

遵循Unicode编码规范,并赋予每个字符独特的数值,在任何编程语言、平台或系统中都无需担忧其应用

Java的访问权限

三种访问修饰符:private、protected、public

四种访问权限:private、default、protected、public

修饰成员变量/成员方法时,

  • 私有:在该类型的实例或静态方法中只能被实例变量和该方法所在的静态块中的变量所直接访问。
    • 默认:在该类型的实例或静态方法中只能被实例变量、该方法所在的静态块中的变量以及同名的静态方法所直接访问。
    • 保护型:在该类型的实例或静态方法中只能被实例变量、该方法所在的静态块中的变量以及其子类型所直接访问。
    • 公共/共享默认值(Public/Shared by default):在该类型的实例或静态方法中可被所有继承自父类型的所有类型及其所属的静态块中的变量所直接访问。

修饰类,只能由两种访问权限

  • default:同一包下的其他类
  • public:任一包下的任一类访问

final、final、finalize的区别

  • 一旦某个类被标记为final,则它不能再继承其他类。
  • 任何拥有final关键字的方法都无法被重新定义。
  • 被final修饰的变量无法进行修改(这里的修改指的是引用而非对象本身)。

finally通常是配合try-catch语句使用,在出现异常时会自动执行的一段代码,并将其放在finally部分(负责关闭或释放资源相关的代码)

finalize是一个函数,属于Object类型的成员函数。Object是所有类的父类。该函数通常由垃圾回收器调用。当我们将System.gc()方法调用时(即当我们执行垃圾回收操作),垃圾回收器会调用finalize()来释放内存资源。在这个过程中, finalized对象是否能够被回收的最终决定是由系统做出的。

this关键字

指向对象本身的指针

super

指向自己父类的指针(这个超类是指的是;离自己最近的父类)

以上两个关键字均不适用于static环境

全局变量和局部变量

类内声明的全局变量具有预设初始值特性;而局部变量则仅限于方法内部进行声明与使用。

static的主要意义

这种特性不仅允许无需创建对象即可访问其属性和方法,并且通过将初始化操作放置于static代码块中实现性能优化。值得注意的是这类优化仅在类加载时执行。
因此将只需执行一次的一次性的初始化操作放置于static代码块中进行。

Java跳出多重循环(ok: break ok;)
为什么要有包装类?

遵循"一切皆对象"原则设计。所有引用型变量均源自于Object类,并可在某些情况下被当作Object类型处理;然而基本数据类型则不可。当某个方法要求接收Object类型的参数而实际输入却为数值时,则需采取特别处理。借助Java提供的包装类...问题迎刃而解。

自动封装:可以直接将一个基本数据类型的数据赋给对应的包装类型。

自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本数据类型。

Integer与Double如何进行比较?
  • 避免使用等于号
    • 无法将数值转换为字符串
      • 带有小数位的浮点数值不可转换
      • 整数值无需小数部分处理
  • 不建议采用compareTo方法进行比较(仅适用于相同类型的变量)
    • 可以将所有数值统一为基本数据类型后再进行比较
Integer与int的区别,二者在进行==运算时得到什么结果

二者再进行比较时,Integer会自动拆箱为int在进行比较

封装的目的是什么?为什么会有封装?

实现对类内部细节的隐藏,并通过设置访问权限来限制外部访问成员变量;允许对数据进行有效性验证;确保对象数据的一致性和正确性;同时有助于提升代码模块的独立性和可维护性

多态的理解?

在Java中可以让一个子类对象立即赋值给一个父类引用变量,并不需要进行类型转换操作(向上转型)。

在Java编程中,在调用同一个方法时,同一类别下的不同变量可能会表现出多样化的行为特性即被称为多态性。

继承、重载、将子代实例绑定到父代引用;通过多态机制的具体实现对父代进行功能扩展或修改;定义接口;使用抽象类型来表示共同的行为模式;定义基元类型时需具体化其功能与行为特征

Java为什么是单继承?

Java在设计过程中参考了C++的语法结构。C++允许一个类同时拥有多个父类,在这种情况下,如果两个不同的父类中都定义了一个相同的方法名称,在这种情况下,子类在访问或覆盖该方法时可能会引起混淆。对于Java来说,每个类只能直接继承自一个单一的上层 class(尽管通过 class inheritance 可以实现多级继承)。

重载和重写的区别?

一个编程语言中的一个语义设计选择必须满足以下条件:不允许同一个名称的两个不同的操作出现在同一个类别中;如果这两个操作具有相同的名称,则它们的参数列表也必须不同。(同一操作在子类别与该类别中的表现形式完全一致);覆盖操作通常发生在子类别与该类别之间;其返回类型以及异常处理与该操作完全一致;其访问权限高于或等于其父操作。

构造方法可不可以重写?

不允许重写任何构造方法。每个构造方法都必须与其所属的类名保持一致;若假设某个子类试图修改其父类的构造方法,则意味着该子类必然会出现与其自身定义不一致的情况;这显然违背了构造方法的基本原则

Object类中的方法

Class<?>getClass:返回对象运行时的类。

boolean equals(Object obj):判断指定对象与该对象是否相等。

int hasCode():返回该对象的hasCode值:类似于哈希?

String toString():返回可以表述该对象信息的字符串

wait、notify和notifyAll控制线程的暂停与运行;通过调用clone方法可以获得当前对象的一个副本,并且该副本由protected修饰;finalizer方法参与垃圾回收流程

hasCode()和equals()

hasCode用于获得哈希码,equals用于比较两个对象是否相等。

两个对象相等必须有一样的哈希码,两个对象有相同的哈希码,未必相等

==和equals()

当处理引用数据类型时(如指针或对象引用),通过双等于号操作符能够判断两数之间是否存在相等关系;而当处理引用数据类型时(如指针或对象引用),通过双等于号操作符能够判断其内存地址的一致性如何;同时,在这种情况下(即处理引用数据类型),可以通过双等于号操作符来分析该对象与其他对象进行对比分析

当equals()未被显式实现时,默认情况下Object会使用==运算符来比较两个对象的内存地址一致性。一旦对equals()进行重写后,则基于内容对象进行等效性比较。

String、StringBuffer和StringBuilder的区别

基本类型String是不支持变化的类,并且通过关键字final标记以确保其无法继承或重写。一旦生成一个String对象后,则不能更改其属性值直至该对象被完全销毁。

该类可表示可变长度的字节序列所对应的字符串。通过其内置方法(append、setCharAt等),可以对该字符串进行修改。当构建完成后形成了所需的最终字符串时,则可通过toString()方法将其转换为一个String对象。

StringBuilder和StringBuffer都是可变字符串对象,并且它们共享相同的父类结构以及基本相同的方法集合

使用字符串时,new和“”更推荐哪种方式?

通过调用System.arraycopy方法来复制多个对象并生成新的对象实例,则会使得内存占用增加;通常建议直接指定字符串内容以提高效率

字符串拼接的方式

拼接操作符用于将多个预先定义好的字符串常量进行连接,在编译过程中会被优化为一个完整且不可分割的字符串。该组件StinrBuffer设计支持线程安全,并在其内部实现基于append()方法进行处理。该拼接方式在处理两个以上字符串时表现出较高的性能优势

对象String a = "abc"; 会生成一个新对象来存储字符串内容吗?实际上,在Java中使用语法糖简化了显式调用构造方法的行为。例如:new String("abc") 实际上等价于调用构造函数 public static String(String s) 或者默认构造函数并传递参数的方式(如果使用的是字符数组)。这种表示方法使得代码更加简洁易读而不影响其运行逻辑

JVM通过专门的内存区域——即常量池——来管理所有字符串类型的直接引用。具体来说,在初始化阶段时:

  • 如果条件满足(如if(1)),则可以直接将字符串赋值给变量a;
  • 否则则需要将新字符串‘abc’加入到当前可用的常量池中,并随后完成赋值操作。
    在实际运行过程中:
  • 首先会对所有可访问的预先定义好的字符串进行检查;
  • 然后如果发现没有对应的现有实例,则立即生成新的String对象并将其放入堆内存;
  • 最终会使得变量a指向这个刚生成的新String对象。
接口和抽象类的区别

设计的目的上,二者的区别:

接口本质上是一个规范;从实现者的角度来看,在定义必须提供的服务内容的同时也明确了可访问的功能模块。作为多程序间通信的标准使用。抽象类作为一种模板设计,在系统实现过程中被使用作中间产品。

接口(can)、抽象类(is-a)

改写说明

改写说明

接口中不能包含构造器和初始化块定义,只能定义静态变量。

每个类最多只能有一个单一继承结构,并且能够包含抽象类;然而一个类能够实现多个接口以弥补Java单一继承结构的局限性。

接口的合理利用能够有效地降低程序各模块间的耦合度,并增强系统的可扩展性和可维护性

处理异常的方式

try-catch-finally

将业务代码打包到try块内部,在遇到任何异常时会生成一个异常实例,并将其传递给catch块进行处理。

catch先记录日志,根据异常类型,结合业务情况进行相应处理。

打开了一个资源(如数据库连接等),无论发生异常与否的情况下都需要执行的代码块必须放置在finally块中

当检测到某项错误的条件成立时,则调用throw方法将异常抛出;而当遇到难以处理的情况时,则调用throws关键字将异常抛出以便让JVM进行处理

Finally代码块始终会执行一遍,请务必确保在Finalize代码块中不采用return, throw等会导致程序终止的操作。这些禁止使用的语句可能会使得原本位于try-catch框架中的终止行为无法实现。

Java的异常接口

Throwable为Throwable最上级的父类,在本领域内代表所有无法正常终止的情况。其有两个直接子类别:一个是Error表示无法恢复且不可被捕获的情况;另一个是Exception则允许通过Try-catch机制捕获

Static关键字的理解

使用static关键字修饰的类能够实现继承;可以通过static关键字来修饰成员变量;这些被static关键字修饰的成员属于该类型下的属性;它们不属于单独的对象实例;在该类型首次加载时会被隐式地执行一次;之后就不会再被执行了;无需通过实例化即可访问这些属性;这样就能形成静态代码块;只在类型首次加载时就被执行以提高效率

对泛型的理解

在声明属性或变量时通常省略具体数据类型的设定,在需要特定结构时才会进行动态配置。而泛型机制的主要作用在于解决强制性类型转换带来的设计缺陷。

对Java反射机制的理解

编译期和运行期

处于运行状态时,在Java编程中实现以下功能:一是能够访问该类的所有属性和方法;二是能够调用该对象的任何方法并访问其任何属性;这些功能共同构成Java语言的反射机制。简而言之,在Java环境中只要指定一个类名称就可以利用反射机制获取该类的相关信息。

img
Java反射在实际项目中应用场景有?
  • JavaBean Connectable(JDBC),在创建数据库连接时,默认情况下会调用Reflection API以动态加载所需数据库驱动程序。
  • 很多主流开发框架都提供基于注解或XML的配置接口,在这种情况下解析得到的是一个类资源引用字符串;开发人员可以通过Reflection API动态获取并构造实例对象。
  • 面向切面编程(AOP)的具体实现方案是在程序运行时依赖Reflection API的技术支撑完成对目标对象代理类的生成过程。
Java的四种引用方式?
  • 强引'用':程序首先创建一个新的对象实例,并将该实例赋值给指定的引'用'变量。通过引'用'变量操作实际的对象属于可达状态,在系统垃圾回收机制下不会被收回。
    • 软引'用':常见于内存管理较为严格的软件系统中,在系统内存资源充足的条件下不会触发垃圾回收机制;而当系统内存空间受限时,则会在资源耗尽之前迅速完成垃圾回收。
    • 弱引'用':在系统的垃圾回收循环执行过程中会被标记为可收回对象。
    • 虚引'用'的作用等同于未附加任何引'用'的情形。这种特殊的引'用'方式主要用来追踪特定对象是否已被包含在当前的垃圾收集队列中;并且为了实现其功能需求,必须与其所属的引'用'队列紧密配合才能独立存在。
集合类
Java中的集合类(容器)?

Java中的集合类主要来源于Collection与Map这两个接口的衍生。其中Collection这一接口又衍生出三个子接口:Set、List与Queue。所有Java集合类均基于Set、List、Queue与Map这四个核心接口构建。

当前广泛使用的集合实现类包括HashSet、TreeSet、ArrayList、LinkedList以及ArrayQueue数据结构(线程不安全;键值可允许为空),此外还有TreeMap作为有序映射存储方案

Java中的容器,线程安全和线程不安全的分别有哪些?

位于Java.util包中的大多数容器均为非线程安全对象。例如我们常用的如HashSet、TreeSet、ArrayList等其优点在于运行效率较高。可以通过将这些容器与Collections API相结合来生成相应的同步集合类型。然而像Vector和Hashtable这类集合虽然具备线程安全性但已较为陈旧。

Concurrency-embedded sets support concurrent read and write operations by default, allowing multiple threads to perform insertions and deletions without locking while ensuring thread safety for all write operations. These sets employ more sophisticated synchronization strategies to prevent deadlocks, thus achieving better performance under concurrent access.

基于CopyOnWrite机制设计的一种特殊集合类:该类型通过底层动态数据结构实现快速的数据更新功能。对于此类对象而言,在进行读取运算时可直接引用自身数据而不需任何保护机制;而针对写入运算,则会触发底层系统自动复制当前状态并生成新的数据副本来进行处理。这种机制确保了在所有参与运算的操作者之间实现了完全的数据隔离性保护,并且避免了传统静态数组复制方式所带来的性能瓶颈问题

Map接口有哪些实现类?

常见的是HashMapLinkedHashMapTreeMapConcurrentHashMap这几种数据结构。建议在无需排序时优先选择HashMap作为最佳 Map 实现方案,在需要线程安全的情况下则选用 ConcurrentHashMap

涉及需要按照一定顺序进行排序的场景,在按照插入顺序进行排序时可采用LinkedHashMap结构,在根据键值进行排序时则可选用TreeMap结构。为了确保线程安全可用Collection类进行封装。

简述Map put的实现过程

HashMap是最经典的Map实现,根据他的视角介绍put

  1. 首先检查数组是否为空。如果是空数组,则执行一次扩展操作以增加空间。

  2. 利用哈希算法确定键值对在数组中的索引位置。

  3. 插入新数据时遵循以下逻辑:
    a. 若当前索引处无内容,则新增数据。
    b. 若当前索引已存在键值对,则直接更新对应的值。
    c. 若当前索引无键值对存在,则将新数据连接至链表尾部。
    d. 当链表长度达到预设阈值时(如8),需将链表转换为红黑树结构,并将其合并回主哈希表中。

  4. 再次扩容(如果元素个数超过threshold)

如何得到一个线程安全的Map?
  1. 采用Java Collections集合框架进行封装
  2. 在Java.util.concurrent包中提供Map结构
  3. 避选用Hashtable由于其性能较慢且无法支持null键值对(因为Hashtable不允许将null值作为键或值存入表中,在这种情况下将导致运行时异常),而推荐使用HashMap这种更适合存储null值的数据结构。
JDK7和JDK8中的HashMap有什么区别?

在JDK 7版本中,默认配置下,默认情况下的HashMap采用的是数组加链表的方式进行存储。当发生碰撞时(即两个不同的哈希码指向同一个索引位置),会导致新键值被附加到已存在的记录之后。由此可知,在极端情况下发生严重的碰撞会导致算法的时间复杂度急剧上升。

JDK8采用了数组+链表+红黑树的组合方式,在数据量超过8时会自动转换为红黑树结构,并显著提升了查找速度。

HashMap的底层原理

基于hash算法,通过put方法和get方法存储和获取对象。

当存储对象时:首先采用K/V->put方法进行操作,在具体实现中会调用K类对象的hashCode属性计算出对应的桶位置,并进一步通过equals()方法验证键值对的一致性。当遇到数据冲突的情况时(即哈希码冲突),本系统会根据当前数据量选择是否切换到红黑树结构来处理冲突元素,并以此提升查找效率

为了处理碰撞问题,在数组中存储的元素采用单向链表结构以实现高效的插入和删除操作。当链表长度达到某个临界点时,在特定条件下会转而采用红黑树数据结构以提升整体性能;而当计算复杂度评估降至另一个临界点时,则会转而恢复为单向链表结构以降低空间占用并简化操作流程

为什么HashMap线程不安全?

HashMap在并发执行put操作时,可能会存在出现循环引用的情况,并且最终会导致死锁问题

在多线程场景下进行HashMap规模重估时,在现有两个或多个线程同时发现需要调整其规模的情况下就会导致条件冲突的发生。这种情况下,在尝试同步修改数据结构时可能会出现意外操作交叉的情况。具体而言,在重新分配哈希桶空间的过程中,在将元素移动至新桶位置时(即从旧链表头部取出并附加至新链表尾部),为了避免旧链表尾部遍历带来的潜在问题(如内存泄漏等),系统会将这些被移动到新哈希桶中的元素依次插入到新链表的头部位置上以确保原有队列顺序能够得到正确维护。如果出现了上述条件冲突,则会导致系统的死锁问题无法得到及时解除而影响系统的正常运行效率。

HashMap使用红黑树不使用b/b+树的原因

b+树和b树常用于外部存储中,并被设计为对磁盘操作友好的数据结构。在数据量较小的情况下使用b/b+树时,其查找效率与链表几乎相同。

HashMap和ConcurrentHashMap有什么区别?

HashMap不是 thread-safe 的。在多线程操作时可能会导致数据一致性问题。还可能由于并发插入元素形成环导致查找时出现死循环。这会影响整个程序的运行。

该工具类可被设计为线程安全,并通过synchronized关键字确保线程间的互斥。其底层采用互斥锁机制运行,在这种情况下其性能及吞吐量相对较低。该数据结构实现较为复杂,并未采用全局锁定机制。然而,在具体实现中采用了分粒细度优化策略,并最大限度地降低了因竞争锁定而引发的阻塞与冲突。值得注意的是,在查询操作中无需锁定即可完成数据访问。

在JDK 1.8版本中,ConcurrentHashMap基于Node数组、链表以及红黑树的数据结构实现了其核心功能,并发控制采用了Synchronized和CAS机制来完成操作。该数据结构呈现出高效的线程安全特性。

对LinkedHashMap的理解

通过管理双向链表来维护键值对的顺序,在无需处理 HashMap 和 Hashtable 内部键值对排序的情况下,在插入操作时就可以保持键值对的有序排列。同时也可以避免引入 TreeMap 所带来的额外开销。链接 HashMap 需要维护元素的插入顺序,在性能上略低于 HashMap;不过由于链表结构使得迭代访问 Map 中全部元素时具有较好的性能表现。当一次性获取所有元素时具有较好的性能表现。

基于HashMap的实现方式,在实现过程中对数据结构进行了优化设计。具体而言,在 HashMap 的基础上增加了双向链表的支持以解决其无法始终保持遍历顺序与插入顺序同步的问题。该数据结构在核心功能上与普通的 HashMap 几乎一致,在维护数据结构的方式上进行了重写。

TreeMap的底层原理

采用红黑树作为基础数据结构的地图容器支持多种键值存储方式

Map与Set的区别?

Set是无序的、元素不可重复的集合

Map表示遵循键值对的映射关系的集合体。该数据结构中全部的键属于一个Set类型,并且这些键不考虑顺序且不允许有重复。

List和Set的区别?

Set代表无序元素不可重复的集合;List代表有序元素可以重复的集合。

ArrayList和LinkedList的区别?

从数据结构角度来看, ArrayList采用数组作为其基础存储结构,而 LinkedList 则依赖于双向链表. 在随机访问操作方面, 由于数组结构允许常数时间复杂度的操作 (O(1)), 因此, 在随机访问效率上 ArrayList 表现优于 LinkedList. 相比之下, 在插入与删除操作上, 则呈现出相反的趋势. 从内存占用角度来看, 在相同数据规模下, 由于双向链表中每个节点包含多个指针字段 (如 prev 和 next), 因此, 在内存占用方面 LinkList 的表现会略逊于 ArrayList.

线程安全的List

Vector:古老,线程安全,但是效率低

SynchronizedList相较于Vector具有更好的扩展性和兼容性;每个方法均带有同步锁;并非最佳的选择。

CopyOnWriteArrayList:基于复制底层数组的操作方式实现。所有读取操作均直接作用于集合本身。所有加锁操作均为无序地施加并发生阻塞。因为所有集合的写入操作均针对数组副本执行,因而具有良好的线程安全性。其性能表现最佳。

CopyOnWriteArrayList:基于复制底层数组的操作方式实现。所有读取操作均直接作用于集合本身。所有加锁操作均为无序地施加并发生阻塞。因为所有集合的写入操作均针对数组副本执行,因而具有良好的线程安全性。其性能表现最佳。

CopyOnWriteArrayList的原理

CopyOnWriteArrayList是Java提供的核心并发类之一。具体来说,在该类中实现的是线程安全且保障读操作无需锁的数据结构功能。当进行写操作时会创建一份新列表,在新列表上执行修改,并将原有引用更新为新列表的引用。这种机制确保了所有相关线程的操作同步。

TreeSet和HashSet的区别

HashSet、TreeSet的元素都是不能重复的,并且他们都是线程不安全的。

HashSet允许null值的存在而TreeSet不允许;然而,在元素排列顺序上HashSet不具备保证性而TreeSet则能够支持遵循自然顺序排列以及自定义排序方式。

HashSet底层采用哈希表实现,TreeSet底层采用红黑树实现。

HashSet的核心机制是以HashMap为基础实现的。其默认构造函数会设置初始容量为16,并使用负载因子0.75的HashMap。

Stream(不是IOStream)有哪些方法?

Stream包含了丰富的功能用于实现聚集操作,并且这些功能既可以在中间阶段执行相关操作,也可以在最终阶段完成相应的处理流程。

  • 中间操作:这种操作能够支持流维持开启状态的同时也能够直接调用后续的方法。在上述程序中可以看到map()函数属于中间操作的一种表现形式。
  • 末端操作:这是一种对流进行最终处理的方式,在对某个Stream执行时会触发这种方法的操作流程。
    当对该程序中的某个Stream执行完所有的中间步骤后,
    在完成所有这些中间步骤之后,
    当完成所有的这些步骤之后,
    完成所有的这些步骤之后,
    在完成所有这些步骤之后,
    都会触发该Flow的结束阶段的操作流程。

除此之外,关于流的方法还有如下两个特征:

  • 有状态的方法:这种技术会增添流中的几个关键属性(包括元素的唯一性、最大容量以及按顺序处理这些数据)。通常情况下这些方法会消耗较多的计算资源。
  • 短路方法(short-circuiting method):短路方法无需遍历全部的数据项。

下面简单介绍一下Stream常用的中间方法:

  • filter(Predicate predicate):通过指定Predicate筛选出符合条件的流元素。
  • mapToXxx(ToXxxFunction mapper):将流中的每个元素依次通过指定函数进行一对一转换,并返回新流。
  • peek(Consumer action):逐个处理每个元素执行特定操作,并返回与原流相同内容的新流。此方法主要用于调试信息。
  • distinct():根据equals()方法判断两个元素是否相等后去除重复项。该方法为状态ful操作。
  • sorted():保证后续访问时按照顺序排列数据,并返回与原流相同内容的新流。该方法为状态ful操作。
  • limit(long maxSize):限定后续访问的最大处理数量,并返回与原流相同内容的新流。该方法为状态ful且短路操作。

下面简单介绍一下Stream常用的末端方法:

按照顺序执行操作(Consumer action):按照顺序依次访问并执行每个流中的元素操作。
转换为数组(toArray()):将流中的所有数据项转换为对应的数组结构。
提供三种功能实现方式(reduce()):提供了三种功能实现方式以实现对多个数据项进行特定操作后合并的结果。
最小值(min()):用于获取数据序列中的最小数值结果。
最大值(max()):用于获取数据序列中的最大数值结果。
计数器(count()):用于统计并返回数据序列中的数据项数量结果。
满足条件检测(anyMatch(Predicate predicate)):判断数据序列中是否存在至少一个满足指定条件的数据项结果。
全否匹配(noneMatch(Predicate predicate)):判断数据序列中是否存在全部不满足指定条件的数据项结果。
首项获取(findFirst()):用于获取并返回数据序列的第一个数据项结果。
任意项获取(findAny()):用于在数据序列中找到并返回任意一个符合条件的数据项结果。

Java 8还为所有流式API相应地提供了相应的Builder类构建工具。例如用于生成Int64位整数流的IntStream.Builder、用于生成长整型数值流的LongStream.Builder等工具类构建工具可被开发者使用以生成相应的流。

独立使用Stream的步骤如下:

通过使用Stream或XxxStream中的builder()方法来生成该类型的Builder对象。
继续不断地调用Builder对象上的add()方法,在该流中加入多个元素。
通过调用Builder对象上的build()方法来获取相应的Stream实例。
Stream提供了一系列用于执行聚合操作的方法。

简述Java中的IO流

IO常被用来表示数据的输入输出过程,在Java中将不同类型的输入/输出源统一抽象为一种称为流(Stream)的对象。这些流是由其来源到接收端的有序数据构成,在此过程中使得程序能够以统一的方式访问各种不同的输入/输出源。

  • 基于File类型的流用于实现文件操作;
  • 由ByteArray或CharArray表示的流用于访问内存中的数组;
  • 通过Piped连接的管道流用于实现进程间通信;
  • 基于String类型的流用于访问内存中的字符串;
  • Buffered类型的缓冲_flow在读写数据时进行数据缓存操作;
  • 输入/输出转换器(InputStreamWriter)将字节流量转化为字符流量;
  • 对象导向的数据传输工具(Object-oriented streams)支持对象序列化操作;
  • 打印器相关的数据输出工具(Print-based streams)简化打印操作流程;
  • 通过Pushback机制的数据输入工具能够将已读取的数据重放至缓冲区以便再次读取;
  • 数据导向型数据传输设施(Data-oriented flows)专门处理Java基本类型的数据。
怎么用流打开一个大文件?

为了避免直接将文件中的数据都读入内存中,可以采用多次读取的方式

  • 使用缓冲机制来优化数据传输过程,在每次操作时都会缓存相关数据以减少设备端的操作频率。
  • NIO技术通过内存映射技术管理文件内容并实现高效的数据输入输出操作,在程序运行过程中将文件及其相关数据段直接加载至内存空间中进行处理,并通过对内存空间直接进行操作来模拟文件操作流程。
NIO的实现原理

NIO主要由三个核心部分组成:Channel、Buffer、Selector

线程-》Selector-》多个Channel<=>(互相写入)Buffer(capacity,position,limit)

Java的序列化与反序列化

该机制允许将目标数据编码为二进制形式进行存储或传输,在数据恢复过程中可采用类似的技术实现信息的有效传递。对于像的编码过程则通常指将Java对象写入一个IO流中进行持久化存储或传输;而对像的解码操作则指从该IO流中恢复原始的对象。

进行序列化编码:类的对象应遵循Serializable接口的要求,并通过将该对象输出到一个 ObjectInputStream 实例中并调用 write(Object) 方法进行编码操作;同时读取该数据到另一个目标对象时所使用的读取操作则需调用 ObjectOutputStream 的 read(Object) 方法。

Serializable接口为什么需要定义serialVersionUID变量?

代表了序列化的版本,在操作之前需要确定版本是否一致。

除了Java自带的序列化之外,还了解哪些序列化工具?

JSON:目前使用频繁的格式化数据工具,简单直观,可读性好,有fastjson

protohuf、Thrift、Avro

创建线程的方式有几种?

三种方式

  1. 继承自Thread类。

  2. 定义其子类并重写了run()方法作为线程执行体实现。

  3. 创建实例用于表示 Thread 对象。

  4. 启动了该线程。

  5. 为Runnable接口创建实现类

  6. 定义一个为Runnable接口提供实现的方法,并确保其实现类能够完成其run()方法的功能

  7. 生成一个实例,并将它指定为Thread的目标以创建thread对象

  8. 然后通过调用其start()方法启动线程

  9. 实现Callable接口

  10. 创建Callable接口的实现类,并实现其call()方法

  11. 将该Callable对象封装到Future Task类中

  12. 将该Future Task对象指定为Thread目标,并启动新线程

  13. 通过调用该Future Task对象的get()方法获取子线程执行完成后返回的结果

Thread类的常用方法

常用的构造方法包括 Thread()、Thread(String name)、Thread(Runnable target) 以及 Thread(Runnable target, String name),这些方法分别具有 0 个、1 个和 2 个参数。其中参数包括线程名和目标对象。

常见的静态方法包括以下几种:获取当前运行的线程的方法有currentThread();判断当前运行的任务是否被中断的方法为interruited();让当前任务暂停指定毫秒的方法是sleep(long millis);而yield()则可以让当前任务暂时释放处理器以便其他任务使用。

常见的实例方法为:get+(id,name,priority),用于获取该进程的相关信息;而interrupt+(),则用于导致进程被中断。

run()和start()有什么区别

run方法被称作是负责实现核心功能模块的线程执行体,在该类别的操作中具有关键作用。通过调用该类别的start()方法可以启动相应的线程

如果一个开发者不经过任何包装层的访问直接触达对象的run()方法,则该系统将视为该线程对象为普通的对象实体,并将该run()方法以普通方法的身份运行而不将其视为线程执行体。

线程是否可以重复启动会有什么后果?

仅限于那些处于新建状态的线程才能执行启动操作;如果启用了该方法,则会触发非法新线程状态异常;然而,在启用了该方法后...其活动方式则由JVM中的线程调度器决定。

介绍一下线程的生命周期?

(new)新建->(start)就绪->运行(run)->阻塞->死亡

image-20211207110927700
如何实现进程同步?

同步方法:synchronized关键字修饰

使用synchronized关键字修饰的语句段,在编译时会被系统自动加装内置锁以确保数据一致性,并通过互斥机制实现安全的多线程访问

ReentrantLock具有与现有方案相同的基本功能,并且增强了支持公平锁机制的能力(例如允许实现公平锁的构造函数以减少运行效率)

volatile关键字:实现了对域变量访问的一种无锁机制,在编程中使用volatile相当于让虚拟机会意识到该域可能由其他进程进行更新;每次调用时都需要重新计算该域的实际值而非依赖寄存器中的数据

原子变量:java.util.concurrent.atomic provides a convenient way to create atomic variables, which offer a collection of utility classes that ease thread synchronization tasks.

Java多线程之间的通信方式

wait( ) 、 notify( ) 、 notifyAll( )(这些方法的前提条件是线程之间需通过 synchronized 保证线程安全),这三个方法均为本地实现且被 final 标记为无法重写。其中 wait( ) 方法可以让进程暂时释放对象锁并进入阻塞状态;而 notify( ) 则可用于唤醒一个正在等待相应对象锁的线程使其转为就绪状态; notifyAll( ) 则可同时唤醒所有仍处于等待相应对象锁的状态下的进程并使它们均转为就绪状态。

每个锁对象都设置了两个相关联的队列:一个用于分配给试图争夺锁的任务(称为就绪队列),另一个用于分配给处于等待状态的任务(称为阻塞队列)。只有在相关进程被唤醒之后才会将该进程转移至就绪状态,并加入到等待调度的任务列表中。相反地,在线程被blocking之后就会立即加入到阻塞任务列表中,并等待相应的资源释放或信号触发后才能重新返回到就绪状态。

这些method(其使用前提是对进程间的通信使用Lock以确保线程安全)都位于Condition接口中,并且相较于传统的wait+notify机制,在安全性与效率方面(await mechanism)更为卓越。

BlockingQueue

Java5实现了阻塞队列接口。尽管它是Queue类的一个子类,其主要功能并非作为数据容器使用。相反地,在程序设计中这一接口主要用于实现进程间的通信机制。具体而言,在生产者进程中尝试将元素放入BlockingQueue时(若 BlockingQueue已满),相关生产者进程会被阻塞;而在消费者进程中试图从BlockingQueue中获取元素时(若 BlockingQueue为空),相关消费进程同样会被阻塞。这种机制通过生产者与消费者之间的取入与取出操作实现了对进程间通信的有效控制。

sleep()和wait()的区别

sleep( ) 是 Thread 类提供的静态方法,而 wait( ) 则是 Object 类中的成员函数; sleep( ) 可以在任意上下文中调用,但 wait( ) 只能在同步区域中被调用; 调用 sleep( ) 方法不会释放当前锁,而调用 wait( ) 方法则会释放当前锁,并通过 notify 机制重新获取锁。

如何让子线程先执行,主进程再执行

一旦启动了子线程,在后续步骤中调用该子线程的join()方法,则会导致主线程序必须等待该子线程执行完毕之后才能继续执行。

阻塞进程的方式
  • 通过调用sleep()方法主动释放处理资源
  • 阻塞式IO方法被调用时,在返回前会进入等待响应的状态
  • 希望获取一个同步监视器但因当前同步监视器已被占用而无法实现
  • 线程处于等待某个特定事件(notify)的状态中
  • 程序应尽量避免使用suspend()以防止因挂起线程而导致的死锁风险
Synchronized和Lock的区别

Synchronized作为Java关键字,在JVM层次上实现了加锁与解锁的功能;Lock是一个接口类,在代码层面上提供了实现加锁与解锁功能的机制。

Synchronized可以加在代码块,方法上面;Lock只能写在代码中

S会在程序正常结束或发生错误时自动完成对锁的解锁操作;对于Lock而言,则不自行完成对自身的解锁操作,则需在Finalize阶段手动完成相应的操作。

S会导致线程拿不到锁一直等待,Lock可以设置获取锁失败的超时时间

S不知道是否获取锁成功,Lock可以调用tryLock得知加锁是否成功

S锁支持重入与不支持中断同步运行,并采用非抢占式分配策略;Lock锁则支持重入与中断同步运行,并能够根据需要选择支持抢占与非抢占两种模式的组合方式;通过细 grain locking机制实现资源利用率的提升

Synchronized的底层实现原理
复制代码
    public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
    }
img

通常情况下,在代码块运行时

monitorenter:

每个对象都充当一个 watchdog agent,并一旦被占用即进入锁定状态。在执行 entering the monitor process 时会尝试获取 monitor 的所有权。

当 monitor 的进入计数器为零时,则该线程会自动切换至 monitor 状态,并将 enter counter 设置为 1 次;此时这条线程便成为该 monitor 的拥有者。如果某条线程已经对该 monitor 实行了占用,则重新切换至 monitor 状态时会自动增加 enter counter;其他尚未占用此 monitor 的线程会在 enter counter 降回到零时自动阻塞等待;再次尝试切换至监控状态后即可重新获得对该 monitor 的所有权

monitorexit:

必须由objectref对应的monitor持有者负责执行monitorexit指令。
当指令执行完毕后, monitor的进入次数减1, 若此时entercount降为零, 则表示该线程退出了 monitor, 以后将不再属于该monitor的所有者。
在此情况下, 其他被该monitor阻塞无法运行的线程仍有机会重新获取该monitor的所有权。

monitorexit指令出现两次,在第一次运行中实现了同步正常退出并解耦了锁;在第二次运行中则采用了非同步方式完成了对锁的释放操作。

复制代码
    public class SynchronizedMethod {

    public synchronized void method() {
        System.out.println("Hello World!");
    }
    }
img

通过逆编译分析得知:同步过程并未借助monitorenter和monitorexit指令完成;然而,在常规方法的基础上新增了ACC_SYNCHRONIZED这一标识符。JVM机制正是基于该标识符来实现同步功能的:

在方法被调用时,在进入该函数之前,“该指令会验证 ACC_SYNCHRONIZED 访问标志是否被设置。”如果这个访问标志被设置为 true,则执行线程将先尝试获取 monitor 对象;只有在成功获取后才能继续执行函数体;函数体完成后会立即释放该 monitor 对象。在函数体运行过程中,“其他任何线程在该方法执行期间均无法再次获得该 same monitor 对象。”

三、总结

在功能上两种同步机制并无明显差异;然而其中一种同步机制采用的是隐式的实现方式,并不需要借助字节码机制即可完成操作。在JVM中执行这两个指令的过程是基于操作系统提供的互斥原语mutex机制完成的,在被阻塞的线程会被挂起、等待重新调度的过程中会经历从用户态到内核态再到用户态的状态转换循环,在这一过程中系统会在两者之间来回切换状态从而导致一定的性能损失

synchronized可以修饰静态方法和静态代码块吗?

S可以修饰静态方法,不能修饰静态代码块

当为静态方法进行修饰时,监视器锁定机制是其所属类的实例;而静态方法所使用的锁则等同于该类的整体锁定机制。

谈一谈ReetrantLock的实现原理

首先主要基于AQS实现这一技术框架,并且其中AQS代表AbstractQueuedSynchronizer这一缩写。该框架内部实现了两个关键类型的抽象类:一个是同步队列另一个是条件队列。具体而言,同步队列采用双向链表结构来存储所有处于等待状态的所有线程;而条件队列则采用单向链表来存储所有处于等待状态的线程。值得注意的是,在这些等待状态下的线程在被唤醒时会根据各自的逻辑安排被添加到相应的同步或条件队列中,并最终由相应的管理机制完成其等待与唤醒的工作流程。

在同步队列中存在两种模式:独占模式与共享模式。它们之间的区别在于AQS在唤醒线程节点时是否传递唤醒信号。这些模式各自对应独占锁与共享锁。

在我们的开发中,AQS被定义为一个抽象类,在这种情况下无法直接创建实例对象。当我们在为自定义锁实现一些特殊功能时,则需要考虑以下几点:我们可以继承AQS,并在此基础上重新定义获取和释放锁的方法,并且同时需要对状态进行有效的管理。

Reentrantlock实现了对Lock接口的支持,并包含三个内部类。其中Sync内部类遵循AQS协议接口设计,在此基础上又新增了两个统一的 Sync 接口实现类。这两个新实现分别用于实现公平锁与非公平锁的功能。通过重写tryAcquire和tryRelease方法即可了解 ReentrantLock 实现的具体工作原理——即基于 AQS 的独占锁定机制(locking),这正是我们常说的悲观锁特性。

如果不使用Synchronized和Lock如何保证线程安全?

volatile的关键字实现了对字段访问的一种无锁机制,在虚拟机中对带有的字段进行修改时会触发相关操作以防止数据被其他线程同时修改;当读取这些字段时系统会自动重新计算其值以避免依赖于寄存器中的旧值

原子变量:该工具类通过java.concurrent.atomic包提供的功能实现对原子类型变量的支持,并且能够使得线程同步操作更加便捷。

本段代码主要通过使用...标签来实现对本地变量进行保护性访问,并通过引入一个基于键值对的映射机制来管理这些变量。具体而言:每个线程对象都包含一个ThreadLocalMap实例(该对象存储了一系列键值对),其键基于 ThreadLocal.threadLocalHashCode 计算得出而不会发生冲突(即同一个 Thread 对象的所有本地变量均会被映射到同一个 ThreadLocalMap 实例中)。

在Java语言中,默认情况下多线程共享的基本数据类型的引用是不可变的特性,在定义时若在其声明中附加final关键字即可实现这一特性。然而当共享的数据涉及对象时,则必须依靠被引用对象自身的行为来确保不会对其状态造成任何影响这一前提条件才能实现类似效果

说一说Java中乐观锁和悲观锁的区别?

悲观锁是一种基于最坏情况假设的数据访问机制,在Java编程语言中常用于控制多线程环境中的同步问题。其工作原理是在获取数据库资源前始终认为他人可能对该资源进行修改,并因此在获取该资源的过程中都会被锁定;只有当持有该资源的锁时才不会被阻塞;而这种机制在Java中通常通过synchronized关键字或自定义Lock对象来实现。

乐观锁:在获取数据的过程中假设其他人不会进行修改操作而无需进行加锁操作,并且在执行更新操作时会检查此时间段内是否有其他人在进行相应的修改操作以避免冲突。常用于单机环境下的多读场景并能有效提高吞吐量。

公平锁和非公平锁是怎么实现的?

一种方法是通过Java内置的关键字synchronized来对相应的类、方法或代码块施加锁定机制。另一种则是采用ReentrantLock机制。前者仅限于非公平锁类型,而后者则是一种默认采用非公平但可实现公平的锁定机制。

了解过Java里面的锁升级嘛?

在JDK1.6版本之前,synchronized始终是一个重量级的同步机制,其效能较低。然而在JDK1.6版本之后,JVM为提升同步操作的效率而对synchronized进行了优化改进措施,引入了偏向锁以及轻量级的锁定机制以替代原有的同步操作方式。随着时间的发展,系统中的同步机制已发展出四个不同的状态:无锁状态、偏向锁、轻量级锁定机制以及重量级锁定机制这四种状态会根据竞争情况逐步升级以适应系统负载的变化

如何实现互斥锁(mutex)?

在Java中,默认实现互斥同步的主要手段是synchronized关键字(S),它是一种基于(Block Structured)机制的同步语法体系.S经Javac进行编译后会生成两个字节码指令.这两个指令均需以一个reference类型参数指定对象进行解锁与锁存.

从 JDK 5 开始,Java 标准类库中新增加入了名为 J.U.C 包的组件库(也被称为 JUC 包),其中 Lock 接口实现了全新的互斥同步机制。该接口允许开发者采用非块状结构的方式来实现互斥同步功能。通过该接口技术可采用非块状结构来实现互斥同步,并避免受到不同编程语言特性的影响。

分段锁是怎么实现的?

在处理多个任务时,采用线性顺序的操作会削弱系统的扩展能力,并且频繁地进行上下文切换同样会降低系统性能。当多个线程同时试图对同一资源进行修改时会产生竞争现象并引发相关问题。采用互斥锁本质上就是采用串行的方式仅允许一个线程进行访问而无法实现真正的多线程并行。对于可伸缩性而言独占锁就是对系统可扩展性构成最大威胁的机制。

我们有三种方式降低锁的竞争程度:

  1. 缩短锁保持的时间
  2. 控制在合理范围内的锁请求次数
  3. 采用具备协调功能的排他锁机制

在某些特定场景下,我们可以将锁分解技术进一步拓展,使其能够应用于一组独立对象上的分段lock分解,这种技术被称为分段式lock机制.实际上来说,这意味着在一个容器内部可能同时存在多把互斥 lock,每把 lock专门保护容器中的一部分数据.这样一来,当多个线程同时对容器中的不同数据段执行读操作时,各 lock之间就不会产生竞争关系,从而能够有效地提升并发处理效率.这是ConcurrentHashMap所采用的核心技术原理:首先将整个数据集划分为若干个独立的存储单元,然后对每个存储单元配上一把互斥 lock.一旦某一个线程通过获取对应的 lock来对某一存储单元的数据进行读写操作时,其他存储单元中的数据仍然能够被其他无冲突的线程安全地读取

说一说对于读写锁的了解

与传统锁的主要区别在于其规定:允许多个节点进行读操作的同时只能有一个节点进行写操作。在大多数实际应用场景中, 读取操作远超 writes. 为此而设计的一种机制

谈一谈对于JUC的理解

JUC是java.util.concurrent的缩写,包含了一些并发编程的基础组件。

原子更新、锁与条件变量、线程池、阻塞队列、并发容器、同步器

介绍下ThreadLocal和他的应用场景

ThreadLocal本质上是一个为线程提供私有局部变量存储服务的容器。它主要用于保护多态编程中的对象状态信息,在Java虚拟机中扮演着重要的角色。实际上它并不是真正意义上的数据存储容器而是通过映射机制来实现对内存区域的有效访问与管理。每个类都有自己的ThreadLocal实例用于管理与该类相关的对象实例信息包括引用计数器等元数据以及这些对象实例在内存中的位置信息等。

该方法的主要应用场景是:将每一个线程与一个JDBC连接绑定,在这种情况下就可以确保每一个线程都能独立地在自己的Connection上执行数据库操作,在不影响其他进程的同时实现资源的有效管理

介绍一下线程池

系统重新启动一个线程会产生高昂的成本(必须进行与操作系统之间的交互)。特别是在程序频繁创建大量短暂生存期的线程时,在这种情况下(即当这种情况发生时),通过采用线程池策略能够显著提升系统的运行效率。

与数据库连接池类似的是,在线程池一启动时就创建了大量空闲的线程。程序接受一个可运行的对象并将其传递给线程池,在这种情况下由线程池驱动启动一个空闲进程来执行其run()或call()方法。当上述方法执行完毕后,在这里不会立即释放并返回这些对象到外部环境;相反地,在完成当前任务后会在线程池中等待下一个Runnable对象去进一步处理这两个操作

Java5引入了一个新的Executors工具包用于生成线程池,并提供多个静态工厂方法以创建这些线程池;这些生成的线程池都由ThreadPoolExecutor这一特定类负责实现。

介绍一下线程池的工作流程

发起提交请求→检查核心线程池是否达到饱和状态→评估工作队列的负载情况→确认线程池资源是否已被占用→采用饱和度优先策略来处理当前queued的任务

处于运行状态时能够接收新的请求。
当系统进入关闭状态后将不再处理新请求。
终止作业以停止运行。
执行完所有工作流程后退出系统。

JVM包含几部分

类加载器:负责将class文件加载至运行时数据区执行引擎:负责将字节码传递给操作系统本地库接口:为Java提供所需的不同语言接口

JVM是如何运行的

配置为Java虚拟机(JVM)运行环境并识别相应的Java运行时环境(JRE),然后安装于Java虚拟机上进行初始化以获取本地调用接口,并执行Java应用程序

没有程序计数器会怎么样?

缺少程序计数器单元的Java程序,在流程控制功能上将无法实现有效的管理与协调。此外,在多线程处理方面同样无法正确地轮流切换状态以保证系统的稳定运行。

Java的内存分布情况?

JVM在执行Java程序时负责划分内存空间为若干个不同的数据区域。

程序计数器:当前线程所执行的字节码的行号指示器

Java堆:存放对象实例

类存放在哪?

方法区和Java堆也同样是各个线程共享的内存区域,并且都用于存储已由虚拟机加载的类型信息、常量、静态变量以及即时编译器编译生成后缓存的代码数据等信息。

对象的实例化过程

该字节码文件中与执行类相关联的构造函数对应于()方法。该方法包含非静态变量、非静态代码块及其相关联的构造器。

()方法可以重载多个,类有几个构造器就有几个()方法;

执行顺序为:静态变量-》静态代码块-》变量-》代码块-》构造器

类加载方式,双亲委派机制?
介绍一下Java的垃圾回收机制

如何定义垃圾?

  • 引用计数法:在涉及对象互相引用的情形下(即一个对象A被另一个对象B所引用来表示),系统会将A的对象实例数目进行统计(即A的对象实例数目加一)。相反地,在某些情况下(即当一个对象失效不再被其他对象所引用来表示),则会减少该实例数目(即该实例数目减一)。这种看似简单的机制实际上涉及到许多复杂的情况处理(例如如何处理相互之间存在循环引用的对象)。

  • 可达性分析算法:“GC roots”的根节点集合基于某种特定规则选取(即选择能够代表整个系统关键运行状态的核心节点),随后沿着系统中的实际运行关系逐步深入探索(即沿着实际存在的数据流或指针关系一步步深入)。在遍历过程中所经过的路径定义为“引用链”(即记录了各节点之间的关联关系)。若某对象与GCroots之间不存在任何一条完整的引用来连接,则该象将无法继续使用。

可以作为GCroots的对象有几种?

虚拟机栈中的引用对象包括线程调用参数、临时变量等。
在方法区内涉及Java类的引用类型主要是静态变量。
常量在方法区中的引用对象也是需要考虑的因素。
例如Native方法所涉及的引用对象。
同步锁所持有的对象通常是关键资源。
用于反映虚拟机内部状态的JMXBean及其本地代码缓存等。

怎么回收垃圾?

  • 分代收集
    • 标记清除:完成标记后将所有相关数据彻底清除
    • 标记复制:该过程将可用内存划分为容量相等的两部分,在每次操作中仅使用其中一块内存空间。当当前内存块耗尽后,则将尚未处理的对象依次迁移至另一块空闲内存区域,并将已无作用的内存空间范围内的数据进行释放
    • 标记整理:此阶段与"标记-清除"算法步骤一致,在完成对象标记后不再对回收对象进行一次性清理。而是通过连续迁移所有存活对象至内存的一端区域,并直接释放超出范围的部分以实现整体回收管理
5.29 内存泄漏和内存溢出有什么区别?

内存泄漏(memory leak):该术语指程序运行过程中临时变量被分配了未被垃圾回收机制回收的内存空间。这些未释放的资源会持续占用系统资源而不为其他进程所使用。尽管无法利用这些资源也无法将它们重新释放回系统供其他应用使用从而导致了系统性能问题。

内存溢出(out of memory):直观地讲, 内存溢出是指程序运行过程中申请的内存量超过了系统提供的可用内存量空间, 从而导致发生内存量不足而引发的溢出会引发问题.

GC roots 作为起始节点集,在通过引用关系向下探索时所经过的路径被视为"引用链"。若某对象与 GC roots 之间没有未被探索的引用路径,则该对象无法继续使用。

可以作为GCroots的对象有几种?

  • 虚拟机栈中的引用对象包括各线程调用参数以及临时变量等。

  • 方法区中的Java类静态引用类型的变量。

  • 常量通过特定机制被引用。

  • (例如Native方法)会涉及特定类型的引用操作。

  • 同步机制确保持有对象的安全性。

  • JMXBean以及本地代码缓存等是反映虚拟机运行状态的关键组件。

怎么回收垃圾?

  • 按代收集
  • 标记清除操作:完成所有标记后将统一进行清理操作
  • 标记复制:该系统将可分配内存空间按容量划分为大小相等的两块区域,并采用轮流使用的方式进行资源分配。当当前内存区域耗尽时(即当前使用的那一块已经用满),存活的对象会被复制到另一个内存区域中以供后续处理;随后会一次性释放已不再需要的空间。
  • 标记整理:其核心流程与'标记-清除'算法相同;但其后续清理策略不同于传统方法;而是通过将存活对象向内存空间一端集中排列;从而实现对剩余可用空间的有效利用
5.29 内存泄漏和内存溢出有什么区别?

内存泄漏(memory leak):内存泄漏被称为程序运行过程中的资源浪费现象,在为临时变量分配了未回收的存储空间后便长期占存,并且无法释放给其他程序使用而导致的问题就是内存泄漏。

OM(out of memory):明确地讲OM指的是程序运行期间动态申请的内存空间超过了计算机系统提供的可用内存空间这一情况,在这种情况下程序无法获得足够的连续内存空间从而导致系统出现资源紧张的情况

全部评论 (0)

还没有任何评论哟~