深刻理解 JVM 的内存区域

深刻理解运行时数据区

代码示例:html

1. JVM 向操做系统申请内存java

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

2. JVM 得到内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小编程

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

3. 类加载(类加载的细节后续章节会讲):缓存

  这里主要是把 class 放入方法区、还有 class 中的静态变量和常量也要放入方法区安全

4. 执行方法及建立对象oracle

  启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会建立一个 对象,对象引用  就存放在栈中框架

  后续代码中遇到 new 关键字,会再建立一个对象,对象引用  就存放在栈中ide

 

 

总结一下 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 要被回收条件比较苛刻(仅仅是能够不表明必然由于还有一些参数能够进行控制):

一、该类全部的实例都已经被回收,也就是堆中不存在该类的任何实例。

二、加载该类的 ClassLoader 已经被回收

三、该类对应的 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 的建立方式及内存分配的方式

 

1String str=abc”;

 

当代码中使用这种方式建立字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,若是在,就返回该对象引用,不然新的字符串将在常量池中被建立。这种方式能够减小同一个值的字符串对象的重复建立,节约内存。(str 只是一个引用)

 

 

2String str = new String(abc)

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

 

 

三、使用 new,对象会建立在堆中,同时赋值的话,会在常量池中建立一个字符串对象,复制到堆中。

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

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

public class Location {

  private String city;

  private String region;

}

 

 

 

 

 

4String str2= "ab"+ "cd"+ "ef";

编程过程当中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,若是咱们使用 String 对象相加,拼接咱们想要的字符串,是否是就会产生多个对象呢    例如如下代码:

分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论上来讲,这段代码是低效的。

编译器自动优化了这行代码,编译后的代码,你会发现编译器自动优化了这行代码,以下

String str= "abcdef";

大循环使用

 

 

intern

String  intern 方法,若是常量池中有相同值,就会重复使用该对象,返回对象引用

 

 

一、new Sting() 会在堆内存中建立一个 a的 String对象,king"将会在常量池中建立

二、在调用 intern方法以后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

三、调用 new Sting() 会在堆内存中建立一个 b的 String 对象

四、在调用 intern 方法以后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

因此 a  b 引用的是同一个对象

相关文章
相关标签/搜索