JVM内存模型篇

JVM内存结构

JVM内存模型篇

Java代码执行流程

JVM内存模型篇

计算机系统指令集分为栈的指令集和寄存器的指令集。

JVM是采用栈的指令集,其与硬件耦合度小,指令集小,指令多。性能逊于寄存器指令集

JVM生命周期

1、虚拟机的启动

​ 虚拟机的启动是通过类加载器创建一个初始类实现的

2、虚拟机的执行

3、虚拟机的退出

​ 程序正常结束

​ 程序执行出现异常或错误

​ 操作系统错误导致虚拟机出现错误

类加载子系统

JVM内存模型篇

类记载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。

Class Loader只负责class文件的加载,至于是否可以运行由Execution Engine决定。

类加载器的三个阶段

加载阶段

1、通过类的全限定类名获取此类的二进制流

2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构

3、在内存区生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接阶段

1、验证

​ 确保class文件的字节流信息符合虚拟机要求,保证被加载类的正确性

2、准备

​ 为变量分配内存,并且设置该变量的默认初始值

​ 这里不包含用final修饰的static,因为final在编译时就分配了

3、解析

初始化阶段

初始化阶段就是执行类构造器的方法,比如静态代码块的赋值、类构造类方法。

类加载器分类

JVM支持两种类型加载器,分别为引导类加载器,和自定义类加载器。

  • 引导类加载器
  • 自定义类加载器

​ 使用自定义类加载器场景

1、隔离加载类

2、修改类的加载方式

3、扩展加载源

4、防止源码泄露

双亲委派机制

工作原理

1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行

2、如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,直到最顶层的加载器。

3、如果父类加载器成功加载,则返回成功。若加载失败,则让子类加载。一次递归,直到加载成功。

4、加载第三方jar包时,引导类加载器加载接口后,通过反向委托使用系统类加载器加载接口的实现类。

沙箱安全机制

如自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而应到类加载器会首先加载JDK中自带的文件报错信息 为 :没有main方法,与双亲委派机制同理

其他

在JVM中表示两个class对象是否为同一个类 ,有两个必要条件

1、类的完整类名必须一致

2、加载这个类的classLoader必须一致

类的主动使用和被动使用

主动使用和被动使用区别

被动使用并不会导致类的初始化

主动使用的情况

​ 1、创建类的实例

​ 2、访问某个类或接口的静态变量,或者对该静态遍历赋值

​ 3、调用类的静态方法

​ 4、反射

​ 5、初始化一个类的子类

​ 6、Java虚拟机启动时被表明启动类的类

运行时数据区

JVM内存模型篇

程序执行时:

​ 每个线程独立的拥有 程序计数器、本地方法栈、虚拟机栈

​ 线程共享:方法区和堆空间

程序计数器(pc寄存器)

作用:

​ pc寄存器用于存储下一条指令的地址,即将要执行的指令代码,由执行引擎读取下一条指令

常见问题

​ 1、为什么使用PC寄存器记录当前线程的执行地址?

​ 答:因为CPU需要不停的切换各个线程,切换一个线程后需要记录下次执行从哪开始。明确下次开始的地址

​ 2、 PC寄存器为什么被设定为每个线程一份

​ 答:因为每个线程是独立的,cpu执行时并发的执行多线程,切换时需要记录每个线程执行的地址。

虚拟机栈

Java的指令是基于栈的架构

栈是运行时单位,而堆是存储单位

作用:

​ 主管JAVA程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。

JVM对栈的操作只有出栈和压栈。且允许Java栈的大小是动态的或是固定不变的。

对于栈来说不存在垃圾回收,但存在异常

栈可能的异常

1、StackOverFlowError

​ 当固定栈的容量时,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量。

2、OutofMemoryError

​ 虚拟机的栈容量是可变时,尝试扩展内存但无法申请额外内存。

