堆空间的分代划分

堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Survivor和To Survivor组成。

从底层深入理解理解运行时数据区

 

GC概念

Grabager Collection 垃圾回收器,在JVM中时自动化的垃圾回收机制,我们一般不需要去关注。在JVM中GC的重要区域是堆空间。我们也可以通过一些主动方式去让他主动发起。System.gc()。项目中不要去用。

 

JHSDB工具介绍

JHSDB工具是一款基于服务性代理实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言实现的API集合。

从底层深入理解理解运行时数据区

通过这段代码,实用工具可查看T1对象所在堆的位置与T2对象所在堆的位置不同。(T1经过15次垃圾回收进入了老年代,T2刚刚进入堆内存,处于新生代)。具体使用细节此处不详细讲解。

 

虚拟机内存优化技术

栈优化技术-------栈帧之间数据的共享

在一般的模型中。两个不同的栈帧一般是独立的存在,但是大部分JVM 在实现的过程中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起。这样做不但节约了部分空间,更加重要的是在方法调用的时候就可以直接共用一部分数据。无需进行额外的参数辅助传递。

以以下程序为例:

从底层深入理解理解运行时数据区

从底层深入理解理解运行时数据区

内存溢出

栈溢出

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

Java.lang.StackOverflowError(单个虚拟机栈)一般普通的方法调用是很难出现的,如果遇到了可能写了无限递归。

虚拟机栈带给我们的启示:方法的执行因为要打包成栈帧。所以天生比面向过程的简单循环要慢。所以树的遍历算法中递归和非递归(未封装,直接循环来实现)都有其存在的意义。递归的代码更简洁,非递归代码复制但速度快。

栈内存的OOM(此处指的整个运行时数据区的栈空间)的发生条件是:不断地创建线程,JVM会向操作系统不断地申请栈内存,机器没有足够的内存,就会导致直接死机。

 

注:(1)在java语言中,当创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象,同时创建一个操作系统线程,而这个操作系统线程的内存不是JVMMemory,而是系统中剩下的内存(直接内存,堆外内存)

 

扩展:(2)当你使用JAVA线程,JVM内会创建一个Thread对象。但是同时也会在操作系统里创建一个真正的物理线程(参考JVM规范),操作系统会在剩下的内存中创建这个物理线程,而不是在JVM中。因此想要更多的创建线程,还要预留充足的堆外内存。

从底层深入理解理解运行时数据区

从底层深入理解理解运行时数据区

 

堆溢出(内存重点)

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

如果是内存溢出,则通过调整堆内存参数解决。

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

 

