java常见面试题
一、什么情况下会把方法区内存撑爆
1、加载大量类:
动态生成类:使用像 Java 的 java.lang.reflect.Proxy
或者第三方库如 CGLIB、Javassist 来动态生成大量的代理类。每个新生成的类都会占用方法区的一部分内存。
加载外部类文件:通过自定义 ClassLoader
加载大量的类文件。每次加载一个新的类文件时,它都会被加载到方法区中。
2、填充运行时常量池:
字符串常量:向运行时常量池中添加大量的字符串常量。可以使用 String.intern()
方法将字符串添加到常量池中。注意,从 Java 7 开始,字符串常量池已经移到了堆内存中,但这仍然会影响方法区的大小。
3、使用大数量的注解:
元注解:创建大量的带有复杂注解的类或方法。注解信息也会存储在方法区中。虽然这种方式的效果可能不如前两种明显,但在某些情况下也可能会增加方法区的使用。
4、设置较小的方法区大小:
调整 JVM 参数:通过设置较小的 -XX:MaxMetaspaceSize
(Java 8 及以后版本)或 -XX:MaxPermSize
(Java 7 及以前版本),你可以限制方法区的最大可用内存。这样可以更快地达到方法区满的状态。
二、常见的垃圾回收机制
1、引用计数法
原理:引用计数法是一种简单的垃圾回收算法。在这种算法中,每个对象都有一个引用计数器。当对象被创建时,引用计数器初始化为1;当有一个新的引用指向该对象时,计数器加1;当引用离开作用域或者被设置为null时,计数器减1。当引用计数器的值为0时,表示该对象不再被引用,可以被回收。
优点:实现简单,判定效率高,回收没有延迟性。
缺点:无法处理循环引用的情况。例如,对象A引用对象B,对象B又引用对象A,此时它们的引用计数都不为0,但实际上这两个对象已经无法被外界访问,应该被回收。
2、标记 - 清除算法(Mark - Sweep)
原理:标记 - 清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,从根对象(如栈中的变量、静态变量等)开始,通过可达性分析算法标记所有从根对象开始能够被访问到的对象;在清除阶段,回收未被标记的对象占用的内存空间。
优点:可以解决循环引用的问题。
缺点:标记和清除的效率都不高。标记阶段需要遍历所有对象,清除阶段会产生大量不连续的内存碎片,当需要分配较大对象时,可能会因为没有足够的连续内存而触发垃圾回收,降低程序性能。
3、复制算法(Copying)
原理:将内存分为大小相等的两块,每次只使用其中一块。在垃圾回收时,将存活的对象从使用的这块内存复制到另一块空闲的内存中,然后将使用过的这块内存全部清空。
优点:实现简单,运行高效,不会产生内存碎片。
缺点:可用内存被压缩到原来的一半,空间利用率低。
4、标记 - 整理算法(Mark - Compact)
原理:标记 - 整理算法也是分为标记和整理两个阶段。标记阶段与标记 - 清除算法类似,通过可达性分析标记出存活的对象;在整理阶段,将存活的对象向一端移动,然后直接清理掉端边界以外的内存。
优点:解决了标记 - 清除算法产生内存碎片的问题,又不需要像复制算法那样浪费一半的内存空间。
缺点: 整理阶段需要移动大量存活对象,效率相对较低,如果对象存活率较高,移动的成本就会很高。
5、分代收集算法(Generational Collection)
原理:根据对象存活周期的不同将堆内存划分为不同的区域,一般分为新生代(Young Generation)和老年代(Old Generation)。新生代中的对象通常存活率较低,采用复制算法进行垃圾回收;老年代中的对象存活率较高,通常采用标记 - 清除或者标记 - 整理算法进行垃圾回收。
优点:针对不同代的对象特点采用不同的回收算法,提高了垃圾回收的效率。
缺点: 算法较为复杂,需要考虑不同代之间的交互和对象晋升等问题。
三、JVM强弱引用的区别
强引用:
强引用是最常见的普通对象引用,只要还有强引用指向对象,对象就存活,垃圾回收器不会处理存活对象。一般把一个对象赋给一个引用变量,这个引用变量就是强引用。当一个对象被强引用变量所引用,它就处于可达状态,是不会被垃圾回收的,即使之后都不会再用到了,也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
软引用:
软引用是一种相对强引用弱化了一些的引用,用java.lang.ref.SoftReference实现,可以让对象豁免一些垃圾收集。当系统内存充足的时候,不会被回收;当系统内存不足的时候,会被回收。
弱引用:
弱引用需要用java.lang.ref.WeakReference实现,它比软引用的生存期更短,对于弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否够,都会回收该对象的占用内存。
虚引用:
虚引用要通过java.lang.ref.PhantomReference类来实现,虚引用不会决定对象的生命周期,如果一个对象只有虚引用,就相当于没有引用,在任何时候都可能会被垃圾回收器回收。它不能单独使用也不能访问对象,虚引用必须和引用队列联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。
四、怎么判断一个对象是否达到回收标准
在Java中,对象的回收主要依赖于垃圾回收器(Garbage Collector,GC)。垃圾回收器的工作原理是跟踪并管理内存中的所有对象,当对象不再被引用时,垃圾回收器会自动将其回收。
判断一个对象是否可以被回收,主要看这个对象是否还存在于引用链中。如果一个对象的所有引用都被清除,那么这个对象就无法再通过任何路径被访问到,这个对象就可以被垃圾回收器回收。
具体来说,垃圾回收器会遍历所有的引用链,找到那些还在引用链中的对象,然后把这些对象标记为活跃对象。对于那些没有被标记为活跃对象的对象,垃圾回收器就会将其回收。
需要注意的是,垃圾回收并不是即时发生的,而是由垃圾回收器在后台自动进行的。因此,我们不能直接判断一个对象是否可以被回收,而只能通过垃圾回收器来完成这个任务。
五、双亲委派和类加载机制
类加载机制:
在Java的世界里,每一个类或者接口,在经历编译器后,都会生成一个个.class文件。
类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。
类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。
除了使用和卸载两个过程,前面的5个阶段 加载、验证、准备、解析、初始化 的执行过程,就是类的加载过程。
双亲委派机制:
双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。
这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。
这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。
六、一个class类的运行流程
一个Java类从编写到运行大致要经历这么几个过程:编译(前端编译生成class文件)、加载、验证、准备、解析、初始化、使用、卸载
七、Stream流常见的作用
- 简化遍历和操作:传统的集合操作通常需要使用for循环,而Stream流通过函数式编程的方式,可以简化遍历、过滤、映射等操作。
- 支持链式调用:Stream流的操作分为中间操作和终端操作。中间操作返回一个新的Stream对象,可以进行链式调用;终端操作返回一个具体的结果,不再返回Stream对象。
- 提高代码可读性:Stream流结合Lambda表达式,使得代码更加简洁和易读。
过滤:
.filter(Predicate<T> predicate):根据给定的条件筛选元素。
排序:
.sorted() 和 sorted(Comparator<? super T> comparator):对流中的元素进行自然排序或使用自定义比较器排序。
聚合:
.reduce(BinaryOperator<T> accumulator):通过指定的累积函数将流中元素逐步缩减为单个值。
.count():计算流中元素的数量。
.max(Comparator<? super T> comparator) 和 min(Comparator<? super T> comparator):找到流中的最大或最小元素。
匹配:
.allMatch(Predicate<T> predicate)、anyMatch(Predicate<T> predicate) 和 noneMatch(Predicate<T> predicate):检查是否所有、任意或没有元素满足某个条件。
收集:
.collect(Collector<? super T, A, R> collector):将流的结果收集到另一个容器中,比如列表、集合等。
终止操作:
终止操作会触发实际的流处理过程。在调用终止操作之前,所有的中间操作都是懒加载的,即它们不会立即执行,直到有终止操作被调用时才会开始处理。
.forEach(Consumer<? super T> action):遍历流中的每个元素并对其执行某些操作。
.toArray():将流转换为数组。
.findFirst() 和 findAny():返回第一个或任意一个元素(对于并行流来说,可能不是第一个)。
并行处理:
流可以是顺序的也可以是并行的。可以通过调用parallel()方法来创建并行流,从而利用多核处理器的优势加速处理大量数据。
短路操作:
有些操作可以在不需要处理整个流的情况下提前结束,比如anyMatch()、allMatch()和findFirst()等,当条件满足时就可以停止进一步处理。