jvm
jvm是Java Virtual Machine
的缩写,虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现。java虚拟机有自己的硬件架构,如处理器、堆栈 、寄存器等,还具有相应的指令系统。java虚拟机屏蔽了与具体操作系统平台相关的信息,使得java程序在java虚拟机上运行的代码可以在多个平台不加修改地运行。jvm是用来解析和运行java程序的
jdk、jre和jvm的关系
JDK(java development kit):java语言的软件开发包。包含java运行时环境jre
JRE(java runtime environment):Java运行时环境。包括jvm
JVM(java virtual machine):一种用于计算机设备的规范
java程序设计语言、java虚拟机、java类库统称为jdk,也是用于程序开发的最小环境
java类库api中的java se的api子集和java虚拟机两部分统称为jre,也是java程序运行的标准环境
jvm内存结构图
- 堆:存放对象实例,也是垃圾收集器管理的内存区域,也就是GC堆
- 成员变量在类中声明的是基本类型的变量时,其变量名及值是放在堆内存中的
- 声明的是引用类型的变量时,变量会存储一个内存地址,变量名及对应的对象也是存放在堆内存中的
- 方法区:用于存储被虚拟机加载好的类型信息,常量,静态变量、即时编译器编译后的代码缓存等数据
- 运行时常量池:也是方法区的一部分。class文件中的常量池表,用于存放编译期生成的各种字面量与符号引用,在类加载后存放到常量池中
- 虚拟机栈:java方法执行时,java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息
- 局部变量的类型是基本类型时,其变量名及值放在虚拟机栈中,也就是栈帧中
- 局部变量的类型是引用类型时,其存放的内存地址是放在虚拟机中的栈,所指向的对象是在堆内存中的
- 本地方法栈:为虚拟机使用到的本地(native)方法服务
- 判断:判断一个变量是堆上还是在栈上,和这个变量是基础数据类型和引用数据类型没有关系,和这个变量是局部变量、静态变量还是成员变量有关
- 程序计算器:存储的是地址,通过改变计数器的值来选择下一条需要执行的字节码指令
- 注意:一个进程存在多个线程,每一个线程都存在自己的栈和程序计算器,为了线程切换后能恢复到正确的位置上,一个进程中共用一个栈和一个方法区
- 基本数据类型中对应的是内存空间存储的是具体值
- 引用数据类型中对应的是new对象的地址
- 成员变量在堆上
- 静态变量在方法区
遇到new创建对象时,虚拟机会发生什么
1、首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过,没有的话就先执行相应的类加载过程
2、虚拟机给新生对象分配内存,将一块确定大小的内存块从java堆中划分出来
3、虚拟机将分配到的内存空间都初始化为零值,保证了对象的实例字段可以在不赋初值就直接使用
4、对对象进行设置,例如这个对象是哪个类的实例、对象的hash码等
5、对对象进行初始化,这样一个对象才被创建出来了
类加载过程
从类的生命周期看,一个类包括以下阶段
1、加载:主要完成的事情
- 通过全类名获得定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区运行时数据结构
- 在内存中生成一个代表该类的class对象,作为方法区这些数据的访问入口
2、连接
- 验证:
- 文件格式的验证:验证字节流是否符合class文件的规范
- 原数据的验证:对字节码描述的信息进行语义上的分析,保证描述的信息符合java语言规范的要求
- 字节码的验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的
- 符合引用验证:确保解析动作能正常执行
- 准备:正式为类变量设置分配内存,并设置初始值的阶段,这些内存在方法区分配
- 只对static修饰的静态变量进行内存分配,赋默认值
- 对final的静态字面值常量直接赋初值
- 解析:虚拟机将常量值中的符号引用替换为直接引用的过程,主要对类和接口,字段,类方法,接口方法,方法类型,方法权柄,方法的限定符
- 符号引用:就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
- 直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针
3、初始化:为类的静态变量赋初值
jvm的垃圾回收算法
1、标记清除算法:分为”标记“和”删除“两个阶段,首先标记出所有需要回收的对象,在标记完成之后,统一回收被标记的对象;也可以标记存活的对象,统一回收所有未被标记的对象
- 执行效率不稳定:如果java堆中包含大量对象,大部分需要被回收的,必须进行大量的标记和删除的动作,导致两个过程的执行效率随着对象的增长而降低
- 空间碎片化:标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致在运行过程中需要分配较大对象无法找到足够连续的内存而不得不提前触发另一次垃圾收集
2、标记复制算法:将可用北村按容量分为大小相等的两块,每次只是用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后把已使用过的内存空间一次清理掉
- 优点:对于多数对象都是可回收对象,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时不需要考虑空间碎片的 复杂,只要移动堆顶指针,按顺序分配即可
- 缺点:将可用的内存缩小了一半,空间浪费较多,如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
3、标记整理算法:标记过程和标记清除算法一样,但是后续不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界之外的内存
- 缺点:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
如何判断对象是否存活??
1、引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象就是不可能再被使用的
但是没有引用计数算法来管理内存,原因是需要配合大量的额外处理才能保证正确地工作,比如很难解决对象之间相互循环引用的问题
对象objA和objB都有一个字段instance,令objA.instance = objB和objB.instance = objA,实际上这两个对象已经不可能再被访问,但是因为互相引用对方,他们的引用计数不为零
2、可达性分析算法
java、c#都是通过可达性分析算法来判定对象是否存活的
- 基本思路:通过一系列称为GC roots的根对象作为起始节点集,从节点开始,根据引用关系向下搜索,搜索走过的路程被称为引用链,如果某个对象到GC roots间没有任何的引用链相连,或者就是从GC roots到这个对象不可达时,则证明这个对象是不可能再被使用的