JVM内存模型及相互关系

jdk1.8JVM内存模型

jvm其实是由运行时数据区+类装载子系统+执行引擎组成的,我们通常说的jvm内存模型指的是运行时数据区。
在这里插入图片描述看看

运行时数据区

一、堆(线程共享)

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。堆中存放对象实例,数组。
有必要先了解下堆的区域划分,JVM的堆内存分为新生代(Young Generation)和旧生代(Old Generation)。新生代分为Eden区和Survivor区。
在这里插入图片描述
堆中存放的对象实例越来越多,如果不进行垃圾的清理,将会导致内存溢出,这里就要进行垃圾回收。首先要进行垃圾的理解:垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如何判断是不是垃圾?可根据某些算法来判定。

垃圾回收算法
1.引用计数法:对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

2.可达性分析算法(GC Roots根):该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

在Java中,可作为GC Root的对象包括以下几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象

可以看出可达性算法主要判别是引用有效,那到底哪些引用有效,不用被回收呢?

1. 强引用
强引用是指在程序代码中普遍存在的,类似Object obj = new Object()这类似的引用,这种是宁愿出现内存溢出,也不会回收这些对象。
2. 软引用
软引用是用来描述一些有用但并不是必需的对象,只有在内存不足的时候JVM才会回收该对象。
3. 弱引用
弱引用是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
4. 虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

二、虚拟机栈

线程私有的,每个方法执行时会创建一个栈帧。栈帧,是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素。用来存储局部变量表、操作数栈、方法出口、动态链接。栈中存放对象的引用,基本数据类型。栈数据结构:先进后出
介绍一下有关栈帧内容。 每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在这里插入图片描述

1.局部变量表、操作数栈:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。 操作数栈也常被称为操作栈,和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。举个栗子,有这么一段简单代码:

public class MathTest {
    public static int  complete(){
        int a = 4;
        int b = 6;
        int c = a + b;
        return c;
    }
    public static void main(String[] args) {
        int complete = complete();
        System.out.println(complete);
    }
}

通过javac 编译后 javap -c 来查看类的字节码。如下

 public static int complete();
    Code:
       0: iconst_4
       1: istore_0
       2: bipush        6
       4: istore_1
       5: iload_0
       6: iload_1
       7: iadd
       8: istore_2
       9: iload_2
      10: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  // Method complete:()I
       3: istore_1
       4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: iload_1
       8: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
      11: return

通过JVM指令码表来分析一波。来看complete()方法中,
iconst_4(int值入栈),

istore_0(将栈顶int类型值保存到局部变量0中),把栈顶的值弹出,放到局部变量表为0的地方(a=4)。

bipush 6(valuebyte值带符号扩展成int值入栈),当int取值-128~127时,JVM采用bipush指令将常量压入栈中。采用bipush指令将6压入栈中。

istore_1(将栈顶int类型值保存到局部变量1中),把栈顶的值弹出,放到局部变量表为1的地方(b=6)。

iload_0 (从局部变量0中装载int类型值入栈),把4放入操作数栈。

iload_1 则是取局部变量1 入栈,把6放入操作数栈。

iadd 很明显执行加法操作,这里就是从操作数栈的栈顶弹出两个int类型的值做加法操作,4+6。

istore_2(将栈顶int类型值保存到局部变量2中),(在执行到这行指令时,首先会在局部变量表里分配一个局部变量c,然后再进行赋值c=10)。

iload_2(从局部变量2中装载int类型值入栈)。

然后返回,注意:这里的栈都是指的操作数栈。

2.动态链接:每一个线程(方法)中有一个栈帧,保存了一个可以指向当前方法所在类的运行时常量池(类方法对应的jvm指令码的地址)。当需要调用其他方法时,可以从运行时常量池中取出对应的符号,然后将符号引用转换为直接引用,就可以调用方法。
在类被装载时,对应的每个方法都会在方法区对应一块jvm指令码地址,把这个地址存入动态链接里。当程序运行,通过类的头指针(创建对象时)进行方法调用时,就会把这个符号转变成方法对应指令码。
在这里插入图片描述

三、方法区(线程共享)

用于存储已经被虚拟机加载的类信息,常量,静态变量等。jdk7以前,方法区的实现是永久代,jdk8开始方法区的实现使用元空间取代了永久代。
元空间和永久代最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存。

方法区存储的是什么?
类型信息
常量(final修饰)
静态变量(static修饰)
即时编译器编译后的代码缓存

设置方法区的内存大小

(1)元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定

(2)默认值依赖于平台,windows下,-xx:MetaspaceSize是21.75M,-XX:MaxMetaspaceSize的值是-1,即没有限制

(3)默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生溢出,虚拟机一样会抛出OutOfMemoryError:Metaspace

(4)-xx:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端jvm来说,其默认值-xx:MetaspaceSize的值是21M,这就是初始的高水位线,一旦触及这个水位线,Full GC将会触发并卸载没用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则降低该值。

(5)如果初始高水位线设置过低,上述高水位线调整情况发生很多次,通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁的GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

运行时常量池

(1)运行时常量池是方法区的一部分,常量池表是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载存放到方法区的运行时常量池中。

(2)运行时常量池创建时机:在加载类和接口到虚拟机后,就会创建对应的运行时常量池

(3)jvm为每一个已加载的类型(类或者接口)都维护一个常量池,池中的数据项和数组项类似,使用索引访问

(4)运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换成真实地址

(5)运行时常量池类似于传统编程语言的符号表,但是它所包含的数据比符号表更加丰富

(6)当创建类或者接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则jvm会抛出OutOfMemoryError异常。

(7)运行时常量池具备动态性,比如使用String类的intern方法加入运行时常量池中

四、程序计数器

用来记录线程执行过程中字节码的行号指示器。如果当前线程是native方法,则其值为null。程序计数器是一块很小的内存空间,它是线程私有的。这个区域是唯一一个不抛出OutOfMemoryError的运行时数据区。执行引擎每次会更新线程到正在执行的jvm字节码行号。
在这里插入图片描述

五、本地方法栈

本地方法栈与虚拟机栈相似,虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口

相互关系

举个栗子,Object obj = new Object(); 其中obj 是对象引用,存放在虚拟机栈中,而对象实例(new Object())则存在堆中,这里栈指向了堆

private static Object obj=new Object(); 由于静态变量是存储在方法区,obj存入方法区,在创建的静态变量是对象类型时,对象实例还是会存入堆中的。所以,这里方法区指向了堆

然后来说一种堆指向方法区,在我们将编译好的类文件运行时,方法区会加载类的类元信息。这里有必要先说一下对象头指针,在每个对象被new出来时,都会有一个头指针指向方法区中加载的类的信息,可以根据头指针找到对应的类的方法,虚拟机通过这个指针确定这个对象是哪个类的实例。

对象在JVM中的存储形式

因为牵涉到头指针,这里介绍一下对象在jvm的存储形式。
在这里插入图片描述
对象在JVM中的存储,一共三个部分,为对象头、实例数据、对齐填充。
实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对齐填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
在这里插入图片描述
对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁类型状态等信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。在申请锁、升级锁时都要读取对象的Mark Word。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值