【Java虚拟机——运行时数据区】

运行时数据区

作用

用于存放Java程序运行时所产生的数据和信息

组成部分

1、 程序计数器

作用:用于存放线程中将要执行的下一条字节码指令的地址,但是如果当前正在被执行的方法是本地(native )方法,那么程序计数器中的值是未定义的。

为什么执行本地方法时程序计数器中的值是未定义的?

因为本地方法的运行与Java语言无关,而是由更为底层的C/C++语言实现的,不会产生字节码文件所以不会有字节码指令,也就没有字节码指令的地址。

程序计数器占用的内存空间很小,存储着固定长度的字节码指令地址,所以虽然程序计数器是存储空间,但是不会存在内存溢出异常。程序计数器记录着线程的运行进度,由于计算机中存在着很多个线程,并且每个线程的执行进度都是不同的,所以程序计数器也是线程私有的。为了对线程的运行进度进行记录,所以程序计数器跟随线程的创建而创建,随线程的销毁而销毁。最后程序计数器的运行速度也是很快的。

特点:内存占用小、不存在内存溢出异常、线程私有、同线程共存亡、运行速度快

2、 Java虚拟机栈

作用:描述着Java方法执行的先后顺序,栈是运行时的单位

Java栈中存放着栈帧,栈帧中含有局部变量表操作数栈动态链接以及方法返回地址等信息,Java虚拟机中的每一个Java方法被调用时虚拟机都会创建一个对应着该方法的栈帧。每一个Java方法被调用直到执行完成的过程都是该方法对应的栈帧在Java虚拟机栈中从入栈到出栈的过程。在Java虚拟机栈中栈顶的栈帧代表正在运行的栈帧,也可以称为当前栈帧,当前栈帧所对应的方法也被称为当前方法。

Java虚拟机规范允许Java虚拟机栈的大小是固定不变的或者动态的,Java虚拟机栈的大小是可以通过参数进行调节的,当栈中存放的栈帧过多时就会产生内存溢出异常,准确来说:如果栈的大小是固定的,那么就会产生栈溢出异常,如果栈的大小是动态的,那么当Java虚拟机想要对栈进行扩容时,如果扩容时出现了异常,那么就会产生内存溢出异常。

Java虚拟机栈是线程私有的、同时也是与线程共存亡的、大小可以调节、会出现内存溢出异常。

栈帧:

局部变量表

用于存放被调用方法的参数以及方法中所定义的局部变量,如果是基本类型的变量那么存储的就是基本类型变量的值,如果是引用类型的变量那么存储的就是引用类型对象的地址

操作数栈

一个临时变量存储着方法运行时的操作数和中间结果,用于实现方法中所有的运算过程

动态链接

一个指向运行时常量池的方法引用(方法所在地址),由于一个方法在运行的过程中可能会调用另外一个方法,运行时常量池中存在调用方法的地址

方法返回地址

当一个方法执行完成之后需要返回到调用该方法的地方,所以需要保存调用该方法时的地址

3、 本地方法栈

本地方法栈和Java虚拟机栈很像,只不过Java虚拟机栈管理的是Java方法的调用,而本地方法栈管理的是本地方法的调用。

本地方法栈是线程私有的、可以调整大小、同样也会出现内存溢出异常

4、 堆

堆是存储空间,堆内存中存放的是Java虚拟机在运行程序时所创建的对象

堆存放着运行时产生的对象,所以在Java虚拟机启动时堆就会创建,并且创建时也可以根据参数调节堆内存占用空间的大小,但是当堆中创建的对象过于多时就会出现内存溢出异常,为了防止频繁地产生内存溢出异常所以堆内存中存在着垃圾回收机制,并且在堆中年轻代的垃圾回收频率要高于老年代的垃圾回收频率,由于Java虚拟机中堆内存只有一个,所以堆内存也是线程共享的,并且也是运行时数据区中占用内存空间最大的区域。

堆的内部结构

新生代

伊甸园区:存放着的是新创建的对象

幸存者0区

幸存者1区

幸存者区中存放着经过一次及一次以上垃圾回收后仍然没有被删除的对象,两个幸存者区在同一时刻总是有一个区域中是没有对象的。

老年代:用于存放经过15次垃圾回收之后仍然被保留下来的对象,这些对象由于长时间一直被使用所以这些对象会被移动到老年区,当然也不一定是15次垃圾回收之后,这个次数是可以根据参数进行调节的,但是最大是15次,那是因为在对象头中有一个4bit大小的空间用于存放该对象经历过多少次的垃圾回收,4位二进制数最大表示为15所以说次数最大为15。

