1.Java类加载过程和JVM内存结构经典布局

讲解之前,我们先来看下一个Java 程序的执行流程

1. 一个Java 程序的执行流程

  • 1.*.java 文件通过 Javac.exe编译器编译成 *.class 字节码文件

*.java —>词法解析—>(通过token流) 语法解析—》语义分析—>生成字节码

  • 2.ClassLoader 将*.class 字节码数据加载到内存

字节码必须通过类加载过程到JVM环境后才能执行,执行有三种方式

  • 1.解释执行
  • 2.JIT编译执行
  • 3.JIT编译和解释混合执行

字节码类加载过程涉及到 三个阶段

  • 1.Load 读取*.class 文件产生二进制流,初步校验类的相关信息,创建实例
  • 2.Link
    • 2.1 验证,更详细的校验,比如数据类型是否正确等
    • 2.2 准备,为静态变量分配内存,并设置默认值
    • 2.3 解析,确保类和方法之间相互引用正确性,完成内存结构布局
  • 3.Init 执行类的构造器<clinit>方法,虚拟机栈中通过返回值赋值。
    加载的方式分为编译执行和解释执行

那么内存结构布局是什么样的呢?如果好奇跟我一起来看看。

2 JVM内存结构经典布局

Java 内存模型包括堆,虚拟机栈,程序计数器,本地方法栈,元空间。

下图是经典的Java JVM 内存结构经典布局:
1.Java类加载过程和JVM内存结构经典布局

2.1 Heap (堆区)

2.1.1 Heap (堆区)作用

存储着几乎所有实例对象
堆由垃圾回收器自动回收
堆区由各大线程共享使用。

2.1.2 Heap (堆区)大小控制

  • 堆区初始值 -Xms256M
  • 堆区最大值 -Xmx1024M

-X 表示JVM运行参数
ms 是memory start简称,代表最小堆容量
mx 是memory max 简称,代表最大堆容量

2.1.3 Heap (堆区)注意事项

线上生产环境需将Xms 和Xmx 设置一样,避免GC调整堆带来的额外压力

2.1.4 Heap (堆区)分类

堆分为两大块:新生代和老年代

新生代就是对象产生之初,
老年代就是快销毁的时候。

2.1.5 Heap (堆区)调优参数

-XX:MaxTenuringThreshold=15

这个参数能配置计数器的值达到某个阈值的时候,对象从新生代晋升至老年代。
如果参数为1,新生代直接移至老年代。
默认值为15

-XX:+HeapDumpOnOutOfMemoryError

设置这个参数可以在JVM内存溢出的时候输出堆内信息

2.2 Metaspace(元空间)

JDK8 中使用元空间替换永久代

如果出现以下类似的异常

 "Exception in thread 'dubbo client x.x connector'
java.lang.OutOfMemeoryError:PermGenspace"

修复方案如下:

-XX:MaxPermSize=1280m 

2.3 JVM Stack(虚拟机栈)

2.3.1 什么是虚拟机栈?

  • 栈是一个后进先出的数据结构
  • JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。
  • 栈中的元素用于支持虚拟机进行方法调用
  • 每个方法从开始调用到执行完成的过程就是栈帧从入栈到出栈的过程。
  • 在活动线程中,只有位于栈顶的帧菜是有效的,成为当前栈帧。
  • 正在执行的方法成为当前方法,栈帧是方法运行的基本结构。
  • 在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

2.3.2 栈溢出

*Error 表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。

2.3.3 虚拟机栈,栈帧,操作栈的关系

帮助记忆和理解:

  • 虚拟机栈是JVM的心腹大将,
  • 当前方法的栈帧都是正在战斗的战场
  • 其中的操作栈都是参与战斗的士兵

2.3.4 栈帧分类

栈帧包括

  1. 局部变量表
  2. 操作栈
  3. 动态链接
  4. 方法返回地址

2.3.4.1 局部变量表

局部变量表是存放方法参数和局部变量的区域

2.3.4.2 操作栈

  • 操作栈是一个初始状态为空的桶式结构栈。
  • 在方法的执行过程中,会有各种指令往栈中写入和提取信息。
  • JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
  • 字节码指令集的定义都是基于栈类型的
  • 栈的深度在方法元信息的stack属性中

2.3.4.3 动态链接

每个栈帧中包含一个常量池中对当前方法的引用,目的是支持方法调用过程中的动态链接。

2.3.4.4 方法返回地址

方法执行有两种退出情况,

  • 第一:正常退出

即:正常执行到任何方法的返回字节码指令,如RETURN IRETURN,ARETURN等

  • 第二:异常退出

无论何种退出情况,都将返回至方法当前被调用的位置,方法退出的过程相当于弹出当前栈帧。

退出可能有三种方式

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC计数器指向方法调用后的下一条指令

2.4 Native Method Stacks(本地方法栈)

  • Native Method Stacks(本地方法栈)是线程私有的
  • 虚拟机栈主内,本地方法栈主外,这个内外是相对于JVM来说的。
  • 本地方法栈为Native方法服务
  • 最著名的本地方法是System.currentTimeMillis();

2.5 Program Counter Register(程序计数寄存器)

在程序计数寄存器中,Register 的命名来源于CPU的寄存器,CPU只有把数据装载到寄存器才能运行。

每个线程在创建后都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等

  • 线程执行和恢复都要依赖程序计数器
  • 程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常

总结:
.java 通过javac.exe编译成.class,ClassLoader 加载.class 到内存中

  • 堆和元空间是线程共享的,
  • 虚拟机栈 、本地方法栈、程序计数器都是线程内部私有的

Java 内存模型包括堆,虚拟机栈,程序计数器,本地方法栈,元空间。

    1. 堆存储着几乎所有实例对象
    1. 虚拟机栈存放我们写的程序方法和局部变量
    1. 程序计数寄存器,线程执行和恢复都要依赖程序计数器
    1. 本地方法栈存放JNI调用的本地方法,如 System.currentTimeMillis();
    1. 元空间:JDK8 中使用元空间替换永久代

1.Java类加载过程和JVM内存结构经典布局

参考:《码出高效 Java开发手册 Easy Coding》

本节完~


没看够?继续看 2. Java并发编程之进程和线程,并行和并发