JVM——内存模型(三):堆与方法区

前两篇博客我们认识了程序计数器、虚拟机栈与本地方法栈。今天我们来一起认识一下堆与方法区。

关于堆内存,我之前有写过一篇关于堆外内存的博客,里面有详细介绍堆内存。这里为了观看方便,就直接把关于堆内内存的部分拿过来咯。(想了解堆内内存与堆外内存的伙伴们,可以参考:Java——堆外内存详解。)

1.Java堆内存

那什么东西是堆内存呢?我们来看看官方的说法。

“Java 虚拟机具有一个堆(Heap),堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”


也就是说,平常我们老遇见的那位,JVM启动时分配的,就叫作堆内存(即堆内内存)。

Java堆是Java虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存空间,在虚拟机启动的时候创建。而这块内存用来干嘛呢?此内存区域唯一的目的就是用来存放对象实例,几乎所有的对象实例都要在这里分配。

(不过这里需要注意,由于JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上就变得不是那么“绝对的”)。

当然,正是因为Java堆用来存放对象实例,所以该区域也就成了垃圾收集器管理的主要区域了。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

由于现在的垃圾收集器基本都采用分代收集算法,因此从内存回收的角度来看,理解jvm的堆还需要知道下面这个公式:

堆内内存 = 新生代+老年代+持久代(在jdk1.8中永久代放在了直接内存)


 如下图:

而从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区,但是不论它如何划分,都与存放的内容无关,无论是在哪一个区域,存放的都是对象的实例。(进一步划分的目的是为了更好地回首内存,或者更好地分配内存) 

在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描。

注意:在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World

此外,堆的内存不需要是连续空间,因此堆的大小没有具体要求,既可以固定,也可以扩大和缩小。我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值。

 

2.方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类字节码、class/method/field等元数据对象、static-final常量、static变量、即时编译后的代码等数据。

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但为了与堆区别开开来,它有一个别名叫做“Non-Heap”,即非堆。

另外,方法区包含了一个特殊的区域“运行时常量池”,它们的关系如下图所示:

(注意,在JDK1.7之前,字符串常量池存放在运行时常量池中,JDK1.7将字符串常量池从运行时常量池分离出来,放到了堆里。JDK1.8由于取消了“永久代”,方法区放在了元空间。)

注:想要了解字符串创建和存储的机制的伙伴可以参考:杂谈——字符串创建和存储的机制及相关的例子

下面来了解一下方法区中存储的这些东西。

(1)加载的类字节码:要使用一个类,首先需要将其字节码加载到JVM的内存中。至于类的字节码来源,可以多种多样,如.class文件、网络传输、或cglib字节码框架直接生成。
(2)class/method/field等元数据对象:字节码加载之后,JVM会根据其中的内容,为这个类生成Class/Method/Field等对象,它们用于描述一个类,通常在反射中用的比较多。不同于存储在堆中的java实例对象,这两种对象存储在方法区中。
(3)static-final常量、static变量:对于这两种类型的类成员,JVM会在方法区为它们创建一份数据,因此同一个类的static修饰的类成员只有一份;
(4)JIT编译器的编译结果:以hotspot虚拟机为例,其在运行时会使用JIT即时编译器对热点代码进行优化,优化方式为将字节码编译成机器码。通常情况下,JVM使用“解释执行”的方式执行字节码,即JVM在读取到一个字节码指令时,会将其按照预先定好的规则执行栈操作,而栈操作会进一步映射为底层的机器操作;通过JIT编译后,执行的机器码会直接和底层机器打交道。如下图所示:

3.方法区的实现

方法区的实现,虚拟机规范中并未明确规定,目前有2种比较主流的实现方式:

(1)HotSpot虚拟机1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作为实现,即将GC分代收集扩展至方法区。这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有-XX:MaxPermSize的上限)。在JDK1.7+之后,HotSpot逐渐改变方法区的实现方式,如1.7版本移除了方法区中的字符串常量池(上文汇中有说到)。

(2)HotSpot虚拟机1.8+:1.8版本中移除了方法区并使用metaspace(元数据空间)作为替代实现。metaspace占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。

由此我们可以引申:为什么使用“永久代”并将GC分代收集扩展至方法区这种实现方式不好,会导致OOM?

首先要明白方法区的内存回收目标是什么。方法区存储了类的元数据信息和各种常量,它的内存回收目标理应当是对这些类型的卸载和常量的回收。但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。因此,回收方法区内存不是一件简单高效的事情,往往GC在做无用功。另外随着应用规模的变大,各种框架的引入,尤其是使用了字节码生成技术的框架,会导致方法区内存占用越来越大,最终OOM。

4.运行时常量池

上文中说到,类的字节码在加载时会被解析并生成不同的东西存入方法区。类的字节码中不仅包含了类的版本、字段、方法、接口等描述信息,还包含了一个常量池。常量池用于存放在字节码中使用到的所有字面量和符号引用(如字符串字面量),在类加载时,它们进入方法区的运行时常量池存放。

Class关键常量池与运行时常量池的区别如下:

  • Java虚拟机对于Class文件的每一部分(包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但是对于运行时常量池,Java虚拟机却没有做任何细节的要求,因此不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。一般来说,除了保存Class文件中描述的符号引用,翻译出来的直接引用也会直接存储在运行时常量池中。
  • 运行时常量池对于Class文件常量池的另外一个重要特征就是具备动态性。Java语言并不要求常量一定只有编译期才能产生,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,即运行期间也可能将新的常量放入池中。这种特性被开发人员利用的比较多的就是String类的intern()方法。

其实我们也可以理解为:class文件常量池只是.class文件中的、静态的;而运行时常量池,是在运行时将所有class文件常量池中的东西加载进来的,它是动态的,可以在运行时将新的常量放入运行时常量池中。

而关于字符串常量池,上文有说到,这里就不提了。

由于运行时常量池方法区的一部分,它会收到方法区内存的限制,当常量池无法申请到内存的时候就会抛出OutOfMemoryError异常。

 

 

 

好啦,以上就是堆与方法区的相关知识总结,如果大家有什么更具体的发现或者发现文中有描述错误的地方,欢迎留言评论,我们一起学习呀~~

 

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

参考文章:《深入理解Java虚拟机》周志明著

https://www.cnblogs.com/manayi/p/9651500.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值