堆中为什么要分区?

由于堆中存在着垃圾回收机制,但是由于垃圾回收算法不是完美的,无法满足在所有的情况下效率都很好,因此才会对堆内存进行分区,根据不同的区域选择最为合适的垃圾回收算法,提高垃圾回收算法的效率。

堆中对象创建并进行内存分配的过程?

假设两个幸存者区中没有一个对象,新生成的对象刚开始会被存放伊甸园区内,伊甸园区中的对象经过一次垃圾回收后会将保存下来的对象存放在幸存者0区内,继续再生成新的对象仍然存放在伊甸园区,此时进行第二次垃圾回收,对伊甸园区和幸存者0区中的对象都进行垃圾回收,再将剩下保存下来的对象一起移动到幸存者1区,此时幸存者0区中不存在对象,按这样的过程一直循环,保证两个幸存者区中总是有一个区域中是不存在对象的,直到对象经历的垃圾回收的次数达到所设置的次数,就会将对象移动到老年代中。

分代收集思想

我们发现在堆中创建对象并进行内存分配的过程中进行过很多次地垃圾回收,但是这个垃圾回收不是对整个堆内存的,而只是对年轻代区域的,由于存在于老年代中的对象是已经经过很多次垃圾回收后才存放在老年代中的,所以就可以暂且认为老年代中的对象在较长的一段时间内是仍旧会被使用的,所以对老年代进行垃圾回收的频率较低,只会在老年代空间快存不下对象时才进行垃圾回收,并且老年代的空间较大如果频繁使用垃圾回收会使程序运行效率变低。因此才会采用分代收集思想,分为部分收集和整堆收集

部分收集

新生区收集(Minor GC):对新生代进行垃圾回收

老年区收集(Major GC):对老年代进行垃圾回收

整堆收集(Full GC):对整个堆内存和方法区内存进行垃圾回收

整堆收集出现的情况

System.gc()方法

老年区空间不足

方法区空间不足

开发期间尽量避免整堆收集

字符串常量池在JDK7及之后被移动到了堆内存中,为什么?

在JDK7之前字符串常量池存储在永久代(现在的方法区内存)中,而且只有在发生FullGC时才会对方法区内存进行垃圾回收,但是由于Full GC只有在老年代空间不足或者方法区空间不足的情况下才会触发,所以导致对字符串常量池的垃圾回收频率较低,并且由于在程序运行过程中可能会在字符串常量池中生成很多个字符串,于是就会很容易导致方法区内存的空间不足,因此JDK7及之后将字符串常量池移动到了堆内存中,是因为堆内存中垃圾回收的频率较高。

在堆中不仅可以根据参数调节堆内存占用空间的大小,还可以根据参数调节每个区域所占用内存的比例

默认情况下

-XX:NewRatio = 2

新生代 : 老年代 = 1 :2

-XX:SurvivorRatio = 8

伊甸园区 : 幸存者区 : 幸存者区= 8 : 1 : 1

5、 方法区

方法区内存中主要用来存储加载的类的信息、即时编译期编译后的信息以及运行时常量池

方法区内存中存放着加载的类的信息,当加载的类过多时,就会出现内存溢出异常,方法区内存在Java虚拟机启动时被创建,创建时可以根据参数调节占用空间的大小,由于方法区内存只有一个,因此是线程共享的。

方法区内存的垃圾回收机制?

方法区内存中存放着类的信息,由于Java虚拟机只有在类被使用时才会对类进行加载,所以方法区内存中进行垃圾回收的大多不是被加载的类,而是运行时常量池中废弃的常量和不再被使用的类,类被回收的条件相当的苛刻,必须满足三个条件之后才能被回收,该类及其子类的对象全部被回收、该类的类加载器被回收、该类的Class对象被回收。

方法区的内部结构

类信息

即时编译器编译后的信息

运行时常量池

运行时常量池中存放着编译期间生成的字面量和符号引用,但是是在类被加载(加载、链接、初始化中的加载)到内存中之后,才会将字面量和符号引用存放到运行时常量池中,最后经过解析(验证、准备、解析),将符号引用转换为直接引用(类、方法、变量等所在地址)。运行时常量池就是一张表,虚拟机根据这张表找到要执行的类名、方法名、参数类型、字面量等信息。

字面量

例如:


        int a = 1;

其中的1就是字面量

符号引用

Java中任意类型的字面量都可以作为符号引用,只要在使用(解析)时可以准确的找到需要查找的目标(类、方法、成员变量、常量)即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值