03-JVM内存模型:堆与方法区

一、堆(Heap)

1.1.什么是堆

  堆是用于存放对象的内存区域。因此,它是垃圾收集器(GC)管理的主要目标。其具有以下特点:

  • 堆在逻辑上划分为“新生代”和“老年代”。由于JAVA中的对象大部分是朝生夕灭,还有一小部分能够长期的驻留在内存中,为了对这两种对象进行最有效的回收,将堆划分为新生代和老年代,并且执行不同的回收策略。不同的垃圾收集器对这2个逻辑区域的回收机制不尽相同,在后续的章节中我们将详细的讲解。
  • 堆占用的内存并不要求物理连续,只需要逻辑连续即可。
  • 堆一般实现成可扩展内存大小,使用“-Xms”与“-Xmx”控制堆的最小与最大内存,扩展动作交由虚拟机执行。但由于该行为比较消耗性能,因此一般将堆的最大最小内存设为相等。
  • 堆是所有线程共享的内存区域,因此每个线程都可以拿到堆上的同一个对象。
  • 堆的生命周期是随着虚拟机的启动而创建。

1.2.堆异常

  当堆无法分配对象内存且无法再扩展时,会抛出OutOfMemoryError异常。

  一般来说,堆无法分配对象时会进行一次GC,如果GC后仍然无法分配对象,才会报内存耗尽的错误。可以通过不断生成新的对象但不释放引用来模拟这种情形:

 1 /**
 2  * java堆溢出demo
 3  * JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 4  * Created by chenjunyi on 2018/4/25.
 5  */
 6 public class HeapOOM {
 7
 8     static class OOMObject {
 9     }
10
11     public static void main(String[] args) {
12         List<OOMObject> list = new ArrayList<>();
13         //不断创建新对象,使得Heap溢出
14         while (true) {
15             list.add(new OOMObject());
16         }
17     }
18
19 }

  上述代码中对象不断的被创建而不进行引用释放,导致GC无法回收堆内存,最终OutOfMemoryError,错误信息:

1 java.lang.OutOfMemoryError: Java heap space

二、方法区(Method Area)

2.1.什么是方法区

  方法区,也称非堆(Non-Heap),又是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据对象、static-final常量、static变量、jit编译器编译后的代码等数据,。另外,方法区包含了一个特殊的区域“运行时常量池”,它们的关系如下图所示:

(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编译后,执行的机器码会直接和底层机器打交道。如下图所示:

2.2.运行时常量池

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

  运行时常量池是方法区中一个比较特殊的部分,具备动态性,也就是说,除了类加载时将常量池写入其中,java程序运行期间也可以向其中写入常量:

1 //使用StringBuilder在堆上创建字符串abc,再使用intern将其放入运行时常量池
2 String str = new StringBuilder("abc");
3 str.intern();
4 //直接使用字符串字面量xyz,其被放入运行时常量池
5 String str2 = "xyz";

2.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。

2.4.方法区异常

  在2.3一节中,我们了解到方法区的2种实现方式最终都会有一个最大值上限,因此若方法区(含运行时常量池)占用内存到达其最大值,且无法再申请到内存时,便会抛出OutOfMemoryError。

  在下面的例子中,我们将使用cglib字节码生成框架不断生成新的类,最终使方法区内存占用满,抛出OutOfMemoryError:

 1 /**
 2  * java方法区溢出OutOfMemoryError(JVM参数适用于JDK1.6之前,借助CGLIB)
 3  * JVM参数:-XX:PermSize=10M -XX:MaxPermSize=10M
 4  * Created by chenjunyi on 2018/4/26.
 5  */
 6 public class JavaMethodAreaOOM {
 7
 8     public static void main(String[] args) {
 9         while (true) {
10             Enhancer enhancer = new Enhancer();
11             enhancer.setSuperclass(OOMObject.class);
12             enhancer.setUseCache(false);
13             enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(objects, args));
14             enhancer.create();
15         }
16     }
17
18     static class OOMObject {
19     }
20
21 }

  报错信息为:

1 Caused by: java.lang.OutOfMemoryError: PermGen space
2   at java.lang.ClassLoader.defineClass1(Native Method)
3   ···

  其实,在日常开发中,不仅仅使CGlib字节码生成框架会产生大量的class信息,动态语言、JSP、基于OSGI的应用都会在方法区额外产生大量的类信息。

原文地址:https://www.cnblogs.com/manayi/p/9651500.html

时间: 2024-08-25 08:46:33

03-JVM内存模型:堆与方法区的相关文章

jvm内存模型(运行时数据区)

