JVM之运行时数据区
整个JVM构成里面,由三部分组成:类加载系统、运行时数据区、执行引擎
按照线程使用情况和职责分成两大类
-
线程独享 (程序执行区域)
-
不需要垃圾回收
-
虚拟机栈、本地方法栈、程序计数器
-
-
线程共享 (数据存储区域)
- 垃圾回收
- 存储类的静态数据和对象数据
- 堆和方法区
一、堆
Java堆在JVM启动时创建内存区域去实现对象、数组与运行时常量的内存分配,它是虚拟机管理最大的,也是垃圾回收的主要内存区域 。
内存划分:
堆内存为什么会存在新生代和老年代?
分代收集理论:绝大多数对象都是朝生夕灭的。熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
-
如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;
-
如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域。
这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
内存模型的变迁:
- Young 年轻区 :主要保存年轻对象,分为三部分,Eden区、两个Survivor区。
- Tenured 年老区 :主要保存年长对象,当对象在Young复制转移一定的次数后,对象就会被转移到Tenured区。
- Perm 永久区 :主要保存class、method、filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到OOM :PermGen space 的错误。
- Virtual区: 最大内存和初始内存的差值,就是Virtual区。
- 由2部分组成,新生代(Eden + 2*Survivor ) + 年老代(OldGen )
- JDK1.8中变化最大是,的Perm永久区用Metaspace进行了替换
- 注意:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中。区别于JDK1.7
- 取消新生代、老年代的物理划分,新生代、老年代不再做物理隔离,新增H区存放超大对象。
- 将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的新生代、老年代区域
二、虚拟机栈
栈内存为线程私有的空间,每个线程都会创建私有的栈内存,生命周期与线程相同,每个Java方法在执
行的时候都会创建一个栈帧(Stack Frame)。栈内存大小决定了方法调用的深度,栈内存过小则会导
致方法调用的深度较小,如递归调用的次数较少。
Java虚拟机栈异常情况:
- 如果线程分配的容量超过了Java虚拟机栈允许的最大容量(Xss默认1m),会抛出StackOverflowError异常
- 如果Java虚拟机栈动态扩展,并且尝试扩展的时,无法申请到足够的内存,会抛出OutOfMemoryError异常
- 如果在创建新的线程时,没有足够的内存去创建对应的虚拟机栈,会抛出OutOfMemoryError异常【不一定】
栈帧是什么?
栈帧(Stack Frame)是用于支持虚拟机进行方法执行的数据结构。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
- 局部变量表:主要存放了编译期可知的各种数据类型和对象引用。
- 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中
- 动态连接:主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为动态连接。
- **返回地址:**方法的返回地址。
三、本地方法栈
本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务。
为什么需要本地方法?
Java是一门高级语言,我们不直接与操作系统资源、系统硬件打交道。如果想要直接与操作系统与硬件打交道,就需要使用到本地方法了。说白了,Java可以直接通过native方法调用cpp编写的接口!多线程底层就是这么实的。
四、方法区
方法区的具体实现有两种:永久代(PermGen)、元空间(Metaspace)
方法区存储什么数据?
主要有如下三种类型
- 第一:Class
- 类型信息,比如Class(com.hero.User类)
- 方法信息,比如Method(方法名称、方法参数列表、方法返回值信息)
- 字段信息,比如Field(字段类型,字段名称需要特殊设置才能保存的住)
- 类变量(静态变量):JDK1.7之后,转移到堆中存储
- 方法表(方法调用的时候) 在A类的main方法中去调用B类的method1方法,是根据B类的方法表去查找合适的方法,进行调用的。
-
第二:运行时常量池(字符串常量池):从class中的常量池加载而来,JDK1.7之后,转移到堆中存储
- 字面量类型
- 引用类型–>内存地址
-
第三:JIT编译器编译之后的代码缓存
如果需要访问方法区中类的其他信息,都必须先获得Class对象,才能取访问该Class对象关联的方法信息或者字段信息。
永久代和元空间的区别是什么?
- JDK1.8之前使用的方法区实现是永久代,JDK1.8及以后使用的方法区实现是元空间。
- 存储位置不同:永久代所使用的内存区域是JVM进程所使用的区域,它的大小受整个JVM的大小所限制。元空间所使用的内存区域是物理内存区域。那么元空间的使用大小只会受物理内存大小的限制。
- 存储内容不同:永久代存储的信息基本上就是上面方法区存储内容中的数据。元空间只存储类的元信息,而静态变量和运行时常量池都挪到堆中。
为什么要使用元空间来替换永久代?
- 字符串存在永久代中,容易出现性能问题和永久代内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 计划将HotSpot 与 JRockit 合二为一。
方法区实现变迁历史:
五、字符串常量池
三种常量池的比较
**class常量池:**一个class文件只有一个class常量池
- 字面量:数值型(int、float、long、double)、双引号引起来的字符串值等
- 符号引用:Class、Method、Field等
**运行时常量池:**一个class对象有一个运行时常量池
- 字面量:数值型(int、float、long、double)、双引号引起来的字符串值等
- 符号引用:Class、Method、Field等
**字符串常量池:**全局只有一个字符串常量池
- 双引号引起来的字符串值
字符串常量池如何存储数据?
为了提高匹配速度, 即更快的查找某个字符串是否存在于常量池 Java 在设计字符串常量池的时候,还
搞了一张StringTable, StringTable里面保存了字符串的引用。StringTable类似于HashTable(哈希
表)。在JDK1.7+,StringTable可以通过参数指定 -XX:StringTableSize=65539
字符串常量池案例:
package com.dreamoy.jvm.object;
import java.util.HashMap;
public class StringTableDemo {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("hello", 53);
map.put("world", 35);
map.put("java", 55);
map.put("world", 52);
map.put("通话", 51);
map.put("重地", 55);
test();
}
public static void test() {
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2);//false
String str3 = new String("abc");
System.out.println(str3 == str2);//false
String str4 = "a" + "b";
System.out.println(str4 == "ab");//true
final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab");//true
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");//false
String str7 = "abc".substring(0,2);
System.out.println(str7 == "ab");//false
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");//false
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());//true
}
}
总结:
- 单独使用**””**引号创建的字符串都是常量,编译期就已经确定存储到String Pool中。
- 使用**new String(“”)**创建的对象会存储到heap中,是运行期新创建的。
- 使用只包含常量的字符串连接符,如**”aa”+”bb”**创建的也是常量,编译期就能确定已经存储到StringPool中。
- 被final修饰的变量会变为常量,编译期就能确定已经存储到String Pool中。
- 使用包含变量的字符串连接,如**”aa”+s**创建的对象是运行期才创建的,存储到heap中。
- 运行期调用String的**intern()**方法可以向String Pool中动态添加对象。
六、程序计数器
程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。
为什么需要程序计数器?
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换(系统上下文切换)后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
七、直接内存
直接内存(堆外内存)与堆内存比较:
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
直接内存的使用场景:
- 有很大的数据需要存储,它的生命周期很长
- 适合频繁的IO操作,例如:网络并发场景