文章目录
  1. JVM学习01-JVM内存模型
  2. 1. JVM 简介
  3. 2. JVM 启动流程
  4. 3. JVM 内存模型
    1. 3.1 程序计数器
    2. 3.2 java虚拟机栈
    3. 3.3 本地方法栈
    4. 3.4 java堆
    5. 3.5 方法区
  5. 4. 线程工作的内存模型

[TOC]

JVM学习01-JVM内存模型

1. JVM 简介

JVM(Java Virtual Machine)是java虚拟机的缩写,JVM是一个虚构出来的计算机,并给出了一套JVM的规范。java虚拟机包括一套字节码指令、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与操作系统平台相关的信息,它只需要知道java文件最后生成的字节码文件,就能够将字节码生成具体与平台相关的机器指令,然后就可以在不同平台上不加修改的运行。这也是java跨平台的重要特点,一次编译,到处运行。

2. JVM 启动流程

当我们执行命令 “java Xxx” 之后,JVM就开始启动进行对java字节码的执行,启动流程如下。

  • 1. 装载环境配置

当运行java指令即java.exe后,就会获取到java的安装路径。然后通过路径去查找java.dll来确定jre的路径,最后找不到就通过java的版本来确定。确定好jre路径后,通过路径去寻找JVM.cfg文件,读取配置确定需要装载的JVM.dll。

  • 2. 装载JVM.dll

获取到JVM.dll之后就开始对该文件的装载,而JVM.dll就是JVM的主要的实现。

  • 3. 初始化JVM,获取JNIEnv实例

初始化JVM之后就可以通过JNI调用本地方法来获取JNIEnv的实例,而通过JNIEnv实例,我们可以调用如findClass等操作来获取需要执行的class。由于运行java实例有两种途径,一种是java -jar,还有一种是.class直接运行。通过jar运行的时候会去获取META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类,通过.class运行的话就直接获取到了class运行的主类。

  • 4. 运行main方法

通过JNIEnv实例获取到运行的主类,最后找到main方法后进行执行。

详细参考:《JVM虚拟机的启动流程原理》

3. JVM 内存模型

下面是JVM的内存模型图,稍微对网上的图进行了一下整合修改:

下面对内存模型做下基本的介绍:

3.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,由于多个线程之间需要进程CPU的资源,所以当处于运行状态的线程需要知道下一条运行的字节码指令的时候,就需要通过程序计数器来选取,所以每个线程都需要一个独立的程序计数器,即程序计数器时线程私有的。

3.2 java虚拟机栈

java栈中保存着java方法执行的内存模型,由于保存的是每个线程方法的运行时参数,所以栈是线程私有的。栈是由一系列的帧组成的(java栈有时也叫帧栈),每个方法执行的时候都会创建一个帧压入栈中,用来存储局部变量、操作数栈、动态链接、方法出口等信息。

  • 1. 存储局部变量

局部变量包括方法的参数和方法内的变量。局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项。
这个方法又分为静态方法实例方法

1
2
3
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {     
return 0;
}

runClassMethod :

1
2
3
public int runInstanceMethod(char c,double d,short s,boolean b) {     
return 0;
}

runInstanceMethod :

这里需要注意两点:

  • 第一从图中可以明显看出,实例方法的在存储局部变量的时候明显比静态方法多了一个对自身引用的存储,这个我们在调用方法的时候能够理解,因为静态方法是不用使用this的。
  • 第二,由于每调用一个方法就会为该线程创建一个帧,那么当我们调用方法的深度很深的时候(如递归),那么可能就会导致栈溢出(stackOverflowError)。栈的大小决定方法调用的深度(栈深度或者帧的数量),如果栈的大小是固定的,栈深度如果超过了栈最大深度,那么就会导致栈溢出(stackOverflowError);当然如果栈的大小是可以伸缩的,在超过内存空间的时候,则抛出OutofMemoryError。我们可以通过JVM参数 -Xss参数来设置分配栈的大小。

  • 2. 操作数栈

操作数栈和局部变量区的结构基本一样,但不是通过索引进行访问的,而是通过压栈和出栈的方式访问。操作数栈可以理解为一个计算产生值的一个临时存储区域。
如当运行该方法的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static int add(int a,int b){
int c=0;
c=a+b;
return c;
}
// 反编译后的指令
/* 第一行代码L2 */
0 iconst_0; /* 将0压入栈中 */
1 istore_2; /* 弹出, 存储在局部变量2中即c中 */
/* 第二行代码L3 */
2 iload_0; /* 局部变量0即a压栈 */
3 iload_1; /* 局部变量1即b压栈 */
4 iadd; /* 弹出两个变量求和, 并将结果压入栈中*/
5 istore_2; /* 弹出结果, 存储到局部变量2即c中 */
/* 第三行代码L4 */
6 iload_2; /* 将局部变量2即c压入栈中 */
7 ireturn; /* 弹出返回 */

