进阶——深入理解JVM的内存区域

1、Java方法的运行与虚拟机栈

2、深入理解JVM的内存区域

2.1 深入理解运行时数据区

2.1.1 JVM向操作系统申请内存:

        JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给JVM,接下来JVM就进行内部分配。

2.2.2 JVM获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小

-Xms30m-Xmx30m -Xss1m-XX:MaxMetaspaceSize=30m

2.2.3 类加载

2.2.4 执行方法及创建对象:
        启动 mian 线程,执行main方法,开始执行第一行代码。此时堆内存中会创建一个order对象,对象引用 order就存放在栈中。

总结JVM运行内存的整体流程:

        JVM在操作系统上启动,申请内存,先进行运行时数据区得初始化,然后把类加载到方法区,最后执行方法。

方法得执行和推出过程在内存上得体现上就是虚拟机中栈的入栈和出栈。

同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的

3、内存溢出

栈溢出:

       HostSpot版本中栈的大小是固定的,是不支持拓展的。

       java.lang.StackOverflowError: 一般是不会出现的,除非发生了无限递归。

       OutOfMemoryError: 不断地建立线程,JVM申请栈内存,机器没有足够的内存(出现这种情况,就是机器死掉了)。

注意:

        栈区的空间JVM没有办法限制,因为JVM在运行过程中会有线程不断的运行,没办法限制,所有只限制单个虚拟机栈的大小。

堆溢出:

       内存溢出:申请内存空间,超出最大的堆内存空间。

       如果是内存溢出,通过调大 -Xms,-Xmx参数。

       如果不是内存泄漏,就是说内存中的对象确实是都是必须存活的,那就应该检查JVM堆参数的设置,与机器内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

方法区溢出:

        ① 运行时常量池溢出

        ② 方法区中保存的 Class 对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。

注意:Class要被回收,条件比较苛刻

        (1)该类所有实例都已经被回收,也就是堆中不存在该类的任何实例;

        (2)加载该类的ClassLoader已经被回收。

        (3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

本机直接内存溢出

        直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),也同样会出现OOM异常:

由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了OOM,同时Dump文件很小,可以考虑重点排查下直接内存方面的原因。

4、常量池

Class常量池(静态常量池)

运行时常量池

       运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,ta包括了若干种不同的常量:从编译期可知的数值变量到必须运行期解析后才能获得的方法或字段引用。(虚拟机规范中写的。。。)

       运行时常量池是在类加载完成之后,将Class常量池中的符号引用值转到运行时常量池中,类在解析之后,将符号引用替换成直接引用。运行时常量池在JDK1.7之后,就移到堆内存中了,物理空间是这么划分的,而逻辑上还属于方法区(方法区是逻辑分区)。

       在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,变动的只是方法区中内容的物理存放位置,但是运行时常量池和字符串常量池被移到了堆中。但是不论物理上如何存放,逻辑上还是属于方法区。

字符串常量池

       JDK1.8,字符串常量池是存放在堆中,并且与java.lang.String类有很大关系。设计这块内存区域的原因在于:String对象作为Java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效的使用字符串,可以提升系统的整体能力。

String类分析(JDK1.8)

String 对象是对char数组进行了封装实现的对象,主要有2个成员变量:char数组hash值

进阶——深入理解JVM的内存区域_第1张图片

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的创建方式及内存分配的方式

①  String str = "abc";

       当代码中使用这种方式创建字符串对象时,JVM首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种那个是可以减少同一个值得字符串对象得重复创建,节约内存。(str只是一个引用)

进阶——深入理解JVM的内存区域_第2张图片

②  String str = new String("abc");

在编译类文件时,"abc" 常量字符串将会放入到常量结构中,在类加载时,"abc"将会在常量池中创建;其次,在调用new时,JVM命令将会调用String得构造函数,同时引用常量池中的"abc"字符串,在堆内存中创建一个String对象;最后str将引用String对象。

进阶——深入理解JVM的内存区域_第3张图片

③ 使用new,对象会创建在堆中,同时赋值的话,会在堆中创建一个字符串对象,复制到堆中。

具体的复制过程是先将常量池中的字符串压入栈中,在使用String的构造方法是,会拿到栈中的字符串最为构造方法的参数。

这个构造函数是一个char数组的赋值过程,而不是new出来的,所以是引用了常量池中的字符串对象,存在引用关系。

进阶——深入理解JVM的内存区域_第4张图片

④ String str = "ab" + "cd" + "ef";

编译过程,字符串的拼接是很常见的。上边说过String对象是不可变的,如果使用String对象相加,拼接想要的字符串,会不会产生多个对象?

分析一下:首先会生成 ab 对象,在生成 abcd 对象, 最后生成 abcdef 对象,理论上讲这一行代码是低效的。但是:编译器自动优化了这行代码:String str = "abcdef";

 

你可能感兴趣的:(JVM,jvm,java)