jvm面试题看这一篇就够了

转发:https://blog.csdn.net/banzhuanhu/article/details/108307271
补充:
1.jvm内存模型哪些是私有,哪些是共有的
在JVM内存模型中,有一些是线程私有的(Private),而另一些是线程共享的(Shared)。下面是常见的私有和共有内存区域:

1.私有内存区域:

程序计数器(Program Counter):每个线程都有一个独立的程序计数器。

Java栈(Java Stack):每个线程都有自己的Java栈,用于存储方法调用的信息、局部变量等。

本地方法栈(Native Method Stack):与Java栈类似,但用于执行本地(native)方法。

2.共享内存区域:

堆(Heap):用于存储对象实例和数组。所有线程共享堆内存。

方法区(Method Area):用于存储已加载的类的元数据、静态变量、编译后的代码等。所有线程共享方法区。(元数据也在此存放)

运行时常量池(Runtime Constant Pool):方法区的一部分,用于存储编译期生成的常量、符号引用等。所有线程共享运行时常量池。

需要注意的是,虽然堆、方法区和运行时常量池是线程共享的,但对于具体的数据访问和操作,可能会涉及到同步机制,以确保数据的一致性和线程安全。

2.一个方法是如何在jvm中执行的

在JVM中,一个方法的执行涉及多个步骤和组件。下面是一个方法在JVM中的执行过程:

编写代码:程序员使用Java语言编写方法的代码。

编译:代码被编译器(如javac)编译为字节码文件(.class文件),包含了方法的字节码指令。

加载类:类加载器负责将字节码文件加载到JVM中,并创建对应的Class对象。

验证:验证器对加载的字节码进行验证,确保符合JVM规范。

准备:为方法的静态变量分配内存空间,并设置默认初始值。

解析:将符号引用转换为直接引用,即将方法调用转换为实际的内存地址。

初始化:对类进行初始化,包括执行静态代码块和静态成员变量的赋值。

创建栈帧:在方法调用时,JVM会为该方法创建栈帧,用于存储局部变量、操作数栈等信息。

方法执行:JVM按照字节码指令逐条执行方法的代码。这包括从栈帧中读取和修改局部变量、操作数栈上的数据,根据指令进行跳转或方法调用等操作。

返回结果:方法执行完成后,会将返回值存储在指定的位置,并将控制权返回给调用者。

3.JVM 调优的常用参数有哪些?请解释其中一些常见参数的作用。

-Xmx:设置最大堆内存大小。
-Xms:设置初始堆内存大小。
-Xss:设置线程栈的大小。
-XX:PermSize:设置永久代初始大小。
-XX:MaxPermSize:设置永久代最大大小。
-XX:+UseParallelGC:启用并行垃圾回收器。
-XX:+UseConcMarkSweepGC:启用并发标记清除垃圾回收器。

4.jvm栈是设置对象属性值的过程

在JVM中,栈不是直接用于设置对象属性值的过程。栈主要用于方法调用和执行期间的操作,而对象属性的设置通常发生在堆中。

当在方法内部创建一个对象时,对象本身会被分配在堆内存中,而在方法的栈帧中会存在一个引用指向该对象。可以通过使用该引用,在栈中进行对对象属性的访问和修改。

下面是一种设置对象属性值的典型过程:

首先,在栈帧中定义一个局部变量,用于保存对应的对象引用。

通过使用该对象引用,可以通过点操作符(".")来访问对象的属性。

根据属性的类型,可以对其进行赋值操作,即将新的值存储到该属性的位置上。

当方法执行完成后,栈帧被销毁,但对象本身仍然存在于堆内存中,并保留了之前设置的属性值。

需要注意的是,设置对象属性值的过程涉及到对象引用的传递和赋值,而非直接在栈上修改对象属性。栈帧中的局部变量只是保存了该对象的引用,以便在方法中对其进行访问和修改。

在多线程环境下,需要特别注意对对象属性的并发访问和修改。为了保证线程安全性,可能需要采用同步机制(如锁或volatile关键字)来确保对象属性的一致性和可见性。

总结来说,JVM的栈不是直接用于设置对象属性值的过程,而是在方法调用和执行期间对局部变量和对象引用进行操作。通过对象引用,可以访问和修改堆中的对象属性。

5.破坏双亲委派模型的原因及方法

自定义类加载行为:有些特殊场景下,可能需要定制化的类加载行为。例如,在某些框架或应用程序中,可能需要加载特定版本的第三方库,而不是使用系统中已经存在的版本。通过破坏双亲委派模型,可以实现自定义的类加载逻辑,满足特定需求。

