Java 内存模型和 JVM(Java虚拟机)密切相关。编译器(也就是 javac 命令) 负责将 Java 代码编译成可执行的字节码,并提供了一种独立于硬件和操作系统的运行环境。JVM 管理了 Java 程序运行时所需要的内存。JVM 必须按照 Java 内存模型的规定来管理内存,以保证多线程程序的正确性和可靠性。
Java 内存模型
Java 内存模型通过一系列机制来保证多线程程序中的正确性和可靠性。
Java 内存模型是规定了多线程程序中各个线程访问共享数据时的行为规范。在 Java 中,每个线程都拥有自己的工作内存,而共享数据则存储在主内存中。Java 内存模型通过定义一组规则,保证了线程之间共享数据时的可见性、有序性和原子性。
可见性
可见性指的是当一个线程修改了共享数据后,其他线程能够立即看到这个修改。Java 内存模型通过使用 volatile 关键字和 synchronized 关键字等机制来实现可见性。
有序性
有序性指的是程序执行的顺序不会被重排序,从而保证了线程执行指令的顺序与程序中编写的顺序一致。Java 内存模型通过使用 volatile 关键字和 synchronized 关键字等机制来实现有序性。
原子性
指的是若干个操作是不可被打断的整体性,不会出现部分执行成功的情况。Java 内存模型通过使用 synchronized 关键字和 Atomic 类等机制来实现原子性。
垃圾分代回收机制
Java 虚拟机的垃圾回收机制中,分代回收是一个重要的概念。分代回收是指将 Java 堆内存分为不同的代,然后针对每一代采用不同的垃圾回收算法和回收策略。通常将堆内存分为新生代和老年代两个部分。
新生代通常是指刚被创建的对象,因此它的对象生命周期比较短暂。为了高效地回收新生代中的对象,一般采用复制算法,将新生代分为一个较小的 Eden 区和两个较小的 Survivor 区,当 Eden 区满时,将其中存活的对象复制到一个 Survivor 区中,再将其中存活的对象复制到另一个 Survivor 区中。随着时间的推移,存活下来的对象最终会被复制到老年代中。
老年代则是指存活时间较长的对象,通常采用标记-清除或标记-整理算法进行回收。
通过将堆内存分为新生代和老年代两个部分,可以根据对象的生命周期采用不同的回收算法和策略,提高垃圾回收的效率和性能。
内存区域的划分
在 JVM 中,内存区域主要分为以下几个部分:
程序计数器(Program Counter Register)
程序计数器是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都拥有独立的程序计数器,线程切换时会切换对应的程序计数器。
Java 虚拟机栈(JVM Stack)
Java 虚拟机栈(Java Virtual Machine Stack)是 Java 虚拟机(JVM)运行时的一块内存区域,用于存储线程的方法调用栈。
每个线程在创建时,JVM 都会为其分配一块独立的栈空间,用于存储该线程的方法调用过程中的局部变量、操作数栈、返回值、方法返回地址等信息。栈的大小可以通过启动参数或程序运行时动态设定,但是如果栈空间不足,就会抛出栈溢出异常。
Java 虚拟机栈采用先进后出(Last-In-First-Out)的栈结构,每当调用一个方法时,就会将该方法的参数、局部变量以及返回地址等信息压入栈顶;当方法返回时,栈顶的数据会被弹出。
Java 虚拟机栈的生命周期与线程的生命周期一致,当线程执行完毕或因异常退出时,JVM 会回收该线程的栈空间。
本地方法栈(Native Method Stack)
本地方法栈与 Java 虚拟机栈类似,只不过它是为虚拟机执行本地方法(Native Method)服务的。在执行本地方法时,虚拟机会使用本地方法栈来支持这些方法的执行。
Java 堆(Java Heap)
Java 堆是 Java 虚拟机所管理的内存中最大的一块。所有线程共享同一个 Java 堆,用于存储对象实例。Java 堆可以分为新生代和老年代两部分,其中新生代又可以分为 Eden 空间、Survivor 空间(一般有两个)。
将 Java 堆划分为新生代和老年代的主要原因是为了更有效地进行垃圾回收。
Java 中的垃圾回收是自动的,它通过检测不再被引用的对象来释放内存。在这个过程中,垃圾收集器必须遍历整个堆来找到不再使用的对象。对于大型堆,这个过程可能非常耗时。
新生代和老年代的划分可以帮助优化垃圾回收过程。通常情况下,大多数对象都是短期存活的,因此它们被分配到新生代中。由于新生代中的对象生命周期较短,垃圾收集器可以更频繁地运行,以便及早地回收这些对象。
相反,老年代中的对象通常是长期存活的,因此垃圾收集器可以更少地运行,并且在运行时会更加注重性能。通过将堆分成两个区域,垃圾收集器可以更有效地管理内存,提高垃圾回收的效率和性能。
此外,Java 还提供了一些不同的垃圾收集器类型和算法,可以针对不同类型的应用程序和工作负载进行优化。例如,可以使用不同的垃圾收集器来处理新生代和老年代中的对象,以便更好地优化内存管理。
方法区(Method Area)
方法区也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,用于存储编译器生成的各种字面量和符号引用。在运行时,虚拟机还会动态的将符号引用转换为直接引用。
总结
Java 内存模型是一种规范,用于定义多线程情况下共享内存的交互方式。它规定了线程之间如何交换数据、同步访问共享变量等行为,以确保多线程程序的正确性和可靠性。Java 内存模型包含了一系列规则和约束,其中包括各种内存屏障和同步机制等。
Java内存空间划分则是将内存空间划分为不同的区域,每个区域都有其自身的用途和管理方式。在Java虚拟机中,通常将内存空间划分为以下几个区域:
因此,Java内存模型和内存空间划分都是 Java 虚拟机中非常重要的概念,但它们关注的方面不同。内存空间划分是关注内存空间的物理组织结构和用途,而内存模型则是关注多线程下内存访问的行为规范和约束。
常见面试问题
String 常量池存在于哪里?
在 Java 中,String 常量池(String Constant Pool)是一种特殊的内存区域,它位于方法区(Method Area)中。它用于存储编译器在编译时生成的字符串常量,这些字符串常量在编译期间就确定了,并且它们的值在整个程序生命周期内都不会发生变化。这样做的好处是可以提高字符串的重用率,避免浪费内存空间。在 Java 7 中,由于常量池被移到了堆中,所以现在常量池也可以称为运行时常量池(Runtime Constant Pool)。
Java 中堆和栈有什么区别?
堆和栈是 Java 中两种不同的内存区域,它们在内存分配方式、存储内容、空间大小、空间管理、作用范围和内存碎片等方面有所区别。了解堆和栈的区别可以帮助 Java 开发人员更好地管理内存,从而提高Java应用程序的性能和稳定性。
Java的内存回收机制是通过垃圾回收(Garbage Collection)来实现的。Java虚拟机(JVM)会自动地检测不再被引用的对象并将它们回收。
JVM的内存被划分为不同的区域,包括年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation)和元空间(Metaspace)。年轻代主要存储新创建的对象,当年轻代满时,会触发Minor GC,清理不再被引用的对象。年老代则主要存储存活时间较长的对象,当年老代空间不足时,会触发Full GC,清理年老代和永久代中不再被引用的对象。永久代主要存储一些固定的Java类信息,而元空间则是Java 8及以上版本中替代了永久代的区域。
JVM会使用不同的垃圾回收算法来清理不再被引用的对象,常用的算法包括标记-清除算法(Mark and Sweep)、复制算法(Copying)、标记-整理算法(Mark and Compact)和分代收集算法(Generational Collection)等。
垃圾回收机制的优点是可以自动地管理内存,避免了程序员手动释放内存带来的问题。但是,垃圾回收也有一些缺点,例如会在某些情况下造成停顿时间过长,影响程序的响应速度。因此,在实际的应用中,需要根据具体的应用场景选择适合的垃圾回收算法和配置垃圾回收参数。
JVM 的垃圾回收机制的主要步骤
JVM 的垃圾回收机制主要包括以下几个步骤:
标记
在这一阶段,垃圾回收器会遍历整个 Java 堆,标记所有被引用的对象。在标记过程中,会采用一些算法来提高标记效率,如根搜索算法、计数算法、可达性分析算法等。
清除
在标记完成后,垃圾回收器会清除所有未被标记的对象,这些对象被认为是无用的垃圾对象。
整理
在清除过程中,可能会产生一些内存碎片,这会影响后续的对象分配。为了解决这个问题,垃圾回收器会对存活的对象进行整理,将它们往一端移动,使剩余的内存空间连续。
JVM 的垃圾回收机制的类型
JVM 的垃圾回收机制有以下几种类型:
标记-清除算法(Mark and Sweep)
这种算法先标记所有的存活对象,然后清除所有未被标记的对象。但是,它会产生内存碎片,会影响后续的对象分配。
复制算法(Copying)
这种算法将 Java 堆分为两个相等的区域,每次只使用其中的一半,当这一半的空间用完时,将其中存活的对象复制到另一半空闲的区域。这种算法不会产生内存碎片,但是需要更多的空间来存储对象。
标记-整理算法(Mark and Compact)
这种算法先标记所有存活的对象,然后将它们移动到内存的一端,然后清除掉其余的对象。这种算法不会产生内存碎片,但是会降低垃圾回收的效率。
分代算法(Generational)
这种算法将 Java 堆分为不同的区域,每个区域有不同的对象存活时间。通常将 Java 堆分为新生代和老年代,使用不同的垃圾回收算法和频率来处理不同的区域。这种算法提高了垃圾回收的效率和性能。
常见面试问题
JVM的永久代中会发生垃圾回收么?
在 JDK 8 及之前的版本中,JVM 中有一个永久代(PermGen),用于存储类的元数据信息、常量池等内容。在永久代中确实会发生垃圾回收,但是垃圾回收机制与堆(Heap)中的垃圾回收机制不同。永久代的垃圾回收主要是回收无用的类信息和常量,而不是回收对象实例。
在 JDK 8 之后,永久代被移除,被一个称为元空间(Metaspace)的区域所取代。元空间不再是 JVM 堆的一部分,而是直接使用本地内存。因此,在元空间中也会发生垃圾回收,但是垃圾回收机制与传统的垃圾回收机制有所不同,它主要是回收已经被加载的类信息。元空间的垃圾回收机制使用的是基于标记-清除算法的垃圾回收机制。
Minor GC 与 Full GC 分别发生在什么时候?
Minor GC 和 Full GC 都是垃圾回收(Garbage Collection)的过程,用于清理Java虚拟机(JVM)堆中不再使用的对象。
Minor GC 发生在年轻代(Young Generation)中,用于回收年轻代中不再被引用的对象。年轻代被划分为 Eden 空间、Survivor 0 空间和Survivor 1 空间。当 Eden 空间满时,触发 Minor GC。在Minor GC中,Eden空间中不再被引用的对象会被清理,并将存活的对象移动到Survivor 0 或 Survivor 1 空间,当一个 Survivor 空间被填满时,存活对象将被移动到另一个 Survivor 空间,如果 Survivor 空间不足以存储存活对象,则存活对象将被移动到年老代(Old Generation)。
Full GC发生在年老代中,用于回收年老代和永久代(Permanent Generation)中不再被引用的对象。年老代是 JVM 堆中存储较长时间的对象的区域。Full GC会清理整个堆,包括年轻代和年老代,并且会停止应用程序的运行。Full GC 的触发条件比较复杂,一般包括以下几个方面:当年老代空间不足时,当永久代空间不足时,当系统内存不足时,当调用 System.gc() 方法时等。
阅读量:2033
点赞量:0
收藏量:0