文章目录
- 前言
- 一、JVM 内存模型的理解
- 1.第一部分:线程共享区(堆和方法区)
- 2.第二部分:线程独占区(程序计数器、虚拟机栈和本地方法栈)
- 3.JVM的几个知识点
- 3.1 垃圾回收就指线程共享区(堆和方法区)的回收?
- 3.2 JVM的内存分配算法有哪些?
- 3.3 JVM优化技术有哪些?
- 3.4 JVM用于提高对象分配效率的技术有哪些?
- 二、堆内存模型的理解
前言
理解 JVM 内存模型和堆内存模型是 Java 开发中的重要知识点,也经常会在面试中被问及。下面结合示意图,记录对这两个知识点的理解。
一、JVM 内存模型的理解
JVM内存模型可理解为2部分5大块
,如下图所示:
1.第一部分:线程共享区(堆和方法区)
1.1 Heap
Heap用来保存对象实例的属性值,并不保存对象的方法(以帧栈的形式存在JVM Stack中),对象实例在Heap中分配好以后,需要在Stack中保留4字节的Heap内存地址,用来定位该对象实例在Heap中的位置。(元空间替换永久代后,方法区的字符串常量池和一般变量也保存在Heap中)
1.2 Method Area
方法区是一个JVM的概念,元空间( Metaspace)是方法区的具体实现
JDK1.8后的方法区的实现由永久代替换为元空间,其与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小,有两个参数:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N,对于64位JVM来说,元空间的默认初始大小是20.75MB,默认的元空间的最大值是无限。MaxMetaspaceSize用于设置metaspace区域的最大值,这个值可以通过mxbean中的MemoryPoolBean获取到,如果这个参数没有设置,那么就是通过mxbean拿到的最大值是-1,表示无穷大。
方法区内有个非常重要的区域-运行时常量池(Runtime Constant Pool,简称RCP)。类的版本、字段、方法、接口等信息保存在运行时常量池中。
2.第二部分:线程独占区(程序计数器、虚拟机栈和本地方法栈)
2.1 程序计数器
程序计数器用来记录程序的跳转。
2.2 虚拟机栈(栈内存)
Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。如下图所示
2.3 Native Method Stack
与VM Stack相似,VM Stack是为JVM提供执行JAVA方法的服务,Native Method Stack是为JVM提供执行native方法的服务。
3.JVM的几个知识点
3.1 垃圾回收就指线程共享区(堆和方法区)的回收?
JVM初始化运行的时候,都会分配好线程共享区和线程独占区,当线程终止时,线程独占区的三块(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被及时释放掉。因为线程独占区的生命周期与所属线程相同,而线程共享区的生命周期与JAVA程序运行的生命周期相同,这就是系统垃圾回收的场所只发生在线程共享区的原因(实际上对大部分虚拟机来说只发生在Heap上)
3.2 JVM的内存分配算法有哪些?
两种方式是指针碰撞(Pointer Bumping)和空闲列表(FreeList)。
类加载检查通过后,虚拟机为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。Java堆中内存是否绝对规整,决定了采用的内存分配算法(Java堆是否规整是由所采用的垃圾收集器是否带有压缩功能决定, 因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表
)
3.2.1 堆内存绝对规整的情况,已使用内存和空闲的内存分两部分存放,中间有一个指针作为分界点指示器,分配内存时,就是把指针向空闲的那边挪到一段与对象大小相等的距离,这种方式称为-指针碰撞,如下图
3.2.2 堆内存不规整的情况,已使用内存和空闲的内存相互交错,没办法采用指针碰撞,虚拟机必须维护一个列表,记录哪些内存块是可用的,分配时,从列表中将一块足够大的空间划分给对象,并更新列表记录,这种方式称为-空闲列表,如下图
3.3 JVM优化技术有哪些?
即时编译器(Just-In-Time Compiler,JIT): JIT 编译器是 Java 虚拟机的核心组件之一,负责将 Java 字节码动态编译成本地机器码,以提高程序的执行效率。JIT 编译器可以根据程序的运行情况进行优化,包括方法内联、循环展开、逃逸分析等技术,从而使得程序在运行时达到更高的性能。
逃逸分析(Escape Analysis): 逃逸分析用于确定对象的动态作用域,从而优化内存分配和对象的生命周期。逃逸分析可以实现栈上分配、同步消除、标量替换等优化,减少对象在堆上的分配和访问开销,提高程序的性能和内存利用率。
垃圾回收(Garbage Collection): 垃圾回收是 Java 虚拟机管理内存的重要机制,负责自动回收不再使用的对象,并释放其占用的内存空间。优化垃圾回收算法和调优垃圾回收器参数可以降低垃圾回收的停顿时间、减少内存碎片化,并提高系统的吞吐量和响应速度。
类加载优化: 类加载是 Java 虚拟机启动时必不可少的过程,优化类加载可以加速程序的启动速度和减少内存占用。类加载优化包括延迟加载、预加载、类缓存、类预编译等技术,可以根据应用的特性和需求来选择合适的优化策略。
锁优化: 锁是多线程编程中常用的同步机制,但不正确的锁使用会导致性能问题和死锁等并发问题。优化锁的使用包括锁粗化、锁消除、锁重偏向等技术,可以减少锁的竞争和粒度,提高程序的并发性能。
字符串优化: 字符串是 Java 程序中常用的数据类型,但频繁的字符串操作会带来性能和内存消耗的问题。优化字符串的使用包括字符串常量池、字符串拼接优化、字符串替换等技术,可以降低字符串操作的开销,提高程序的性能和内存利用率。
编译器优化: 除了 JIT 编译器外,Java 虚拟机还包括一些静态编译器和即时编译器,用于对 Java 代码进行优化和转换。编译器优化包括代码优化、指令优化、内联优化等技术,可以提高程序的执行效率和运行速度。
3.4 JVM用于提高对象分配效率的技术有哪些?
对象池(Object Pool): 对象池是一种将对象预先创建并缓存起来,以便在需要时可以直接重用的技术。通过对象池,可以减少对象的频繁创建和销毁,从而提高对象的重用率和系统的性能。
栈上分配(Stack Allocation): 栈上分配是一种将对象分配在线程栈上而不是堆上的技术。栈上分配可以提高对象的访问速度,减少垃圾回收的压力,但是由于线程栈的大小有限,只适用于一些较小的对象。
TLAB优化(TLAB Optimization): 除了基本的TLAB机制外,Java虚拟机还对TLAB进行了一些优化,例如根据应用程序的运行情况动态调整TLAB的大小、TLAB的并发分配等,以进一步提高对象分配的效率。
标量替换(Scalar Replacement): 标量替换是一种将对象拆分为其成员变量(标量)并分别进行优化的技术。通过标量替换,可以将对象的成员变量分配在栈上或者寄存器中,以减少对象的内存消耗和访问延迟。
对象内联(Object Inlining): 对象内联是一种将小对象的字段直接内联到使用该对象的地方的优化技术。通过对象内联,可以减少对对象的创建和访问,提高程序的性能。
逃逸分析(Escape Analysis): 逃逸分析是一种分析对象在方法中的生命周期和作用域的技术。通过逃逸分析,可以判断对象是否会逃逸到方法外部,从而决定是否可以进行一些优化,如栈上分配、标量替换等。
二、堆内存模型的理解
Java堆是JVM最大的一块内存空间,被划分成两个不同区域:新生代和老年代,新生代又被划分为三个区域:Eden、From Survivor、To Survivor。划分的目的是为了JVM能够更好的管理堆内存中的对象,包括内存分配及内存回收。图示如下:
新生代-Young Generation,主要用来存放新生的对象;
老年代-Old Generation,主要存放应用程序生命周期长的内存对象;
堆大小=新生代+老年代,堆大小可以通过参数-Xms、-Xmx来指定。
默认的新生代和老年代的比例值为1:2,其中,新生代被细分为Eden和Survivor区域, 这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的Eden:from:to=8:1:1,JVM每次只使用Eden和其中一块Survivor区域来为对象服务,总有一块Survivor区域是空闲的,因此,新生代实际可用内存空间为9/10(即90%)的新生代空间, Survivor的两个区是对称的,没先后关系,from和to是相对的。