运行时数据区(runtime data area) jvm定义了几个运行时数据区,这些运行时数据区存储的数据,供开发者的应用或者jvm本身使用.按线程共享与否可以分为线程间共享和线程间独立. 线程间独立的运行时数据区 线程间独立的区域随线程的创建而创建,随线程销毁而销毁.线程独立的区域内存储的数据只有该线程能够访问,对其他线程是不可见的. 程序计数器寄存器(pc Register) 每个线程都有自己的pc(程序计数器)register(寄存器).在任意时点上,jvm中的线程只能执行一个类的一个方

JVM 栈堆和方法区

栈区 栈区描述的是方法执行的内存模型.每个方法在执行时都会创建一个栈帧(存储局部变量.操作数栈.动态链接.方法出口等) JVM为每个线程创建一个栈,栈属于线程私有,不能实现线程间的共享,用于存放该线程执行方法的信息(实际参数.局部变量等) 基本类型变量去,执行环境上下文,操作指令区(存放操作指令) 堆区 堆用于存储创建好的对象和数组(数组也是对象) JVM只有一个堆,被所有线程共享 堆是一个不连续的内存空间,分配灵活,速度慢! 方法区 Jvm只有一个方法区,被所有线程公用 存放整个程序中唯一的元

运行时数据区域(堆 栈 方法区 常量池)和内存分配策略

内存管理 内存分配和内存释放 内存分配由程序完成,内存释放由GC完成 运行时数据区域 (1)程序计数器(program counter register) 一块较小的内存空间 当前线程所执行的字节码的行号指示器,字节码解释器在工作的时候就是通过改变程序计数器的值来选取下一跳要执行的指令 多线程环境下,线程轮流切换执行,程序计数器保证线程切换之后能恢复到正确的位置 每个线程都有一个独立的程序计数器 线程私有 没有任何异常 java方法,程序计数器的值为当前正在执行的虚拟机字节码指令的地址 nati

Eclipse中的快捷键快速生成常用代码(例如无参、带参构造,set、get方法),以及Java中重要的内存分析(栈、堆、方法区、常量池)

Eclipse中的快捷键快速生成常用代码(例如无参.带参构造,set.get方法),以及Java中重要的内存分析(栈.堆.方法区.常量池) 以上就是Eclipse中的快捷键快速生成常用代码(例如无参.带参构造,set.get方法),以及Java中重要的内存分析(栈.堆.方法区.常量池)的全部内容了,更多内容请关注:CPP学习网_CPP大学 本文固定链接:CPP学习网_CPP大学-Eclipse中的快捷键快速生成常用代码(例如无参.带参构造,set.get方法),以及Java中重要的内存分析(栈.

Jvm(27.14.2),理解升级---堆,栈,方法区

看完GC的回收策略之后,我们再来看一下堆,栈,方法区的交互. 首先我们必须牢记一句话,栈是堆和方法区的引用,学的越多对这句话的理解要越深. 1,这里的堆主要是对局部变量表来说的. 2,栈的内存地址是远远小于堆得,因为在栈中只是对象的引用. 3,gc回收只是回收堆内存,不用考虑栈的内存,因为栈的数据结构就是一旦出栈就会释放的. 栈也是JAVA虚拟机自动管理的,(不是由gc)栈类似一个集合(不过是有固定的容量),是由很多元素(专业术语:栈帧)组合起来的,在我们码代码的时候,每调用一个方法,在运行的时

【转】深入理解JVM—JVM内存模型

http://www.cnblogs.com/dingyingsi/p/3760447.html#3497199 我们知道,计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状,因此,现在CPU同内存交互就变

JVM内存模型-转载

http://my.oschina.net/u/567296/blog/303780 JVM的内部结构如下图: JVM主要包括两个子系统和两个组件: 1. 两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统: 1.1 Class loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域).Java程序员可以exten

深入理解JVM—JVM内存模型

原文地址:http://www.lofter.com/app/QRCodedownload?act=qbbkdlxz_20150313_13 我们知道,计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状,因

jvm的stack和heap,JVM内存模型,垃圾回收策略,分代收集,增量收集(转)

深入Java虚拟机:JVM中的Stack和Heap(转自:http://www.cnblogs.com/laoyangHJ/archive/2011/08/17/gc-Stack.html) 在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,我们从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题. 一般,JVM的内存分为两部分:Stack和Heap. Stack(栈)是JVM的内存指令区.Stack管理很简单,push

JVM内存结构(运行时数据区)

前言 Java程序的运行是通过Java虚拟机来实现的.通过类加载器将class字节码文件加载进JVM,然后根据预定的规则执行.Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些内存区域被统一叫做运行时数据区.Java运行时数据区大致可以划分为5个部分.在这里要特别指出,我们现在说的JVM内存划分是概念模型.如下图所示: JVM运行时数据区分为5种: 程序计数器 虚拟机栈(java栈) 堆 方法区 本地方法栈 程序计数器 程序计数器是一块较小的内存空间,它可