整体结构:
1.说说JVM内存整体的结构?线程私有还是共享的?
JVM(Java Virtual Machine)内存可以分为以下几个部分:
程序计数器(Program Counter Register):是线程私有的,用于记录当前线程执行的字节码指令地址。
Java虚拟机栈(JVM Stack):也是线程私有的,用于存储Java方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈(Native Method Stack):与Java虚拟机栈类似,也是线程私有的,用于存储本地方法执行时的局部变量表、操作数栈等信息。
Java堆(Java Heap):是所有线程共享的内存区域,用于存储Java对象实例和数组等数据。Java堆又可分为新生代和老年代两部分,其中新生代又可分为Eden空间、Survivor空间1和Survivor空间2。
方法区(Method Area):也是所有线程共享的内存区域,用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool):是方法区的一部分,用于存储编译期生成的各种字面量和符号引用,也是所有线程共享的。
直接内存(Direct Memory):不是JVM运行时数据区的一部分,但是JVM可以通过DirectByteBuffer类来操作直接内存。直接内存不受Java堆大小限制,可以使用本地系统内存。
总体来说,除了程序计数器、Java虚拟机栈和本地方法栈是线程私有的以外,其他内存区域都是所有线程共享的。
2.什么是程序计数器?
程序计数器(Program Counter Register)是JVM内存结构中的一部分,是线程私有的内存区域。它是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。当线程执行一个方法时,程序计数器记录的是正在执行的字节码的地址,当线程被切换回来时,它能够恢复到正确的执行位置。
在Java虚拟机中,每个线程都有自己的程序计数器,线程私有,所以它不会发生线程安全问题。当一个线程被创建时,它的程序计数器被初始化为0。在线程执行Java方法的时候,程序计数器会记录当前执行到哪个字节码指令。
程序计数器是一个非常小的内存区域,它是线程私有的,因此对于内存的使用并不是非常显著。但是,程序计数器在JVM的线程切换、异常处理、线程恢复等方面起着非常重要的作用。因此,程序计数器是JVM运行的必要组成部分,它的作用不容忽视。
3.什么是虚拟机栈
虚拟机栈(Java Virtual Machine Stack)是JVM内存结构中的一部分,是线程私有的内存区域,用于存储Java方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息。
当一个线程开始执行一个方法时,JVM会为该方法创建一个栈帧(Stack Frame),并把栈帧压入该线程的虚拟机栈中。一个栈帧包含了一个方法的局部变量表、操作数栈、常量池指针等信息。
Java虚拟机栈的大小是在JVM启动时就可以预设的,每个线程的虚拟机栈大小可以通过JVM启动参数进行配置。如果一个线程的虚拟机栈空间不足以支持当前方法的执行,那么就会抛出StackOverflowError异常;如果JVM不能再为新的栈帧分配内存空间,就会抛出OutOfMemoryError异常。
虚拟机栈是线程私有的内存区域,因此,不同的线程有自己的虚拟机栈。这个特性保证了线程之间的内存隔离,一个线程的虚拟机栈不能被其他线程访问。虚拟机栈的线程私有性使得多线程之间的数据共享和通信需要通过一些协调机制来实现。虚拟机栈的线程私有性也意味着它的内存分配和回收操作是线程独立的,不会发生线程安全问题。
4.Java虚拟机栈如何进行方法计算的?
Java虚拟机栈是线程私有的内存区域,用于存储Java方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息。当一个线程开始执行一个方法时,JVM会为该方法创建一个栈帧(Stack Frame),并把栈帧压入该线程的虚拟机栈中。一个栈帧包含了一个方法的局部变量表、操作数栈、常量池指针等信息。
当一个方法被调用时,JVM会将该方法的栈帧压入调用者线程的虚拟机栈中。执行该方法时,JVM会在该方法的栈帧中创建一个局部变量表,用于存储该方法的参数以及该方法中定义的局部变量。JVM还会在该栈帧中创建一个操作数栈,用于存储该方法中运算的操作数和中间结果。JVM通过操作数栈来进行方法的计算和运算。当方法返回时,JVM会弹出该方法的栈帧,将栈帧中的返回值传递给调用该方法的方法。
Java虚拟机栈通过栈帧来管理方法的调用和返回。在方法调用时,JVM会为该方法创建一个新的栈帧并压入调用者线程的虚拟机栈中;在方法返回时,JVM会弹出该方法的栈帧并返回结果。栈帧的创建和销毁是Java虚拟机栈的核心操作,因为它们涉及到方法的调用和返回,是程序运行的重要环节。
5.什么是本地方法栈
本地方法栈(Native Method Stack)是Java虚拟机栈的一部分,也是线程私有的内存区域,用于执行Native方法的。Native方法是指使用本地语言(如C、C++)编写的方法,可以被Java程序调用。与Java方法不同,Native方法的实现不在Java虚拟机中,而是在本地的操作系统和硬件平台上。
Java虚拟机通过本地方法栈来执行Native方法。当Java程序调用Native方法时,JVM会在本地方法栈中创建一个新的栈帧,并将Native方法的参数和返回值放在该栈帧中。Native方法的计算和运算都是在本地方法栈中进行的。当Native方法执行完毕时,JVM会将该方法的结果返回给Java程序,并弹出该方法的栈帧。
本地方法栈与虚拟机栈的功能类似,都是用于管理方法的调用和返回。但是,它们之间有一个重要的区别,即本地方法栈用于执行Native方法,而虚拟机栈用于执行Java方法。本地方法栈的大小也可以通过JVM启动参数进行配置。如果本地方法栈空间不足以支持当前Native方法的执行,就会抛出StackOverflowError异常;如果JVM不能再为新的栈帧分配内存空间,就会抛出OutOfMemoryError异常。
本地方法栈的线程私有性保证了不同线程之间的内存隔离,一个线程的本地方法栈不能被其他线程访问。本地方法栈的线程私有性还保证了它的内存分配和回收操作是线程独立的,不会发生线程安全问题。
6.什么是方法区
方法区(Method Area)是Java虚拟机中的一块内存区域,用于存储已经被加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是线程共享的内存区域,也是Java虚拟机中的永久代(Permanent Generation)。
方法区存储的是类级别的数据,与Java虚拟机栈和本地方法栈不同,它不是用于存储方法的局部变量和操作数栈等线程私有的数据。方法区的内存结构包含了运行时常量池、静态变量、类信息、即时编译器编译后的代码等内容。
运行时常量池是方法区的一部分,用于存储类和接口中的常量池信息。常量池中存储了常量、符号引用、字面量等数据,是Java程序中一些重要的静态数据结构。静态变量是指被static修饰的类变量,它们被存储在方法区中,它们的生命周期与类的生命周期一样长。
方法区还存储了类和接口的相关信息,包括类的访问修饰符、父类和接口信息、字段和方法信息等。这些信息在类加载的过程中被加载到方法区中,一旦被加载,就会一直存在于方法区中,直到虚拟机退出或被垃圾回收器回收。
另外,方法区还存储了即时编译器编译后的代码,即编译器优化后的本地代码。这些代码是存储在方法区中的,当某个方法被多次调用时,虚拟机会将该方法编译成本地代码,并存储在方法区中,以提高程序的执行效率。
方法区的大小可以通过JVM启动参数进行配置,如果方法区的空间不足以支持类的加载、常量池和代码的存储等操作,就会抛出OutOfMemoryError异常。方法区的线程共享性保证了所有线程都可以访问相同的类信息和静态变量,从而实现了内存的共享。
7.永久代和元空间内存使用上的差异?
永久代(Permanent Generation)和元空间(Metaspace)都是Java虚拟机中用于存储类元信息的内存区域。然而,它们在内存使用上有以下几点差异:
永久代是Java虚拟机规范中的一部分,而元空间是Java 8 引入的新特性,用于代替永久代。因此,永久代只在Java虚拟机中存在,而元空间则是Java虚拟机的一部分,可以使用操作系统的内存。
永久代的内存空间是固定的,在启动JVM时就已经分配了固定大小的内存,而元空间使用的是堆外内存,它的大小可以随着应用程序的需要进行动态调整。
永久代使用的内存是由JVM的堆内存分配的,而元空间使用的内存是由操作系统分配的。
永久代在Java虚拟机内存中的位置是在堆内存的末尾,而元空间位于堆外内存中。
永久代存储了类的元信息和静态变量等,而元空间存储了类的元信息和方法区信息,包括类名、类的父类、接口、字段、方法等。在元空间中,字符串常量池被存储在堆内存中。
永久代有可能会出现内存溢出的情况,例如常量池中存储的数据过多,导致永久代空间不足。而元空间不存在永久代的内存溢出问题,但如果元空间使用过度,会导致堆外内存溢出的问题。
总的来说,元空间比永久代更加灵活和可靠,可以根据应用程序的需要进行动态调整,避免了永久代的内存溢出问题。但是,由于元空间使用的是堆外内存,可能会增加操作系统的内存负担,需要注意内存使用的平衡和性能的优化。
8.堆区内存是怎么细分的?
Java虚拟机的堆区是Java应用程序中最大的内存区域,用于存储对象实例和数组等数据。堆区内存的细分主要有三个方面:新生代、老年代和永久代(在JDK8之前)/元空间(在JDK8及以后)。
新生代(Young Generation):年轻代是Java堆中的一部分,用于存储新创建的对象。年轻代被分为三个部分:一个Eden区和两个Survivor区(From和To),通常比例为Eden : Survivor = 8 : 1。新创建的对象首先会被分配到Eden区,当Eden区内存满时,就会触发一次Minor GC(年轻代垃圾回收),将Eden区和Survivor区中的存活对象复制到另一个Survivor区中,并清空Eden区和原Survivor区的内存。
老年代(Old Generation):老年代是Java堆中的一部分,用于存储存活时间较长的对象。在经过多次Minor GC后,仍然存活的对象就会被转移到老年代。当老年代内存满时,就会触发一次Major GC(老年代垃圾回收),清理老年代中的无用对象。老年代中的对象的生命周期比年轻代中的对象要长,垃圾回收的频率也比较低。
永久代(Perm Generation)/元空间(Metaspace):永久代或元空间是Java堆中的一部分,用于存储Java类的元数据,包括类名、方法名、常量池、字段描述等。永久代是在Java 7及之前的版本中使用的内存区域,而元空间是Java 8及之后的版本中使用的内存区域。永久代和元空间都是用来存储静态内容,与Java应用程序的堆内存无关。
堆内存的细分使得Java虚拟机可以更好地管理对象的内存使用,避免内存溢出等问题。
9.JVM中对象在堆中的生命周期?
在JVM中,对象在堆中的生命周期可以被描述为以下几个阶段:
创建对象:当使用new关键字创建一个新的对象时,JVM会在堆中分配一块内存来存储该对象。
使用对象:对象被创建后,程序可以对其进行操作,例如设置对象的属性值、调用对象的方法等。
对象不再被引用:当一个对象不再被任何引用变量所引用时,即没有任何方式可以访问到该对象时,JVM会将该对象标记为垃圾对象(Garbage Object)。
垃圾回收:当JVM需要更多的堆空间来分配新的对象时,会触发垃圾回收机制(Garbage Collection),该机制会扫描堆内存中所有的对象,并回收那些不再被引用的对象的内存空间。
对象被销毁:在垃圾回收机制释放了该对象的内存空间后,该对象就被销毁了,它所占用的内存空间也被回收。
需要注意的是,JVM中的垃圾回收机制是自动进行的,程序员无法直接控制。但是程序员可以通过优化代码,避免内存泄漏等问题,从而使得垃圾回收机制的效率更高,提高程序的性能和稳定性。
10.JVM中对象的分配过程?
在JVM中,对象的分配过程通常包括以下步骤:
检查堆空间是否有足够的内存来分配新的对象。如果没有足够的内存,则会触发垃圾回收机制来释放一些不再被引用的对象所占用的内存空间。
为对象分配内存。在JVM中,堆内存的分配通常是按照指针碰撞(Bump the Pointer)或空闲列表(Free List)的方式进行的。指针碰撞方式将内存分为已经分配的内存和未分配的内存两部分,用一个指针(指向下一个未分配的内存地址)将这两部分隔开;空闲列表方式将内存分为已经分配的内存和未分配的内存两部分,已经分配的内存被链表所连接,未分配的内存则是一个个独立的块。在分配内存时,JVM会在未分配的内存中找到一块足够大的内存块,并将其标记为已分配状态。
对象初始化。在为对象分配内存后,JVM会将分配到的内存空间清零,然后进行对象初始化,包括设置对象头信息、设置对象的默认值等。
返回对象引用。对象分配并初始化完成后,JVM会返回一个指向该对象的引用,这个引用可以用来访问对象的成员变量和方法等。
11.什么是 TLAB (Thread Local Allocation Buffer)?
TLAB(Thread Local Allocation Buffer)是为了优化Java虚拟机中的对象分配而引入的一种技术,它的存在主要是为了提高多线程程序中的对象分配性能。
在默认情况下,Java虚拟机中的对象分配是通过堆内存分配完成的。由于堆内存是所有线程共享的,多个线程可能会竞争同一块内存区域进行对象分配,从而导致性能下降。而使用TLAB技术后,每个线程都有自己的内存缓冲区,可以避免多个线程之间的竞争,提高对象分配的性能。
TLAB 分配过程
TLAB技术的优点包括:
1.减少了线程之间的竞争。每个线程都有自己的TLAB缓冲区,因此不同线程之间不会发生竞争,避免了锁竞争等问题,提高了对象分配的性能。
2.避免了内存碎片问题。由于每个线程都有自己的TLAB缓冲区,因此可以避免内存碎片问题,提高了内存的利用率。
3.提高了GC的效率。由于对象分配的缓冲区是线程私有的,因此可以使GC更加高效,减少了GC的暂停时间。
需要注意的是,使用TLAB技术也会带来一些额外的开销,例如每个线程都需要维护自己的缓冲区、缓冲区大小的调整等等。因此,在某些情况下,禁用TLAB技术可能会更好地适应特定的应用场景。