运行add(100,98)的时候:

从图中可以看出,操作数栈是一个数据的临时存储区域,通过压栈和出栈进行操作。

  • 3. 其他

除了保存局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。

    1. 当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
    1. 除了处理常量池解析外,帧里的数据还要处理Java方法的正常结束和异常终止。
      • 2.1 如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复到发起调用的方法(上一层方法)的栈。如果方法有返回值,JVM会把返回值压入到发起调用方法(上一层方法)的操作数栈。
      • 2.2 为了处理Java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM运行catch块中的代码。如果没有catch,当前方法立即终止,然后JVM用帧区域数据的信息恢复发起调用的方法(上一层方法)的帧,然后再在当前的方法中重新抛出同样的异常,直到能够被当前方法或者上一层方法处理为止,不能处理则直接抛出异常。

3.3 本地方法栈

直接摘抄了:本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

3.4 java堆

java堆(heap)是JVM管理内存中最大的一个部分,是线程共享的。存放着对象的实例和数组。几乎所有的对象的实例都在堆上分配内存,这里的几乎是因为还有可能存在栈上分配内存的情况。

1
2
3
4
5
栈上分配:直接将数据对象分配在栈中.
1. 一般小对象(几十个Byte),在没有逃逸的情况下, 可以直接分配在栈上
2. 直接分配在栈上, 可以直接回收, 减轻GC的压力
3. 大对象或逃逸对象只能分配在堆上
逃逸:指不是线程私有的对象, 也被其他线程所使用到的对象.

java堆是垃圾收集器管理的主要区域,有时候也被称为”GC堆(Garbage Collected Heap)”。
java堆又分为新生代、老年代以及持久代,这里先不说持久态。
新生代用于存放刚创建或者年经的对象;老年代存放年龄比较老的对象,譬如新生代中的对象一直没有被回收,生存时间足够长,或者对象太大新生代无法创建都会被存放到老年代中;
java的新生代又可以进一步划分为eden,From Survivor(s0),To Survivor(s1)。之所以这么划分是跟GC算法息息相关的,这里只需要知道新生代被划分为这几个空间就好了。

3.5 方法区

方法区(在Hotspot中常被称为永久代)中保存中虚拟机加载的类的元数据信息:如类信息、常量池、静态字段、方法等数据。
这里有个概念容易让人混淆,就是方法区在物理上存储在堆中的,但是逻辑上是方法区和堆独立的。
所以,有的时候堆还可以在划分出一个区域叫做Perm区域(持久代),默认为64M,持久代一般就是指方法区,JVM将方法区加载进内存之后,这些内存通常是不会被回收的。由于持久代是Hotspot中的一个概念,所以持久代在堆中还是方法区中并没有定论,最新的HotSpot也计划将其移除。
这里注意一点:jdk6将String等常量信息放置在方法区中,jdk7已经移动到了堆中。
但是如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收。

4. 线程工作的内存模型

每一个线程都一个和主存相独立的工作内存,工作内存中存放着主存中变量值的一个拷贝(副本)。

  • 当数据从主内存复制到工作存储时,必须出现两个动作:

    • 第一,由主内存执行的读(read)操作;
    • 第二,由工作内存执行的相应的load操作;
  • 当数据从工作内存拷贝到主内存时,也出现两个操作:

    • 第一,由工作内存执行的存储(store)操作;
    • 第二,由主内存执行的相应的写(write)操作,每一个操作都是原子的,即执行期间不会被中断。

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用 volatile 关键字。
但是volatile仅仅只能保证每个线程读取的变量值是主存中最新的,但是不能保证变量的操作是原子性的。要保证线程的安全,还得使用java的同步机制。关于volatile的详细可参考其他博文。

文章目录
  1. JVM学习01-JVM内存模型
  2. 1. JVM 简介
  3. 2. JVM 启动流程
  4. 3. JVM 内存模型
    1. 3.1 程序计数器
    2. 3.2 java虚拟机栈
    3. 3.3 本地方法栈
    4. 3.4 java堆
    5. 3.5 方法区
  5. 4. 线程工作的内存模型