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

  1. 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)

  • 线程共享的数据区:方法区和堆
  • 线程隔离的区域:虚拟机栈,本地方法栈,程序计数器

image.png

程序计数器

线程私有内存。占用的空间非常小。作用就是用来看作当前线程所执行的字节码的行号指示器

  1. 线程私有

    • 每个线程都有一个独立的程序计数器(线程隔离)。
    • JVM 为每个线程创建单独的计数器,在线程切换时能够恢复到正确的执行位置
  2. 作用

    • 字节码指令的地址记录
    • 方法调用返回
    • 分支跳转
  3. 存储内容:

    • 如果线程执行的是 Java 方法,程序计数器记录当前正在执行的 字节码指令地址。
    • 如果线程执行的是 Native 方法(由本地代码实现),程序计数器的值为 undefined。

Java虚拟机栈

属于线程私有内存。生命周期和线程相同。

Java 虚拟机栈是线程私有的,每个线程在创建时都会分配一个虚拟机栈。它的作用是为线程的每个方法调用分配栈帧,栈帧中保存了方法运行所需的基本数据和指令。

  1. 虚拟机栈的组成

    • 局部变量表

      • 存储方法的局部变量,包括基本数据类型、对象引用以及 returnAddress 类型(指向字节码指令的地址)。
      • 局部变量表的大小在编译时确定,不会在运行时动态扩展。
    • 操作数栈

      • 用于执行字节码指令时的临时数据存储区。
      • 比如算术运算时,操作数会被压入操作数栈,计算完成后结果会被弹出。
    • 动态链接

      • 每个栈帧中包含一个指向运行时常量池的引用,用于支持方法调用的动态链接
    • 方法返回地址

      • 方法调用完成后,需要返回调用者的方法继续执行,这个地址会被记录在这里
    • 附加信息

      • 包括调试信息或 JVM 的优化数据
  2. 虚拟机栈的生命周期

    • 栈的创建:

      • 每个线程启动时都会创建一个虚拟机栈,栈的生命周期与线程相同。
    • 栈帧的创建:

      • 每当一个方法被调用时,会创建一个对应的栈帧并压入栈顶。
    • 栈帧的销毁:

      • 当方法调用结束后,栈帧会弹出栈顶并释放。
  3. Java 虚拟机栈可能出现以下两类异常:

    • StackOverflowError

      • 当栈的深度超过了 JVM 设置的栈大小限制时会抛出。
      • 常见于递归调用过深或死循环方法调用。
    • OutOfMemoryError(OOM)

      • 当虚拟机栈无法再为新线程分配内存时会抛出。
      • 常见于创建过多线程的场景。

本地方法栈

本地方法栈(Native Method Stack)是 JVM 内存结构的一部分,与 Java 虚拟机栈类似,但它服务于本地方法(Native Method)的调用。使用本地方法栈的主要目的是为调用非 Java 代码(如 C/C++ 实现的库函数)提供运行环境。

Java堆

Java堆(Heap)是 JVM 中的主要内存区域,用于存储对象实例和数组。堆是线程共享的,所有的对象实例以及数组都在堆上分配内存,并由垃圾回收机制进行自动管理。

  1. Java堆的作用

    • 所有对象(包括数组)都在堆中分配内存。方法中的局部变量虽然存储在栈中,但它们引用的对象位于堆中。
  2. 垃圾回收

    • 堆上的内存由垃圾回收器自动管理,分配和回收内存不需要手动处理
  3. 线程共享

    • 堆是 JVM 所有线程共享的内存区域,存储全局范围的对象

Java堆的内存划分

为了更高效地管理内存,Java 堆通常被划分为以下几个区域:

  1. 新生代(Young Generation)

    • Eden区

      • 对象在此分配内存,大部分对象在此创建。Eden区满时会触发Minor GC。
    • Survivor区(S0/S1)

      • 新生代中存活的对象被移动到 Survivor 区。经过多次 GC 后依然存活的对象会晋升到老年代
  2. 老年代(Old Generation)

    • 存放生命周期较长的对象,例如缓存对象、持久化数据等。此区域满时会触发Major GC(Full GC)
  3. 元空间(Metaspace)

    • 从 JDK 8 开始,类的元数据被存放在元空间中,而不再使用永久代(PermGen)。元空间使用本地内存。

Java堆的生命周期

  1. 堆的创建:

    • 堆在 JVM 启动时创建,其大小由 JVM 参数配置。
  2. 内存分配:

    • 对象在 Eden 区分配内存,无法分配时会触发 GC。
  3. 对象移动:

    • 对象随着年龄增长,从 Eden 区到 Survivor 区,再到老年代。
  4. 堆的销毁:

    • JVM 关闭时,堆的内存会被释放。

堆内存的分配与回收过程

  1. 对象分配:

    • 对象一般在 Eden 区分配内存,若对象较大,直接分配到老年代。
  2. Minor GC:

    • 当 Eden 区满时,触发 Minor GC。存活的对象会被移动到 Survivor 区或老年代。
  3. 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)。

