JDK、JRE、JVM关系
JDK(Java Development Kit): Java的软件开发包,包括Java运行时环境JRE
JRE(Java Runtime Environment):Java运行时的环境, 包括JVM
JVM(Java Vitrual Machine):
- 一种用于计算机上的规范
- Java在不同平台运行不需要重新编译,Java语言使用的Java虚拟机屏蔽了与具体平台的相关信息,Java语言编译程序只需要生成在Java虚拟机上运行的字节码
JDK包括JRE JRE包括JVM
- class字节码文件10个主要组成部分
- 魔数【4个字节】(Magic Number):主要作用是用来标记这是一个Java文件
版本号【4个字节】(Version):
- 前2字节:次版本号(minor_version)
- 后2字节:主版本号(major_version)
含义:
- 指定 class 文件的版本。
- JVM 会检查版本号是否与其支持的范围兼容,否则抛出 UnsupportedClassVersionError。
示例:
- Java 8 的主版本号是 52。
- Java 11 的主版本号是 55。
常量池(Constant Pool)
- 大小:动态,class文件中占据大量内容
- 含义:存储编译期生成的各种常量,是 class 文件的重要部分
组成:
- 常量类型有多种(如 UTF-8 字符串、类名、字段名、方法名、字面量等)。
- 每个常量都有一个索引,供字节码指令访问。
访问标志(Access Flags)
- 大小:2 字节。
- 含义:标识 class 文件的属性,如类的访问级别、是否是接口、抽象类等。
常见标志:
- 0x0020:标识类是接口。
- 0x0400:标识类是抽象类。
- 0x0001:标识类是公共类(public)。
类和父类索引(This Class 和 Super Class)
- 大小:2字节
含义:
- this class: 指向常量池中当前类的类名常量
- Super class:指向常量池中夫类的父名常量
- 注意:Object的夫类索引为0,因为它没有父类
接口表(Interfaces)
- 大小:动态
- 含义:列出当前类所实现的所有接口,指向常量池中接口的描述符
字段表(Fields)
- 大小:动态
- 含义:存储中类中声明的所有字段描述
字段信息包含:
- 字段访问修饰符(public、private)
- 字段类型(int、String)
- 字段名
- 指向常量池的引用
方法表(Methods)
- 大小:动态
- 含义:存储类中声明的所有方法的信息
方法信息包含:
- 方法访问修饰符(private、public)
- 方法名
- 方法描述符(返回值类型和参数类型)
- 方法的字节码指令(存储在Code属性中)
属性表(Attributes)
- 大小:动态
- 含义:用于存储额外的元信息,如类的注解、调试信息等
常见属性:
- Code:方法的字节码指令
- LinNumberTable: 行号与字节码指令对应关系
- SourceFile:源文件名
- Deprecated:标记某个元素已经过时
画一下jvm内存结构图
主要有方法区(Method Area),堆(Heap),虚拟机栈(VM Stack),本地方法栈(Natvie Method Stack),程序计数器(Program Counter Register)
- 线程共享的数据区:方法区和堆
- 线程隔离的区域:虚拟机栈,本地方法栈,程序计数器
程序计数器
线程私有内存。占用的空间非常小。作用就是用来看作当前线程所执行的字节码的行号指示器
线程私有
- 每个线程都有一个独立的程序计数器(线程隔离)。
- JVM 为每个线程创建单独的计数器,在线程切换时能够恢复到正确的执行位置
作用
- 字节码指令的地址记录
- 方法调用返回
- 分支跳转
存储内容:
- 如果线程执行的是 Java 方法,程序计数器记录当前正在执行的 字节码指令地址。
- 如果线程执行的是 Native 方法(由本地代码实现),程序计数器的值为 undefined。
Java虚拟机栈
属于线程私有内存。生命周期和线程相同。
Java 虚拟机栈是线程私有的,每个线程在创建时都会分配一个虚拟机栈。它的作用是为线程的每个方法调用分配栈帧,栈帧中保存了方法运行所需的基本数据和指令。
虚拟机栈的组成
局部变量表
- 存储方法的局部变量,包括基本数据类型、对象引用以及 returnAddress 类型(指向字节码指令的地址)。
- 局部变量表的大小在编译时确定,不会在运行时动态扩展。
操作数栈
- 用于执行字节码指令时的临时数据存储区。
- 比如算术运算时,操作数会被压入操作数栈,计算完成后结果会被弹出。
动态链接
- 每个栈帧中包含一个指向运行时常量池的引用,用于支持方法调用的动态链接
方法返回地址
- 方法调用完成后,需要返回调用者的方法继续执行,这个地址会被记录在这里
附加信息
- 包括调试信息或 JVM 的优化数据
虚拟机栈的生命周期
栈的创建:
- 每个线程启动时都会创建一个虚拟机栈,栈的生命周期与线程相同。
栈帧的创建:
- 每当一个方法被调用时,会创建一个对应的栈帧并压入栈顶。
栈帧的销毁:
- 当方法调用结束后,栈帧会弹出栈顶并释放。
Java 虚拟机栈可能出现以下两类异常:
StackOverflowError
- 当栈的深度超过了 JVM 设置的栈大小限制时会抛出。
- 常见于递归调用过深或死循环方法调用。
OutOfMemoryError(OOM)
- 当虚拟机栈无法再为新线程分配内存时会抛出。
- 常见于创建过多线程的场景。
本地方法栈
本地方法栈(Native Method Stack)是 JVM 内存结构的一部分,与 Java 虚拟机栈类似,但它服务于本地方法(Native Method)的调用。使用本地方法栈的主要目的是为调用非 Java 代码(如 C/C++ 实现的库函数)提供运行环境。
Java堆
Java堆(Heap)是 JVM 中的主要内存区域,用于存储对象实例和数组。堆是线程共享的,所有的对象实例以及数组都在堆上分配内存,并由垃圾回收机制进行自动管理。
Java堆的作用
- 所有对象(包括数组)都在堆中分配内存。方法中的局部变量虽然存储在栈中,但它们引用的对象位于堆中。
垃圾回收
- 堆上的内存由垃圾回收器自动管理,分配和回收内存不需要手动处理
线程共享
- 堆是 JVM 所有线程共享的内存区域,存储全局范围的对象
Java堆的内存划分
为了更高效地管理内存,Java 堆通常被划分为以下几个区域:
新生代(Young Generation)
Eden区
- 对象在此分配内存,大部分对象在此创建。Eden区满时会触发Minor GC。
Survivor区(S0/S1)
- 新生代中存活的对象被移动到 Survivor 区。经过多次 GC 后依然存活的对象会晋升到老年代
老年代(Old Generation)
- 存放生命周期较长的对象,例如缓存对象、持久化数据等。此区域满时会触发Major GC(Full GC)
元空间(Metaspace)
- 从 JDK 8 开始,类的元数据被存放在元空间中,而不再使用永久代(PermGen)。元空间使用本地内存。
Java堆的生命周期
堆的创建:
- 堆在 JVM 启动时创建,其大小由 JVM 参数配置。
内存分配:
- 对象在 Eden 区分配内存,无法分配时会触发 GC。
对象移动:
- 对象随着年龄增长,从 Eden 区到 Survivor 区,再到老年代。
堆的销毁:
- JVM 关闭时,堆的内存会被释放。
堆内存的分配与回收过程
对象分配:
- 对象一般在 Eden 区分配内存,若对象较大,直接分配到老年代。
Minor GC:
- 当 Eden 区满时,触发 Minor GC。存活的对象会被移动到 Survivor 区或老年代。
Major GC / Full GC:
- 当老年代没有足够空间存放新的对象或 Survivor 区晋升对象时,会触发 Major GC,对整个堆进行回收。
容易常驻内存,晋升为老年代的例子
示例1: Web 应用中的会话(Session)对象
在 Web 应用中,用户的会话信息通常需要存储较长时间(如几分钟到几小时)。这些对象在新生代中经过多次 GC 后,最终晋升到老年代。
import java.util.concurrent.ConcurrentHashMap;
public class SessionManager {
private static ConcurrentHashMap<String, Object> sessionCache = new ConcurrentHashMap<>();
public static void addSession(String sessionId, Object session) {
sessionCache.put(sessionId, session);
}
public static Object getSession(String sessionId) {
return sessionCache.get(sessionId);
}
}
- 用途:
用户登录后,其 Session 对象会存储在 sessionCache 中,并可能在整个会话期间保持不变。 - 存活时间:
对象生命周期长,可能晋升到老年代。
示例 2:数据库连接池中的连接对象
数据库连接池(如 DBCP、HikariCP)中的连接对象一般在整个应用生命周期内保持存活,用于复用数据库连接。
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
public class ConnectionPool {
private static DataSource dataSource;
static {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
hikariDataSource.setUsername("root");
hikariDataSource.setPassword("password");
hikariDataSource.setMaximumPoolSize(10);
dataSource = hikariDataSource;
}
public static DataSource getDataSource() {
return dataSource;
}
}
- 用途:
数据库连接池的连接对象通常在程序启动时创建,并在整个程序运行过程中保持存活。 - 存活时间:
数据源对象在老年代中,减少频繁分配和回收带来的开销。
示例 3:常用的缓存数据
public class Cache {
private static ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public static void put(String key, Object value) {
cache.put(key, value);
}
public static Object get(String key) {
return cache.get(key);
}
}
- 用途:
应用中常用的配置数据、热数据、查询结果等可以存入缓存,避免重复加载或计算。 - 存活时间:
缓存数据通常会在应用运行期间常驻内存,因此容易晋升到老年代。
示例 4:Spring 容器中的 Bean 单例对象
import org.springframework.stereotype.Component;
@Component
public class MyService {
public void performTask() {
System.out.println("Performing a task...");
}
}
- 用途:
Spring 的单例对象默认在容器启动时初始化,并在整个生命周期内服务于应用逻辑。 - 存活时间:
单例对象生命周期较长,在多次 GC 后会被提升到老年代。
示例 5:线程池中的工作线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing.");
});
}
executor.shutdown();
}
}
- 用途:
线程池中的线程不会频繁创建和销毁,而是保持活动状态,等待新的任务。 - 存活时间:
线程对象生命周期长,通常驻留在老年代。
java方法区
方法区是 JVM 规范定义的一部分,用于存储 类元信息、常量、静态变量 和一些与方法相关的数据(例如字节码)。尽管方法区的名称是“区”,但它本质上是堆的一部分(在某些实现中会分离出来)。方法区是 JVM 内存模型的一部分,属于线程共享区域。
在 Java 8 之前,方法区的实现是 永久代(PermGen);从 Java 8 开始,永久代被移除,取而代之的是 元空间(Metaspace)。
方法区的作用
- 存储类与方法的相关信息,包括:
- 类信息:每个加载的类的元数据(类名,访问修饰符,父类的信息,接口信息等)。
- 运行时常量池:储编译器生成的各种字面量和符号引用,方法区中常量池是一个重要的子结构
- 静态变量:类的static变量存储在方法区,供整个程序共享
- 方法字节码:类中方法的字节码以及方法表
- JIT编译后的代码:一些 JVM 会将 JIT 编译后的代码存储在方法区
Java 8 前后的方法区变化
Java 8之前(永久代 PermGen)
- 方法区实现为永久代,存储于堆外的独立区域。
- 永久代有固定的大小,默认为 64MB,可以通过 -XX:PermSize 和 -XX:MaxPermSize 配置。
缺点:
- 永久代大小固定,容易导致 OutOfMemoryError: PermGen space。
- GC 清理永久代的效率较低。
Java 8 之后(元空间 Metaspace)
- 方法区实现为元空间(Metaspace),存储在本地内存(非堆内存)。
- 元空间大小不固定,默认动态调整,可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置。
优点:
- 动态分配内存,避免永久代中因固定大小导致的内存问题。
- 元空间容量仅受限于物理内存大小。
运行时常量池
方法区中包含了运行时常量池,用于存储类加载后的一些常量值和符号引用:
- 字面量:字符串、基本类型值等。
- 符号引用:类、方法和字段的引用描述。
运行时常量池在方法区中,由于其内容可能在运行期间动态扩展,GC 会对常量池中的内容进行回收。
方法区的垃圾回收
类元信息回收:
- 如果某个类的所有实例都被回收,并且该类的 ClassLoader 也被回收,则该类的元信息可以被回收。
- 这种回收的频率较低,因为通常会保留大量元信息(例如应用的核心类)。
运行时常量池回收:
- 如果某些常量不再被引用,GC 可以回收这些常量。
- 在 Java 中,字符串常量池可以回收未使用的字符串。
直接内存
是 JVM 内存模型的一部分,它不属于堆内存(Heap Memory),而是由操作系统在本地内存(Native Memory)中直接分配和管理的内存区域。直接内存通常用于高性能的 I/O 操作,因为它能避免堆内存与操作系统内存之间的数据拷贝,从而提升效率。
直接内存的特点
不属于对内存
- 直接内存由操作系统分配,绕过了 JVM 的堆内存管理,不受堆内存大小(-Xmx)的限制
通过ByteBuffer访问
- Java 提供了 java.nio.ByteBuffer 类用于分配和操作直接内存,特别是 ByteBuffer.allocateDirect() 方法
- 相对于普通的堆内存分配(ByteBuffer.allocate()),直接内存操作更接近底层,减少了数据拷贝
高性能I/O操作
- 直接内存的主要优势体现在 I/O 操作中,特别是与通道(Channel)结合使用时,能够减少数据在 JVM 堆内存和操作系统内存之间的拷贝次数
- 手动释放内存
直接内存的优缺点
优点:
高效的数据传输:
- 直接内存避免了数据从 JVM 堆到操作系统内存的拷贝,提高了性能。
- 在网络通信、文件 I/O 等高频操作场景下效果显著。
适合大数据处理:
- 对于需要处理大块数据的应用程序(如 Netty),直接内存能显著降低堆内存的 GC 开销。
独立于堆内存大小:
- 直接内存的大小不受 JVM 堆内存的限制(-Xmx),而是受操作系统可用内存限制。
缺点:
分配和释放成本较高:
- 直接内存的分配需要调用本地操作系统接口,分配和释放的性能比堆内存慢。
难以调试:
- 直接内存使用不当可能导致内存泄漏,但这部分泄漏无法通过 JVM 的堆内存监控工具直接检测。
受本地内存限制:
- 直接内存大小受到操作系统可用内存的限制。如果直接内存分配过多,可能导致系统内存不足。
直接内存的典型使用场景
NIO操作
- Java NIO 中的 ByteBuffer.allocateDirect() 是直接内存分配的入口,配合通道(Channel)可以高效处理文件、网络数据。
高性能网络通信
- 框架如 Netty 广泛使用直接内存,用于减少网络数据传输中的拷贝次数。
大数据处理
- 对于需要频繁读取和写入大量数据的场景(如文件 I/O、缓存服务),直接内存能够提供显著的性能提升。
Java内存分配
新生代内存分配的比例
默认情况下,Eden区 和 两个Survivor区 的大小比例为:8:1:1
即:
- 80% 分配给 Eden 区
- 10% 分配给 Survivor0 区
- 10% 分配给 Survivor1 区
新生代总内存中,Eden区 是分配内存的主要区域,绝大多数对象都会先分配到 Eden 区
内存分配过程
对象分配到 Eden 区:
- 当创建新的对象时,大多数对象会首先分配到 Eden 区。
- 如果 Eden 区满了,JVM 会触发一次 Minor GC(小垃圾回收),将仍然存活的对象移动到 Survivor 区。
对象在 Survivor 区之间移动:
- 在一次 Minor GC 之后,仍然存活的对象会从 Eden 区移动到 Survivor 区(一般是 S0)。
- 在下一次 Minor GC 时,如果对象依然存活,则可能在 Survivor0 和 Survivor1 之间来回复制。
- 经过多次 Minor GC 后,如果对象仍然存活,并且年龄达到了某个阈值(默认为 15,具体由 JVM 参数 -XX:MaxTenuringThreshold 控制),会被提升到 老年代(Old Generation)。
对象晋升到老年代:
- 存活时间较长、生命周期较长的对象会被移动到老年代。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。