解决类加载冲突:在复杂的应用程序中,可能会出现类加载冲突问题,即不同的类加载器加载了同名的类,导致类型不兼容或功能冲突。在这种情况下,通过破坏双亲委派模型,可以采用不同的类加载器加载相同名称的类,从而解决冲突问题。

提供动态扩展机制:有些框架或应用程序需要具备动态扩展的能力,允许用户在运行时加载和替换部分代码。通过破坏双亲委派模型,可以实现自定义的类加载策略,支持动态扩展和插件式开发。


双亲委派模型是Java类加载器的一种工作机制,用于保证类的加载和隔离性。它通过层次化的类加载器结构和优先级机制,确保类的加载在整个应用程序中是有序和一致的。

尽管双亲委派模型提供了许多好处,但有时候可能会出现一些特殊情况需要破坏该模型。以下是一些常见的破坏双亲委派模型的方法:

自定义类加载器:可以编写自定义的类加载器,并重写其loadClass()方法。在loadClass()方法中,可以修改类加载的逻辑,例如直接从指定位置加载特定的类,而不按照双亲委派模型去查找。

使用线程上下文类加载器:Java线程提供了一个上下文类加载器(Context ClassLoader),可以在运行时动态设置当前线程的类加载器。通过设置线程上下文类加载器,可以绕过双亲委派模型,选择性地使用不同的类加载器加载特定的类。

覆盖系统类库:有些特殊场景下,可能需要替换或修改Java标准库中的一些类。可以通过将修改后的类文件放置在应用程序的类路径下,以覆盖原有的系统类库。

6.jmm
JMM(Java内存模型)是一种规范,定义了Java程序中多线程并发访问内存的行为和规则。它确保不同线程之间对共享变量的读写操作具有可见性、原子性和有序性。

JMM主要关注以下几个方面:

可见性(Visibility):JMM确保一个线程对共享变量的修改对其他线程是可见的。这意味着当一个线程修改了某个共享变量的值后,其他线程在之后的读取操作中能够看到最新的值。

原子性(Atomicity):JMM支持原子操作,即对于基本数据类型的读取和赋值操作是原子的,不会被其他线程中断。

有序性(Ordering):JMM保证程序执行的顺序按照开发者编写代码时的顺序执行,但允许编译器和处理器进行重排序来提高性能,只要重排序不会改变单线程程序的执行结果即可。

Happens-Before关系:JMM通过Happens-Before关系来建立操作之间的先后顺序,以控制并发访问的可见性和有序性。Happens-Before关系包括程序次序规则、锁定规则、volatile变量规则、传递性等。
7.volatile为何不能保证原子性

volatile 关键字可以保证可见性和禁止指令重排序,但是它不能保证操作的原子性。原子性指的是一个操作是不可分割的,要么完全执行成功,要么完全不执行,不存在中间状态。

以下是 volatile 无法保证原子性的几个原因:

复合操作:如果一个操作是由多个步骤组成的,即使该变量被声明为 volatile,每个独立的步骤仍然可以被其他线程中断或插入。这可能导致结果的不一致性。

竞态条件:当多个线程同时对 volatile 变量进行读取和写入操作时,就会发生竞态条件。例如,两个线程同时读取 volatile 变量的当前值,并尝试根据此值进行修改和更新。由于没有互斥机制,两个线程可能会覆盖彼此的修改,导致数据的不一致。

非原子性操作:某些操作本身就不是原子性的,即使 volatile 变量在单个线程中使用也无法保证原子性。例如,递增操作(i++)包含读取、修改和写回三个步骤,其中任何一个步骤都可能被其他线程中断或插入,导致不正确的结果。

为了保证操作的原子性,可以使用锁机制(如synchronized关键字)或者使用原子类(如AtomicInteger、AtomicLong等)。这些机制提供了更强的原子性保证,能够确保操作的不可分割性和一致性。

总结起来,尽管 volatile 关键字可以保证可见性和禁止指令重排序,但它无法保证复合操作的原子性。如果需要保证操作的原子性,应该使用锁机制或原子类来实现。

8.volatile与内存屏障的关系

volatile 关键字:volatile 是Java语言提供的关键字,用于修饰共享变量。当一个变量被声明为 volatile 时,对该变量的读写操作会满足一定的语义规则。具体来说,volatile 保证了以下两点:

