JVM内存模型篇
JVM内存结构
Java代码执行流程
计算机系统指令集分为栈的指令集和寄存器的指令集。
JVM是采用栈的指令集,其与硬件耦合度小,指令集小,指令多。性能逊于寄存器指令集
JVM生命周期
1、虚拟机的启动
虚拟机的启动是通过类加载器创建一个初始类实现的
2、虚拟机的执行
3、虚拟机的退出
程序正常结束
程序执行出现异常或错误
操作系统错误导致虚拟机出现错误
类加载子系统
类记载器子系统负责从文件系统或者网络中加载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虚拟机启动时被表明启动类的类
运行时数据区
程序执行时:
每个线程独立的拥有 程序计数器、本地方法栈、虚拟机栈
线程共享:方法区和堆空间
程序计数器(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(垃圾回收)重点对象。
堆的内存结构
设置堆大小指令:
-Xms :表示堆区(新生区和老年区)的起始内存大小
-Xmx :表示堆区(新生代老年代)设置的最大内存
堆区的默认起始内存大小为物理内存的1/64,最大内存为物理内存的1/4。
**出错:**OOM:OutOfMemory 内存满了
新生代与老年代
其中新生代又可以被划分为Eden(伊甸园)空间、S0和S1区。
配置新生代老年代在堆结构的占比(一般不修改)
默认-XX:NewRatio=2,表示新生代占1、老年代占2、新生代占整个堆的1/3.
几乎所有的Java对象都是在Eden区被new出来的
对象分配过程
YGC:又称minor GC,是垃圾回收器的一种。回收Eden区和幸存者区
堆中的垃圾回收
频繁在新生区回收,很少在老年区回收,几乎不再永久区/元空间回收。
堆中的几个垃圾回收
1、Minor GC
针对于新生区的垃圾回收
2、Major GC
针对 老年区进行垃圾回收
3、Full GC
整堆收集,收集整个Java堆和方法区的垃圾收集
方法区
尽管所有的方法区在逻辑上属于堆的一部分,但具体的实现上与堆分开。
方法区与堆一样,是线程共享的内存结构
针对于HotSpot 虚拟机而言。
JDK 1.7 以前 方法区叫做 永久代
JDK 1.8 以后 方法区叫做 元空间
使用本地内存中实现了元空间,不在占用虚拟机内存。增加
方法区的内部结构
方法区存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓存
类型信息
类的全限定类名
类型的直接父类的完整有效名
类的修饰符
类型直接接口的一个有序列表
类的内部属性
类的方法信息
运行时常量池
常量池:class文件中包含了常量池。常量池包含各种字面量和对类型、域和方法的符号引用。
为什么需要一个常量池呢?
将类中的各种信息以符号引用存储,运行时,加载符号对应的信息。减少class文件的大小
运行时常量池是方法区的一部分,常量池通过类加载器加载到方法区后,称为运行时常量池的引用。
其特点是具有 动态性
方法区的演变
1、首先明确,只有hotspot虚拟机才有永久代
2、hotspot虚拟机方法区的变化
-
jdk 1.6 及以前:有永久代,静态变量存放在永久代上
-
jdk 1.7 :有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除保存在堆中。
-
jdk 1.8 :无永久代,类型信息、字段、方法、常量保存在本地内存中,但是字符串常量池、静态变量仍保存在堆中。
方法区的垃圾回收
Java虚拟机规范中对方法区的垃圾回收是比较宽松的
方法区的垃圾回收主要针对两个部分:常量池中废弃的常量和不再使用的类型
常量池:当常量池的常量不用时,回收
不再使用的类型:1、该类所有的实例已经被回收
2、该类的类加载器已经被回收
3、该类对应的Java.lang.Class 对象在任何地方没有被引用。
对象的实例化、内存布局
对象创建的方式
- new
- Class 的newInstance() :反射的方式
- Constructor 的newInstance(Xxxx) :反射的方式
- clone():不调用构造器,但是要实现Cloneable()接口
- 使用返序列化:
- 第三方库
对象创建的步骤
1、判断对象对应的类是否加载、链接、初始化
2、为对象分配内存,
3、处理并发安全问题
4、初始化分配到空间
5、设置对象的对象头
6、执行init()方法进行初始化
对象的内存布局
对象的访问引用
思考: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() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
效率对比
通过StringBuilder的append() 的方式添加字符串的效率远高于使用String的字符串拼接。
原因:
1、StingBuilder的append()的方式:自始至终都只创建一个StingBuilder对象。
2、字符串拼接的方式:创建多个对象,一方面内存读写次数过多,一方面是可能回调用GC,占用线程运行时间
更新中。。。