Java内存区域

image.pngimage.png

程序计数器

程序计数器,记录当前线程所执行的字节码的行号指示器
JVM规范中唯一一个没有规定任何OutOfMemoryError异常的区域
每个线程都需要一个独立的程序计数器

虚拟机栈

  • 线程私有,生命周期与线程相同
  • 每次方法调用的数据都是通过栈传递的
  • Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack)
  • 其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。
  • 实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
  • 局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
  • Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError
    • StackOverFlowError 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
    • OutOfMemoryError 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。

本地方法栈

和虚拟机栈所发挥的作用非常相似,
区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

Java堆

所有对象实例和数组都在这里
垃圾收集器的主要区域,有时候也被称为GC区

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M -vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了 -Xms128m JVM初始分配的堆内存 -Xmx512m JVM最大允许分配的堆内存,按需分配 -XX:PermSize=64M JVM初始分配的非堆内存 -XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配 -Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程 大约需要占用多少内存,可能会有多少线程同时运行等。 -Xmn 设置年轻代内存大小

分代回收算法 —> 新生代和老生代
再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:

  • 新生代内存(Young Generation)
  • 老生代(Old Generation)
  • 永生代(Permanent Generation),即Hotspot中方法区的实现

第二章 Java内存区域与内存溢出异常 - 图3
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了)
取而代之是元空间,元空间使用的是直接内存。
第二章 Java内存区域与内存溢出异常 - 图4

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1)

当它的年龄增加到一定程度,就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

晋升老年代的条件怎么计算? Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  • OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发


方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
又叫永久代

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 为什么会被称之为永久代呢?因为HotSpot虚拟机团队选择将GC分代收集扩展至方法区,像管理Java堆一样管理方法区。即使用永久代来管理方法区

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

混淆点,运行时常量池A和字符串常量池B

  • JDK7 之前,A和B都在方法区,即永久代
  • JDK7,B在堆中,B在方法区中,即永久代
  • JDK8后,A在方法区中,B在堆中。但是永久代被元空间(使用直接内存)取代

JVM 运行时常量池中存储的是对象还是引用呢?

  • 存储是引用

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

2.3 HotSpot对象探秘

对象的创建

image.png
new指令—>运行时常量池中定位—>检查是否执行了类加载过程—>新生对象分配内存—>指针碰撞或空闲列表(取决于Java堆是否规整)—>初始化为0值(不含对象头)—>为对象设置对象头—>对象初始化工作,方法执行

对象的内存布局

布局:对象头(32/64bit,1 运行时数据MarkWord 和2 类型指针 )、实例数据(存储顺序问题)、对齐填充
对象头寸存储两部分信息:
1/对象自身的运行时数据,如哈希码,GC 分代年龄,锁状态标识等,线程持有的锁等,,
2/类型指针 JVM通过这个知道这个对象是哪个类的实例
HotSpot的自动内存管理系统,要求对象起始地址必须是8字节的整数倍—>对齐填充

对象的访问定位

主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图所示。
第二章 Java内存区域与内存溢出异常 - 图6
句柄访问
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图所示。
第二章 Java内存区域与内存溢出异常 - 图7
指针访问
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的。

2.4 实战:OOM异常

内存泄漏还是内存溢出?
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出
String.itern()方法,如果字符串常量池中,已经包含了此String的字符串,则返回代表池中这个字符串的String对象;
否则,将包含的字符串添加到常量池中,并返回次String的对象
1、JDK6及以前,首次出现的string,会复制到永久代,并返回永久代中的应用
2、JDK7以后,不会复制,只是在常量池中记录这个首次出现的实例引用。

各种溢出: 堆溢出、-Xms -Xmx

虚拟机栈溢出和本地方法栈溢出、-Xss

方法区和运行时常量池溢出、-XX:MaxPemSize -XX:PemSize

本地内存直接溢出 -XX:MaxDirectByteBuffer