可见性:当一个线程修改了 volatile 变量的值时,其他线程可以立即看到最新的值。
禁止重排序:volatile 变量的读写操作不会被指令重排序优化。
内存屏障:内存屏障是一种硬件或指令级别的机制,用于控制指令执行的顺序以及对内存操作的可见性。内存屏障可以通过插入特殊的指令来确保指令的执行顺序以及对内存的读写操作的顺序。它们可以防止指令重排序优化,并且可以强制刷新缓存并将修改的数据写回主内存。

在某些情况下,编译器和虚拟机会使用内存屏障来实现 volatile 关键字的效果,以确保操作的有序性和可见性。但是需要注意的是,volatile 关键字还具有其他的语义规则,如禁止指令重排序等,而内存屏障没有办法完全满足这些规则。

9.详细解释下Happens-Before

Happens-Before(发生在之前)是Java内存模型(Java Memory Model,JMM)中的一个关键概念,用于定义多线程程序中操作执行顺序的规则和保证可见性。

Happens-Before规则包括以下几个方面:

程序顺序规则(Program Order Rule):同一个线程中,按照代码的先后顺序执行的操作具有 happens-before 关系。也就是说,线程中前面的操作 happens-before 后面的操作。

监视器锁规则(Monitor Lock Rule):一个 unlock 操作 happens-before 后续的对同一个监视器锁的 lock 操作。这意味着对于一个对象的解锁操作 happens-before 了对该对象的加锁操作。

volatile变量规则(Volatile Variable Rule):对 volatile 变量的写操作 happens-before 后续的对该变量的读操作。这确保了对 volatile 变量的写入对于其他线程是可见的。

传递性(Transitivity):如果 A happens-before B,并且 B happens-before C,则可以推断出 A happens-before C。换言之,如果存在 happens-before 关系的链条,那么链条上的任意两个操作都具有 happens-before 关系。

Happens-Before规则的目的是通过确定操作之间的happens-before关系来提供有序性和可见性的保证。它帮助程序员编写正确且可预测的多线程程序,避免了由于缺乏同步而引发的数据竞争和不确定性。

需要注意的是,Happens-Before规则仅保证了操作之间的顺序关系,并不保证操作的实际执行顺序。实际的指令重排序可能会改变操作的执行顺序,但必须保持 happens-before 关系的一致性。这样做旨在最大限度地优化程序的性能和执行效率,同时仍然满足happens-before关系的要求。

理解和正确使用Happens-Before规则对于编写正确和高效的多线程程序非常重要。在并发编程中,开发人员应该充分利用Happens-Before规则来确保正确的同步和可见性。
  1. Happens-Before底层实现原理
    `Happens-Before规则的底层实现涉及许多因素,包括编译器优化、内存屏障、缓存一致性和处理器指令执行等。下面是一些常见的底层实现原理:

编译器优化:编译器在生成字节码或机器代码时会进行各种优化,例如指令重排序和消除冗余操作。然而,编译器必须确保优化不违反Happens-Before规则。为了实现这一点,编译器需要根据规则的约束条件来指导优化过程。

内存屏障(Memory Barriers):内存屏障是硬件或指令级别的机制,用于控制指令执行的顺序以及对内存的读写操作的可见性。内存屏障可以防止指令重排序,并确保在屏障之前的操作 happens-before 屏障之后的操作。编译器和虚拟机通常会插入合适的内存屏障指令来满足Happens-Before规则的要求。

缓存一致性协议:在多核处理器系统中,每个核心都有自己的本地缓存。缓存一致性协议(如MESI)确保了多个处理器之间对于共享数据的操作的一致性。当一个处理器修改了共享数据时,缓存一致性协议会通知其他处理器使其缓存中的数据无效或更新。这确保了对共享数据的修改对其他线程是可见的,符合Happens-Before规则的要求。

处理器指令执行:处理器按照特定的顺序执行指令,并且可能会采取乱序执行和指令重排序等优化策略以提高性能。然而,处理器必须在执行指令时遵守Happens-Before规则的约束条件。它们会利用内部的硬件机制来确保操作的有序性和可见性,例如指令流水线、重排序限制和指令提交顺序。

综上所述,Happens-Before规则的底层实现涉及编译器优化、内存屏障、缓存一致性协议和处理器指令执行等多个因素的相互配合。这些机制确保了操作的有序性和可见性,同时充分利用硬件和编译器的优化来提高程序的性能和执行效率。`

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值