深入理解 JVM 的内存区域
Posted soy-technology
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解 JVM 的内存区域相关的知识,希望对你有一定的参考价值。
深入理解运行时数据区
代码示例:
1. JVM 向操作系统申请内存:
JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。
2. JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小
-Xms30m -Xmx30m -Xss1m -XX:MaxMetaspaceSize=30m
3. 类加载(类加载的细节后续章节会讲):
这里主要是把 class 放入方法区、还有 class 中的静态变量和常量也要放入方法区
4. 执行方法及创建对象:
启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 对象,对象引用 就存放在栈中。
后续代码中遇到 new 关键字,会再创建一个对象,对象引用 就存放在栈中。
总结一下 JVM 运行内存的整体流程
JVM 在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。
方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。
同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。
从底层深入理解运行时数据区
堆空间分代划分
堆被划分为新生代和老年代(Tenured),新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
(后续对象分配和垃圾回收会细讲这块)
GC 概念
GC- Garbage Collection 垃圾回收,在 JVM 中是自动化的垃圾回收机制,我们一般不用去关注,在 JVM 中 GC 的重要区域是堆空间。
我们也可以通过一些额外方式主动发起它,比如 System.gc(),主动发起。(项目中切记不要使用)
JHSDB 工具
JHSDB 是一款基于服务性代理实现的进程外调试工具。服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的,主要基于 Java语言实现的API 集合。
JDK1.8的开启方式
开启 HSDB 工具:
Jdk1.8 启动 JHSDB 的时候必须将 sawindbg.dll(一般会在 JDK 的目录下)复制到对应目录的 jre 下(注意在 win 上安装了 JDK1.8 后往往同级目录下有一个jre 的目录)
JDK1.9及以后的开启方式
进入 JDK 的 bin 目录下,我们可以在命令行中使用 jhsdb hsdb 来启动它
代码改造
VM 参数加入:
-XX:+UseConcMarkSweepGC
-XX:-UseCompressedOops
JHSDB 中查看对象
实例代码启动
因为 JVM 启动有一个进程,需要借助一个命令 jps查找到对应程序的进程
在 JHSDB 工具中 attach 上去
JHSDB中查看对象
查看堆参数:
上图中可以看到实际 JVM 启动过程中堆中参数的对照,可以看到,在不启动内存压缩的情况下。堆空间里面的分代划分都是连续的。
再来查看对象:
可以看到 JVM 中所有的对象,都是基于 class 的对象
全路径名搜索
双击出现这个 Teacher 类的对象,两个,就是 T1 和 T2 对象
最后再对比一下堆中分代划分可以得出为什么 T1 在 Eden,T2 在老年代
JHSDB 中查看栈
从上图中可以验证栈内存,同时也可以验证到虚拟机栈和本地方法栈在 Hotspot 中是合二为一的实现了
当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下:
1. JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间。
2. JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
3. 完成上一个步骤后,JVM 首先会执行构造器,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、
静态代码块、静态方法,静态变量和常量放入方法区
4. 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 Teacher 对象,对象引用 student 就存放在栈中。
执行其他方法时,具体的操作:栈帧执行对内存区域的影响。栈帧执行对内存区域的影响
从底层深入理解运行时数据区(总结)
深入辨析堆和栈
功能
Ø 以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char 等)以
及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
Ø 而堆内存用来存储 Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
线程独享还是共享
Ø 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
Ø 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
空间大小
栈的内存要远远小于堆内存
虚拟机内存优化技术
栈的优化技术——栈帧之间数据的共享
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了
使用 JHSDB 工具查看栈空间一样可以看到。
内存溢出
栈溢出
参数:-Xss1m,具体默认值需要查看官网:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABHDABI
HotSpot 版本中栈的大小是固定的,是不支持拓展的。
java.lang.StackOverflowError一般的方法调用是很难出现的,如果出现了可能会是无限递归。
虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
OutOfMemoryError:不断建立线程,JVM 申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了)
同时要注意,栈区的空间 JVM 没有办法去限制的,因为 JVM 在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小。
堆溢出
内存溢出:申请内存空间,超出最大堆内存空间。
如果是内存溢出,则通过 调大 -Xms,-Xmx参数。
如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么久应该检查 JVM的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。
方法区溢出
(1)运行时常量池溢出
(2)方法区中保存的 Class对象没有被及时回收掉或者 Class信息占用的内存超过了我们配置。
注意 Class 要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
cglib是一个强大的,高性能,高质量的 Code生成类库,它可以在运行期扩展 Java类与实现 Java接口。
CGLIB包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类。除了 CGLIB包,脚本语言例如 Groovy和 BeanShell,
也是使用 ASM来生成 java的字节码。当然不鼓励直接使用 ASM,因为它要求你必须对 JVM内部结构包括 class文件的格式和指令集都很熟悉。
本机直接内存溢出
直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;
由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可以考虑重点排查下直接内存方面的原因。
常量池
Class 常量池(静态常量池)
在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用
字面量:给基本类型变量赋值的方式就叫做字面量或者字面值。
比如:String a=“b”,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA 在编译的时候一个每个 java 类都会被编译成一个 class文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续 JVM 类加载会具体讲到)就是为了把这个符号引用转化成为真正的地址的阶段。
一个 java类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool类,但是在编译时 People 类并不知道引用类的实际内存地址,因
此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号org.simple.Tool 替换为 Tool类的实际内存地址。
运行时常量池
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。(这个是虚拟机规范中的描述,很生涩)
运行时常量池是在类加载完成之后,将 Class 常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。
运行时常量池在 JDK1.7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。
在 JDK1.8 中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father"。变动的只是方法区中内容的物理存放位置,但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。
字符串常量池
字符串常量池这个概念是最有争议的,King 老师翻阅了虚拟机规范等很多正式文档,发现没有这个概念的官方定义,所以与运行时常量池的关系不去抬杠,我们从它的作用和 JVM 设计它用于解决什么问题的点来分析它。
以 JDK1.8 为例,字符串常量池是存放在堆中,并且与 java.lang.String 类有很大关系。设计这块内存区域的原因在于:String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
所以要彻底弄懂,我们的重心其实在于深入理解 String。
String
String 类分析(JDK1.8)
String 对象是对 char 数组进行了封装实现的对象,主要有 2 个成员变量:char 数组,hash 值。
String 对象的不可变性
了解了 String 对象的实现后,你有没有发现在实现代码中 String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。
我们知道类被 final 修饰代表该类不可继承,而 char[]被 final+private 修饰,代表了 String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。
Java 这样做的好处在哪里呢?
第一,保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
第二,保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
第三,可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字
符串变量通过 new 形式的创建,如 String str = new String(“abc”)。
String 的创建方式及内存分配的方式
1、String str=“abc”;
当代码中使用这种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。(str 只是一个引用)
2、String str = new String(“abc”)
首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String的构造函数,同时引用常量池中的"abc”字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。
3、使用 new,对象会创建在堆中,同时赋值的话,会在常量池中创建一个字符串对象,复制到堆中。
具体的复制过程是先将常量池中的字符串压入栈中,在使用 String 的构造方法是,会拿到栈中的字符串作为构方法的参数。
这个构造函数是一个 char 数组的赋值过程,而不是 new 出来的,所以是引用了常量池中的字符串对象。存在引用关系。
public class Location {
private String city;
private String region;
}
4、String str2= "ab"+ "cd"+ "ef";
编程过程中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,如果我们使用 String 对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢 例如以下代码:
分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论上来说,这段代码是低效的。
编译器自动优化了这行代码,编译后的代码,你会发现编译器自动优化了这行代码,如下
String str= "abcdef";
大循环使用
intern
String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用
1、new Sting() 会在堆内存中创建一个 a的 String对象,king"将会在常量池中创建
2、在调用 intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
3、调用 new Sting() 会在堆内存中创建一个 b的 String 对象。
4、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
所以 a 和 b 引用的是同一个对象。
以上是关于深入理解 JVM 的内存区域的主要内容,如果未能解决你的问题,请参考以下文章