设置栈大小命令:`-Xss

​ 在运行时的 VMoptions 设置

栈内部的存储单位

每个线程都有自己的栈,栈中的数据都是以栈帧的形式存在。

在这个线程上执行的每个方法都对应着一个栈帧,

栈运行原理

​ 1、不同线程中所包含的栈帧是不允许存在相互引用的,即不能在一个栈帧中引用另一个线程的栈帧。

​ 2、Java方法有两种返回方式:一种是正常的return 返回,一种是抛出异常。这两种都会导致方法栈帧被弹出

栈帧的内部结构

1、局部变量表

定义:一个数字数组,主要存储方法参数和定义在方法内部的局部变量

特点:

局部变量表不涉及安全问题

局部变量表所需要的容量在编译器就确定了

局部变量表的基本存储单元是槽(Slot)

32位以内的类型占用一个槽:

64位的类型占用两个操:long double

补充说明:

​ 局部变量表中的变量是重要的垃圾回收根节点。

2、操作数栈(表达式栈)

​ 在执行方法过程中,根据字节码指令,往栈中写入数据或提取数据。

理解为方法执行过程中的一个个指令执行的临时数据

​ 栈顶缓存技术:将栈顶元素全部缓存在物理cpu寄存器中,降低对物理内存的读写

3、动态链接(指向运行时常量池的方法引用)

​ 每个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用

静态链接:当目标调用的方法在编译期间确定并始终保持不变时,调用的过程称之为静态链接

动态链接:当目标调用的方法在编译期间不能确定(接口),调用的过程称之为动态链接。

方法的调用分类

非虚方法:在编译期确定的方法。

虚方法:在编译器不能确定的方法。

4、方法返回地址

存放该方法pc寄存器的值

5、一些附加信息

可有可无

本地方法栈

介绍本地方法栈之前,需要介绍本地方法区

本地方法区

一个native method 就是一个Java调用非Java方法的接口

本地方法栈用于管理本地方法的调用。

其错误与虚拟机栈一致。

堆是每个线程共有的部分,是进程中的唯一。每个JVM实例只能有一个堆

堆中存放的几乎都是对象和数组。栈帧内部的局部变量表存放的就是指向对象或数组的引用。

在方法结束后,堆中的对象不会立即被移除,仅仅在垃圾收集时才会被移除。

堆是GC(垃圾回收)重点对象。

堆的内存结构

JVM内存模型篇

设置堆大小指令:

​ -Xms :表示堆区(新生区和老年区)的起始内存大小

​ -Xmx :表示堆区(新生代老年代)设置的最大内存

堆区的默认起始内存大小为物理内存的1/64,最大内存为物理内存的1/4。

**出错:**OOM:OutOfMemory 内存满了

新生代与老年代

其中新生代又可以被划分为Eden(伊甸园)空间、S0和S1区。

JVM内存模型篇

配置新生代老年代在堆结构的占比(一般不修改)

默认-XX:NewRatio=2,表示新生代占1、老年代占2、新生代占整个堆的1/3.

几乎所有的Java对象都是在Eden区被new出来的

对象分配过程

JVM内存模型篇

JVM内存模型篇

YGC:又称minor GC,是垃圾回收器的一种。回收Eden区和幸存者区

堆中的垃圾回收

频繁在新生区回收,很少在老年区回收,几乎不再永久区/元空间回收。

堆中的几个垃圾回收

1、Minor GC

​ 针对于新生区的垃圾回收

2、Major GC

​ 针对 老年区进行垃圾回收

3、Full GC

​ 整堆收集,收集整个Java堆和方法区的垃圾收集

方法区

尽管所有的方法区在逻辑上属于堆的一部分,但具体的实现上与堆分开。

方法区与堆一样,是线程共享的内存结构

针对于HotSpot 虚拟机而言。

JDK 1.7 以前 方法区叫做 永久代

JDK 1.8 以后 方法区叫做 元空间

使用本地内存中实现了元空间,不在占用虚拟机内存。增加

方法区的内部结构

JVM内存模型篇

方法区存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓存

类型信息

类的全限定类名

类型的直接父类的完整有效名

类的修饰符

类型直接接口的一个有序列表

类的内部属性

类的方法信息

运行时常量池

常量池:class文件中包含了常量池。常量池包含各种字面量和对类型、域和方法的符号引用。

为什么需要一个常量池呢?

将类中的各种信息以符号引用存储,运行时,加载符号对应的信息。减少class文件的大小

运行时常量池是方法区的一部分,常量池通过类加载器加载到方法区后,称为运行时常量池的引用。

其特点是具有 动态性

方法区的演变

1、首先明确,只有hotspot虚拟机才有永久代

2、hotspot虚拟机方法区的变化

  • jdk 1.6 及以前:有永久代,静态变量存放在永久代上

  • jdk 1.7 :有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除保存在堆中。

  • jdk 1.8 :无永久代,类型信息、字段、方法、常量保存在本地内存中,但是字符串常量池、静态变量仍保存在堆中。

方法区的垃圾回收

Java虚拟机规范中对方法区的垃圾回收是比较宽松的

方法区的垃圾回收主要针对两个部分:常量池中废弃的常量和不再使用的类型

常量池:当常量池的常量不用时,回收

不再使用的类型:1、该类所有的实例已经被回收

​ 2、该类的类加载器已经被回收

​ 3、该类对应的Java.lang.Class 对象在任何地方没有被引用。

对象的实例化、内存布局

JVM内存模型篇

对象创建的方式

  • new
  • Class 的newInstance() :反射的方式
  • Constructor 的newInstance(Xxxx) :反射的方式
  • clone():不调用构造器,但是要实现Cloneable()接口
  • 使用返序列化:
  • 第三方库

对象创建的步骤

1、判断对象对应的类是否加载、链接、初始化

2、为对象分配内存,

3、处理并发安全问题

4、初始化分配到空间

5、设置对象的对象头

6、执行init()方法进行初始化

对象的内存布局

JVM内存模型篇
JVM内存模型篇

对象的访问引用

思考:JVM是如何通过栈帧中的对象引用访问到其内部的实例对象呢

答:对象引用指向堆区,堆区内部的类型指针指向方法区内部的对象

执行引擎

任务是将字节码指令解释、编译为对应平台上的本地机器指令。

JVM内存模型篇

执行引擎执行PC寄存器指向操作数栈中的指令。pc寄存器指向哪,执行引擎就在哪执行。

执行引擎运行有两种方式运行代码。

解释器

将字节码文件中的内容翻译成对应的本地平台机器指令。

效率低下

即时编译器(JIT)

将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可。

速度快

为什么即时编译器执行速度快为什么还保留解释器呢?

程序运行后,解释器可以立马发挥作用,省去编译时间,立即执行。响应速度快

而JIT编译器需要将代码编译成本地机器指令。执行速度快

于是:在程序开始运行后,由解释器执行代码,提高响应速度。运行一段时间后,通过热点代码探测。将热点代码通过即时编译器解释成本地代码运行。

String Table

底层结构

String:字符串;使用双引号 “ ” 引起来表示

声明方式是final,不可被继承

jdk 1.8 String的底层实现是char[]

jdk 1.9 String的底层实现是byte[]

目的是为了节约一些空间

String 是不可变的字符序列

字符串特性

1、字符串常量池不会存储相同内容的字符串

2、String 对应的String 常量池对应的是一个固定大小的Hash Table。HashTable的特性导致了特性1

String 的内存分配

Java 6以前,字符串常量池存放在永久代

Java 7,字符串常量池存放在 Java 堆中

Java 8以后,存放在元空间中

intern() 的使用

字符串的拼接操作

1、常量与常量的拼接结果在常量池,原理是编译器优化,编译时期,直接变成拼接后的字符串并存入字符串常量池中。

2、常量池中不存在相同内容的常量

3、只要拼接时其中有个对象是变量,结果就保存在堆中。

4、如果拼接的结果调用intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
JVM内存模型篇JVM内存模型篇

效率对比

通过StringBuilder的append() 的方式添加字符串的效率远高于使用String的字符串拼接

原因:

1、StingBuilder的append()的方式:自始至终都只创建一个StingBuilder对象。

2、字符串拼接的方式:创建多个对象,一方面内存读写次数过多,一方面是可能回调用GC,占用线程运行时间

更新中。。。