方法区溢出

  1. 运行时常量池溢出(在JDK6之前的永久代中,常量池会出现溢出情况。随着永久代更替为元空间后,我们的方法区常量池移动到了堆内存。因此常量池溢出的情况已经很少见了
  2. 方法区中保存的Class对象没有及时被回收掉,或者Class信息占用的存储超过我们的配置。

  注:回收class的条件非常苛刻。

  1. 该类的所有实例都已经被回收,也就是堆内存中不再存在任何该类的实例
  2. 记载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

 

本机直接内存溢出

直接内存不是JAVA虚拟机规范中定义的内存区域。在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存(本地方法栈干的事),然后通过抑制存储在JAVA堆中的DirectByteBuffer对象对这块内存的引用进行操作。这样就能在一些场景中显著提升性能,避免了Java堆和Native堆中来回复制数据。

  1. 本机直接内存的分配不会受到Java堆大小的限制,收本机总内存大小的限制。
  2. 直接内存的大小可以用参数设置
  3. 直接内存申请空间耗费更高的性能
  4. 直接内存IO读写的性能要优于普通内存

 

当我们需要频繁访问大的内存而不是申请和释放空间的时候,通过使用直接内存可以提高性能。

 

使用UnSafe类可以不断地对直接内存申请空间,最终会OOM。

 

注意:由于申请直接内存不由虚拟机管理,所以由此导致的OOM是不会在Heap Dump文件中看出明显异常。当OOM后发现Dump文件很小同时程序直接或间接的使用了NIO,可以考虑是否是直接内存溢出。

 

Unsafe

 

JDK1.9以后不用了

 

 

常量池

Class常量池(静态常量池)

在class文件中,除了有类的版本,字段,方法和接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期的字面量和符号引用。(此处指的常量池并不是我们平时所说的运行时数据区的内存,而是在java文件编译成class文件时,存放在class文件中的内存区域)

从底层深入理解理解运行时数据区

 

字面量

给基本类型或String类型变量赋值的方式就叫做字面量或者字面值(不包括对象)。

比如String a = “b”,这里的b就是字面量,类似的还有int a=1,1是字面量等。

符号引用

符号引用以一组符号来描述所引用的目标。符号引用可是任何形式的字面量,JAVA在编译的时候会被编译成一个class文件,但在编译的时候虚拟机并不知道引用类的实际地址(此时的类就是一个名字,名字可以用任意字面量表示。)在类解析阶段必须根据这个名字找的到类的实际地址从而转为直接引用。因此符号引用存在的意义就是可以根据这个符号找到类真正的地址。(简单来说这个地址符号引用是类加载阶段,我们的需要根据符号引用class文件的数据,然后再分配好内存后转为直接引用。直接引用才是我们运行时数据区需要用到的引用)。

 

运行时常量池(存储类型信息,常量,静态变量)

 

运行时常量池是每一个类或者接口在JVM运行时的表现形式。它包括了若干不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

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

在JDK1.8之后,使用元空间代替永久代来实现方法区(元空间直接操作直接内存)。但是逻辑上元空间依然属于方法区。

 

字符串常量池

字符串常量池是最具有争议的常量池。很多人感觉字符串常量池属于运行时常量池(包括本人),但是在很多资料上说这其实是分开的,官方没有给明确文档。但是我们应该从JVM设计它用于解决什么问题的角度来学习它。

String对象在Java语言中占据非常大的使用比例,因此特殊处理String的内存区域,可以提升系统整体性能。

 

String

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

 

 

String对象的不可变性

从底层深入理解理解运行时数据区

由图可见,String类被final修饰,char数组也被final修饰。类被final修饰代表该类不可继承,而char[]被final+private修饰,代表了String对象不可更改。

 

好处:1.保证String对象的安全性,假设String对象是可变的,那么String对象将被恶意修改。

  1. 保证hash值不会频繁更改,确保了唯一性,使得类似HashMap容器才能实现的key-value缓存功能。
  2. 可以实现字符串常量池,在Java中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如:String str =”abc”,另一种是字符串变量通过new形式创建,如String str = new String(“abc”);

 

String 的创建方式以及内存分配的方式

  • string str = “abc”;

从底层深入理解理解运行时数据区

当代码中使用这种方式创建字符串对象时,JVM首先会检查该对象是否在字符串常量池中。如果,就返回已存在的地址给引用,否则就在字符串常量池中创建新的字符串。这种方式可以减少一个字符串对象的重复创建,节约内存。

从底层深入理解理解运行时数据区

 

 

  • String str = new String(“abc”);

首先在编译类问件时,”abc”字符串常量会被放入常量结构中。在类加载时,”abc”将会在常量池中创建。其次,在调用String构造函数时,同时引入常量池中的”abc”字符串,在堆内存中创建一个String对象。最后str将引用String对象。

new String的对象会创建在堆中。同时赋值的话,会在常量池中创建一个字符串对象,再把该对象复制到堆中。具体复制过程是先将常量池中的字符串压入栈中,再使用String的构造方法时,会拿到栈中的字符串作为构造方法的参数。这个构造函数是一个char数组的赋值过程,而不是传统意义的new。所以是引用了常量池中的字符串对象和。存在引用关系。

从底层深入理解理解运行时数据区

 

 

  • String str = “ab”+”cd”+”ef”;

编译过程中,字符串拼接很常见。前面所说String对象时不可变的,如果我们使用String相加,是否会产生多个String呢?

分析代码结论:首先生成ab对象,然后abcd对象,最后生成abcdef对象。这段代码是低效的。所以编译器用StringBuilder做了自动优化。

从底层深入理解理解运行时数据区

 

  1. Intern

String的inten方法,在new String对象后使用。如果字符串常量池中有这个字面量,则不new直接返回给引用,否则new一个对象.

从底层深入理解理解运行时数据区

从底层深入理解理解运行时数据区

可通过以下代码深入了解。

相关文章: