JVM的内存区域

HotSpot 虚拟机

一、JVM的内存区域

1.1 运行时数据区

1.1.1 程序计数器 Program Counter Register

​ 占据一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此未来线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们成这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空(undefined)。

1.1.2 Java虚拟机栈 Java Virtual Machine Stack

​ 线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量表空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

1.1.3 本地方法栈 Native Method Stacks

​ 本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。

1.1.4 Java堆 Java Heap

​ 对于大多数应用来说,堆空间是jvm内存中最大的一块。Java堆是被所有线程共享,虚拟机启动时创建,此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也就变得不那么绝对了。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。(如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。)

1.1.5 方法区 Method Area

和堆一样所有线程共享,主要用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.1.6 运行时常量池 Runtime Constant Pool

​ 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表 Constant Pool Table,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。Java虚拟机对class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被jvm认可。但对于运行时常量池,Java虚拟机规范没做任何细节要求。运行时常量池有个重要特性是动态性,Java语言不要求常量一定只在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制。当常量池无法再申请到内存时会抛出outOfMemeryError异常。

1.1.7 直接内存 Direct Memory

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。也可能出现OutOfmempryError 异常。

1.2 HotSpot 虚拟机对象探秘

1.2.1 对象的创建

​ Java是一门面向对象的编程语言,java程序运行过程中无时无刻都有对象被创建出来。创建对象的方式:new 关键字 、复制 、 反序列化等。
在虚拟机中除了数组和Class对象的 普通对象是如何创建的呢?
​ 在Java虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。
​ 在类加载检查通过后,虚拟机将为新生对象分配内存。为对象分配内存即将一块确定大小的内存从java堆中划分出来。假设Java堆是规整的所以被使用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点指示器,那所分配内存就是把那个指针向空闲空间方向移动一段与对象等大的距离,这种方式成为”指针碰撞“(Bump The Pointer)。但如果Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法进行简单的指针碰撞了。虚拟机就必须维护一个列表,记录那一块儿内存时可用的,在分配的时候从列表中找出足够大的空间划分给对象实例,并更新列表记录,这种方式被称为”空闲列表“(Free List)。选择哪种分配方式取决于Java堆是否规整,而Java堆是否规整取决于虚拟机采用的垃圾收集器是否带有空间压缩整理的能力。

​ 以上是如何给对象划分空间,除此之外,还需要考虑另外一个问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能会出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选的方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,成为本地本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在对应的缓冲区分配,只有缓冲区用完了,分配新的缓冲区时才需要同步锁定(不用每次分配对象都阻塞其他线程)。虚拟机是否使用TLAB 需要通过-XX:+/-UseTLAB 参数来指定。
​ 内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值。 这部操作保证了 对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问这些字段的数据类型所对应的零值。
​ 接下来,虚拟机还要对对象头进行必要的设置。例如这个对象是哪个类的实例、如何找到元数据信息、对象的哈希码、对象的GC分代年龄等信息。

1.2.2 对象 的内存布局

​ 在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头Header、实例数据Instance Data 、对象填充Padding。
​ Header包括两类信息。

1.2.3 对象的访问定位

​ 创建对象是为了后续使用。Java程序会通过栈上的reference数据来操作堆上的具体对象。由于《Java虚拟机规范》只规定了reference类型是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定,主流的访问方式主要是使用句柄访问直接指针

二、思考与实践

2.1 实例与静态

2.1.1实例变量与静态变量

/**
 *    在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,
 * 其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某
 * 个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,
 * 不用创建任何实例对象,在第一次访问类的任何静态变量或者静态方法的时候, 
 *	静态变量就会被分配空间,静态变量就可以被使用了。
 * 	  总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以
 * 直接使用类名来引用。例如,对于下面的程序,无论创建多少个实例对象,
 * 永远都只分配了一个staticInt变量,并且每创建一个实例对象,
 * 这个staticInt就会加1;但是,每创建一个实例对象,就会分配一个random,
 * 即可能分配多个random,并且每个random的值都只自加了1次。
 *
 */
public class StaticTest {

   private static int staticInt = 2;
   private int random = 2;

   public StaticTest() {
      staticInt++;
      random++;
      System.out.println("staticInt = "+staticInt+"  random = "+random);
   }

   public static void main(String[] args) {
      StaticTest test = new StaticTest();// 输出  staticInt = 3  random = 3
      StaticTest test2 = new StaticTest();// 输出 staticInt = 4  random = 3
   }
   
}

2.1.2实例方法与静态方法

https://blog.csdn.net/biaobiaoqi/article/details/6732117

2.2 OutOfMempryError异常 实战

​ 在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生OOM ,(OutOfMempryError)
本节实战的目的有两个:一个是通过代码验证《Java虚拟机规范》中描述的各个运行时区域储存的内容;第二,希望工作中遇到实际的内存溢出异常时,能根据异常的提示信息迅速得知是哪个区域的内存溢出,知道怎样的代码可能会导致这些区域的内存溢出,以及出现这些异常之后该如何处理。

2.4.1 Java堆溢出

运行结果:

要解决这个内存区域的异常,常规的处理方法是首先通过内存映射分析工具堆Dump出来的堆转出快照进行分析。

可以使用Java VisualVM这个工具打开转储文件。在JDK的bin目录下,这个工具并不是所有版本JDK都有。


分析:第一步,首先确认内存中导致OOM的对象是否时必要的,如果不必要存在的对象导致了溢出,则称之为“内存泄漏” Memory Leak .如果内存中的对象都是有必要存活的一般称为“内存溢出” Memory Overflow.
对于内存泄漏,可以进一步通过工具查看泄漏对象GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器未能回收它们。
如果不是内存泄漏,即内存中对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数 -Xmx与-Xms的设置,与机器内存对比,看看是否有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,应尽量减少程序运行时内存消耗。

2.4.2虚拟机栈和本地方法栈溢出

关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
1.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StcakOverflowError异常。
2.如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够内存时,将抛出OutOfMemoryError异常。

2.3 常量池

参与评论 您还未登录,请先 登录 后发表或查看评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:书香水墨 设计师:CSDN官方博客 返回首页

打赏作者

lemon.V

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值