方法区的作用

  1. 存储类与方法的相关信息,包括:
  2. 类信息:每个加载的类的元数据(类名,访问修饰符,父类的信息,接口信息等)。
  3. 运行时常量池:储编译器生成的各种字面量和符号引用,方法区中常量池是一个重要的子结构
  4. 静态变量:类的static变量存储在方法区,供整个程序共享
  5. 方法字节码:类中方法的字节码以及方法表
  6. 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 会对常量池中的内容进行回收。

方法区的垃圾回收

  1. 类元信息回收:

    • 如果某个类的所有实例都被回收,并且该类的 ClassLoader 也被回收,则该类的元信息可以被回收。
    • 这种回收的频率较低,因为通常会保留大量元信息(例如应用的核心类)。
  2. 运行时常量池回收:

    • 如果某些常量不再被引用,GC 可以回收这些常量。
    • 在 Java 中,字符串常量池可以回收未使用的字符串。

直接内存

是 JVM 内存模型的一部分,它不属于堆内存(Heap Memory),而是由操作系统在本地内存(Native Memory)中直接分配和管理的内存区域。直接内存通常用于高性能的 I/O 操作,因为它能避免堆内存与操作系统内存之间的数据拷贝,从而提升效率。

直接内存的特点

  1. 不属于对内存

    • 直接内存由操作系统分配,绕过了 JVM 的堆内存管理,不受堆内存大小(-Xmx)的限制
  2. 通过ByteBuffer访问

    • Java 提供了 java.nio.ByteBuffer 类用于分配和操作直接内存,特别是 ByteBuffer.allocateDirect() 方法
    • 相对于普通的堆内存分配(ByteBuffer.allocate()),直接内存操作更接近底层,减少了数据拷贝
  3. 高性能I/O操作

    • 直接内存的主要优势体现在 I/O 操作中,特别是与通道(Channel)结合使用时,能够减少数据在 JVM 堆内存和操作系统内存之间的拷贝次数
  4. 手动释放内存

直接内存的优缺点

优点:

  1. 高效的数据传输:

    • 直接内存避免了数据从 JVM 堆到操作系统内存的拷贝,提高了性能。
    • 在网络通信、文件 I/O 等高频操作场景下效果显著。
  2. 适合大数据处理:

    • 对于需要处理大块数据的应用程序(如 Netty),直接内存能显著降低堆内存的 GC 开销。
  3. 独立于堆内存大小:

    • 直接内存的大小不受 JVM 堆内存的限制(-Xmx),而是受操作系统可用内存限制。

缺点:

  1. 分配和释放成本较高:

    • 直接内存的分配需要调用本地操作系统接口,分配和释放的性能比堆内存慢。
  2. 难以调试:

    • 直接内存使用不当可能导致内存泄漏,但这部分泄漏无法通过 JVM 的堆内存监控工具直接检测。
  3. 受本地内存限制:

    • 直接内存大小受到操作系统可用内存的限制。如果直接内存分配过多,可能导致系统内存不足。

直接内存的典型使用场景

  1. NIO操作

    • Java NIO 中的 ByteBuffer.allocateDirect() 是直接内存分配的入口,配合通道(Channel)可以高效处理文件、网络数据。
  2. 高性能网络通信

    • 框架如 Netty 广泛使用直接内存,用于减少网络数据传输中的拷贝次数。
  3. 大数据处理

    • 对于需要频繁读取和写入大量数据的场景(如文件 I/O、缓存服务),直接内存能够提供显著的性能提升。

Java内存分配

新生代内存分配的比例

默认情况下,Eden区 和 两个Survivor区 的大小比例为:8:1:1

即:

  • 80% 分配给 Eden 区
  • 10% 分配给 Survivor0 区
  • 10% 分配给 Survivor1 区

新生代总内存中,Eden区 是分配内存的主要区域,绝大多数对象都会先分配到 Eden 区

内存分配过程

  1. 对象分配到 Eden 区:

    • 当创建新的对象时,大多数对象会首先分配到 Eden 区。
    • 如果 Eden 区满了,JVM 会触发一次 Minor GC(小垃圾回收),将仍然存活的对象移动到 Survivor 区。
  2. 对象在 Survivor 区之间移动:

    • 在一次 Minor GC 之后,仍然存活的对象会从 Eden 区移动到 Survivor 区(一般是 S0)。
    • 在下一次 Minor GC 时,如果对象依然存活,则可能在 Survivor0 和 Survivor1 之间来回复制。
    • 经过多次 Minor GC 后,如果对象仍然存活,并且年龄达到了某个阈值(默认为 15,具体由 JVM 参数 -XX:MaxTenuringThreshold 控制),会被提升到 老年代(Old Generation)。
  3. 对象晋升到老年代:

    • 存活时间较长、生命周期较长的对象会被移动到老年代。

爱跑步的猕猴桃
1 声望0 粉丝