JVM 运行时数据区

1.jvm运行时数据区

一个java文件编译成class文件,交给类加载器加载,被打散成各个部分放到不同的运行时数据区中执行或者存储。jvm运行时数据区大致可以分为以下几部分,(注意:其中的虚拟机栈和本地方法栈在HotSpot虚拟机中被合并成了java栈),而程序员所重点关注的内容包含java栈,方法区,堆;其余只需要了解即可。

 

(1)程序计数器:

这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器(就是指向当前代码运行到哪一行),每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置),程序计数器,这是JVM规范中唯一一个不会导致OutOfMemory(内存泄露,下文简称OOM)的区域。换句话上图中的其余4个区域,都有可能导致OOM。注意:如果线程执行的是java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果是native方法,程序计数器的值为undefined。cpu给线程分配时间片以执行线程,但是cpu分配时间片是抢占式的,如果当前线程在执行任务的时候,时间片被别的线程抢占了,该线程就会被挂起,直到重新分配到了时间片才会被唤醒,继续执行。而程序计数器会记录当前线程执行指令执行到了哪一行,被唤醒的的时候继续从这条指令开始执行就可以了。

(2)虚拟机栈:

虚拟机栈用于存放程序执行方法所需要的数据、指令、返回地址,每调用一个方法,每个方法的执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表、操作数栈、动态链接、方法出口等。虚拟机栈是线程独享的。

(a)局部变量表:

局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。

(b)操作数栈:

1) 操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。

2)存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。

3)数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。

4) java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令。

(c)动态链接:

动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用,那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务

1.查找被引用的类,(如果必要的话就装载它)

2.将符号引用替换为直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。

比如说运行时多态,一个接口可能存在多个实现类,当执行接口的某个方法时会去直接执行真正的实现类中的方法。

(d)方法出口:

方法返回的地方。

为了更好理解,我们将一段代码反编译一下,看看在指令层面是如何运行的。

/**
 * 成员变量
 */
private Object obj = new Object();

public void methodOne(int i){//
    int j = 0;  //iconst_0 :将int类型存入局部变量2
    int k = i+j;
    Object acb = obj;
    long start = System.currentTimeMillis();
    final int a = 0;
    methodTwo();
    return;
}
private void methodTwo() {
}

反编译 javap -c -v xxx.class > p.txt,得到的指令代码如下,大概意思就是从局部变量表中将索引为2和索引为3的整数压入操作数栈中(iload_1和iload_2,就是去局部变量表里取i和j的值),iadd指令将这两个数相加(i+j)后的结果推到操作数栈的栈顶,然后将将这个结果存到局部变量表索引为4的位置,如下

(3)方法区:

存储虚拟机加载的类信息(类的方法,版本,字段,接口),常量,静态变量,即时编译器编译后的代码(JIT)等数据。会存在OOM。方法区也称为永久代(方法区是JVM规范中提出的概念,而永久代是其具体实现(永久代这个词只适用于Hotspot虚拟机)),在jdk1.8,取消了永久代而使用元空间(meta space)代替。元空间与永久代之间最大的区别在于:元空间并不在虚拟机运行时数据区中,而是使用本地内存。理论上系统可以使用的物理内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。

JIT:Just In Time Compiler,即时编译器,他的工作是将java源代码编译成字节码(class)

(a)运行时常量池属于方法区的一部分。类似于hashSet,只能存放不同的实例,常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

String.intern()会将堆中的对象实例放到运行时常量池中

 

(4)堆,用于存储对象的实例

JMM(java memory model) java内存模型,堆内存可以划分为新生代,老年代。而新生代又可以划分为eden区、from Survivor区、to Survivor区。为何分代?因为对象的生命周期不一样(在垃圾回收章节中详细讲)。

2.对象的访问:

Object obj = new Object();这段代码其实分为了两部分,左边声明了一个类型为Object的引用obj,右边在堆内存中开辟了一块区域存放Object对象。那么如何通过引用去访问对象?

对象的访问有两种方式:

(1)句柄池,在java堆中划分一块内存来作为句柄池,句柄池中包含了实例数据和对象类型数据两部分对应的地址值,而引用存储的就是句柄池中的地址。就是说引用通过句柄池去找到对应的对象信息。

 

 

(2)直接指针,引用指向的是堆中对象的地址,直接访问。相比于句柄池,直接指针的方式速度更快,因为少了一次寻址的过程。在Hotspot中就是使用的这种方式来访问对象。

3.给对象分配内存,有以下两种方式:

1).指针碰撞:用一个指针区分已使用的堆内存区域和未使用的堆内存区域,每次创建一个对象就将指针向未使用的堆内存移动一点距离(这种方式必须保证堆内存区域是规整的,就是已使用的内存和未使用的内存完全隔离)。

2).空闲列表:用一张表记录哪些内存地址被使用,哪些不被使用。

如何分配内存由堆内存是否规整决定的,本质上是由垃圾回收机制决定的。

内存是否规整:已使用和未使用的两块区域独立并且连续。

线程安全性问题

当多个线程创建对象时,由于堆内存是线程共享的,使用指针碰撞分配内存或者空闲列表分配内存都可能产生线程安全问题。在多线程环境下,第一个线程还未给对象分配好内存,第二个线程就又来给其他对象分配内存了。

解决方案

1)线程同步:加锁,效率很低,使用CAS(compare and swap,比较并交换)进行线程同步,保证在同一个堆中多线程开辟空间不会冲突,但是cas在冲突激烈的时候会造成大量cpu浪费。

2)本地线程分配缓冲TLAB(Thread Local Allocation Buffer),将堆内存切分成若干块,每个线程操作不同的堆内存块,就不会产生线程安全问题。效率较高。如果分配的这块堆内存大小不够了,会再分配一块内存给该线程,这时候就需要使用线程同步了

-XX:+UseTLAB 来开启,-XX:+TLABSize 设置TLAB大小

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值