JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java 虚拟机 虚拟机是指通过软件模拟的具有完备硬件功能的、运行在一个完全隔离的情况中的完备盘算机体系 常见的虚拟机: JVM 、 VMwave 、 Virtual Box JVM 和其他两个虚拟机的区别: 1. VMwave 与 VirtualBox 是通过软件模拟物理 CPU 的指令集,物理体系中会有许多的寄存器 2. JVM 则是通过软件模拟 Java 字节码的指令集, JVM 中只是主要保留了 PC寄存器,其他的寄存器都进行了裁剪 JVM 是一台被定制过的现实当中不存在的盘算机 1)内存地区分别
实行流程
步伐在实行之前先要把 java 代码转换成字节码( class 文件), JVM 首先需要把字节码通过肯定的方式类加载器( ClassLoader ) 把文件加载到内存中 运行时数据区( Runtime Data Area ) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操纵体系去实行,因此需要特定的下令剖析器 执 行引擎( Execution Engine ) 将字节码翻译成底层体系指令再交由 CPU去实行,而这个过程中需要调用其他语言的接口 本地库接口( Native Interface ) 来实现整个步伐的功能,这就是这 4个主要组成部分的职责与功能 总结来看, JVM 主要通过分为以下 4 个部分,来实行 Java 步伐的,它们分别是: 1. 类加载器( ClassLoader ) 2. 运行时数据区( Runtime Data Area ) 3. 实行引擎( Execution Engine ) 4. 本地库接口(Native Interface) 运行时数据区
一个运行起来的Java进程,就是一个JVM虚拟机,需要从内存中申请一块内存
JVM 运行时数据地区也叫内存布局,但需要注意的是它和 Java 内存模型( (Java Memory Model,简称JMM )完全不同,属于完全不同的两个概念,它由以下 5 大部分组成: 方法区(1.7及从前)/元数据区(1.8开始)
存储类对象(类信息、常量、静态变量,及、即编译器编译后的数据)
.class文件加载到内存后
运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用 字面量 : 字符串 (JDK 8 移动到堆中 ) 、 final 常量、基本数据范例的值 符号引用 : 类和结构的完全限定名、字段的名称和形貌符、方法的名称和形貌 堆 存储代码中 new 的对象(占据内存最大的地区) 栈 存储代码实行过程中,方法之间的调用关系 每个元素,称为一个“栈帧” 每个栈帧,代表了一个方法调用 栈帧里包罗了方法的入口,方法返回的位置,方法的形参,方法的返回值,局部变量... 步伐计数器
一块比力小的空间,主要用来存放一个“地点”,表示下一条要实行的指令在内存中的哪个地方(方法区里,每个方法里面的指令,都是以二进制的情势生存到对应类对象里)
class Test{
public void a(){}
public void b(){}
}
方法a 和方法b会被编译成二进制指令,放到 .class 文件中
实行类加载的时间,就把 .class 文件里的内容,加载起来,放到类对象中
刚开始调用方法,步伐计数器,记录的就是方法的入口的地点
随着一条一条的实行指令,每实行一条,步伐计数器的值都会自动更新去指向下一条指令
如果当前线程正在实行的是一个 Java方法,这个计数器记录的是正在实行的虚拟机字节码指令的地点; 如果正在实行的是一个 Native 方法,这个计数器值为空 本地方法栈
指的是使用 native 关键字修饰的方法
这个方法不用Java实现,而是在JVM内部通过C++代码实现的
JVM内部的C++代码的调用关系
总结:
1)虚拟机栈,步伐计数器,本地方法栈,都是每个线程都有一份
2)堆区,元数据区,在JVM进程中只有一份
一个JVM进程,可能有多个线程
每个线程,有本身的 步伐计数器 和 栈空间,这些线程共用一份 堆 和 方法区
可以说是,每个线程都有本身的私有空间
1)堆 存放new出来的对象
2)方法区/元数据区 存放类对象(类加载后存放的位置)
3)栈 存放方法之间的调用关系
4)步伐计数器 存放每个线程,下一条要实行的指令的地点
1)2)整个Java进程共用一份 3)4)每个线程都有本身的一份
常见问题
class Test{
public int n = 100;
public static int a = 10;
}
void main(){
Test t = new Test();
} n,a,t ,new Test() 处于哪个地区
Test t = new Test();
t这个变量是一个引用范例的变量,存储的是一个对象的地点,不是对象本身
new出来的对象在堆上,同时有创建了一个局部变量 Test t (引用范例的变量),把地点存到 t 里
一个变量处于哪个地区,和变量的形态密切相关
局部变量 处于 栈 上
成员变量 处于 堆 上
静态变量(类属性) 处于 元数据区/方法区 里
2)类加载的过程
基本流程
java代码会被编译成.class文件(包罗一些字节码),java步伐要想运行起来,需要让JVM读取到这些.class文件,并把里面的内容构造成类对象,生存到内存的方法去中
“实行代码”就是调用方法,需要先知道每个方法,编译后生成的指令都是什么
官方文档把类加载的过程,主要分成了五个步骤(三个步骤):
1. 加载 2. 连接 (1.验证 2.准备 3. 剖析) 3. 初始化 1)加载 找到.class文件,打开文件,读取文件内容 每每代码中,会给定某个类的“全限定类名”,例如 java.lang.String java.util.ArrayList ... JVM会根据这个类名,在一些指定的目录范围内查找 “ 加载 ” ( Loading )阶段是整个 “ 类加载 ” ( Class Loading )过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading ,所以不要把二者搞混了 在加载 Loading 阶段, Java 虚拟机需要完成以下三件事变: 1 )通过一个类的全限定名来获取界说此类的二进制字节流 2 )将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3 )在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口 2)验证
验证是连接阶段的第一步 .class文件是一个二进制的格式(某个字节具有特定的含义),需要验证当前读取的这个格式是否符合要求
3)准备
给类对象分配内存空间(终极目标是构造出类对象)
此处只是分配内存空间,并没有进行初始化,这个空间上的内存的值全都是0值
(此时类的 static 成员全是0值)
4)剖析
针对类对象中包罗的字符串常量进行处置惩罚,进行一些初始化操纵
java代码中用到的字符串常量,在编译之后,也会进入到.class文件中
剖析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
final String s = "hello";
此时,.class文件的二进制指令中,会有一个 s 如许的引用被创建出来
由于引用本质上生存的是一个变量的地点,在.class文件中(文件中不涉及到内存地点)
因此,在.class文件中,s 的初始化语句,就会先被设置成一个“文件的偏移量”,通过偏移量,就能找到“hello”这个字符串所在的位置
当这个类真正被加载到内存中的时间,再把这个偏移量,替换回真正的内存地点
在.class文件中会有一条指令,这条指令就形貌了 String s = @100(偏移量)
“hello”字符串距离文件开头的长度是100字节
把字符串的真实地点,替换成文件偏移量的过程,就是“剖析阶段”的主要工作
也叫做,把“符号引用”(文件偏移量)替换成“直接引用”(内存地点)
5)初始化
初始化阶段, Java 虚拟机真正开始实行类中编写的 Java 步伐代码,将主导权移交给应用步伐。初始化阶段就是实行类构造器方法的过程 针对类对象进行初始化,把类对象中需要的属性设置好——> 初始化static成员,实行静态代码块,加载父类... 双亲委派模型
站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器( Bootstrap ClassLoader ),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;别的一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader 属于类加载中,第一个步骤 “加载”过程中的一个环节 如果一个类加载器收到了类加载的哀求,它首先不会本身去实验加载这个类,而是把这个哀求委派给父类加载器去完成,每一个层次的类加载器都是云云,因此所有的加载哀求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈本身无 法完成这个加载哀求(它的搜索范围中没有找到所需的类)时,子加载器才会实验本身去完成加载 此处 “父子” 关系,不是继承关系 这几个 ClassLoader 里面有一个 parent 属性,指向了一个 “父加载器” 类加载器,JVM内置了三个类加载器:
1) BootStrap ClassLoader
负责的是尺度库的目录
2) Extension ClassLoader
负责的是JDK中一些扩展的库
3) Application ClassLoader
负责的是搜索项目当前目录和第三方库对应目录
步伐员也可以本身手动创造新的类加载器
类加载的过程(找.class文件的过程):
1)给定一个类的全限定类名,例如 java.lang.String
2)从 Application ClassLoader 作为入口,开始实行查找的逻辑
3)Application ClassLoader ,不会立刻去扫描本身负责的目录,而是把查找的任务,交给它的父加载器 Extension ClassLoader
4)Extension ClassLoader,也不会立刻扫描本身负责的目录,而是把查找的任务,交给它的父加载器 BootStrap ClassLoader
5)BootStrap ClassLoader,也不会立刻扫描本身负责的目录,也想把它交给本身的父加载器,结果发现它本身没有父加载器。因此,BootStrap ClassLoader只能扫描本身负责的目录
6)没有扫描到,就会回到 Extension ClassLoader,Extension ClassLoader就会扫描本身负责的目录。如果找到,就实行后续的类加载操纵,此时查找过程竣事;如果没找到,把任务交给 Application ClassLoader 实行
7)没有扫描到,就会返回到 Application ClassLoader,Application ClassLoader 会扫描本身负责的目录。如果找到,就实行后续的类加载操纵;如果没找到,就会抛出一个 ClassNotFoundException
双亲委派模型,就是一个 查找优先级 问题——>
确保 尺度库的类,被加载的优先级最高,其次是 扩展库,其次是 本身写的库和第三方库
(若本身实现了一个 java.lang.String,JVM加载的是尺度库的类)
双亲委派模型的打破:
如果本身编写一个类加载器,就不愿定要遵照上述的流程
例如Tomcat里,加载 webapp 时用的就是自界说的类加载器
只能在 webapp 指定的目录中查找,找不到就直接抛异常(不会去尺度库中查找)
优点
1. 克制重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了 2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模 型,而是每个类加载器加载本身的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么步伐运行的时间,体系就会出现多个不同的 Object 类,而有些 Object 类又是用户 本身提供的因此安全性就不能得到保证了 3)垃圾回收机制
对于步伐计数器、虚拟机栈、本地方法栈这三部分地区而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个地区的内存分配与回收具有确定性,因为当方法竣事大概线程竣事时,内存就天然跟着线程回收了。因此此处所讲的有关内存分配和回收关注的为 Java 堆 与 方法区 这两个地区 在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收 GC => 垃圾回收 GC回收的目标,是内存中的对象 对于Java来说,就是new出来的对象 栈里的局部变量,是跟着栈帧的生命周期走的(方法实行竣事,栈帧烧毁,内存释放) 静态变量,生命周期就是整个步伐,始终存在,意味着静态变量是无需释放的 真正要需要GC释放的 堆 上的对象 垃圾的判定算法
在GC中,有如下两个主流的方案:
1. 引用计数(Python,PHP)
new出来的对象,单独安排一块空间,来生存一个计数器
{
Test t = new Test();
Test t2 = t1;
}
出{}之后,t1和t2就烧毁了,引用计数就为0了
生存引用计数,形貌有这个对象有几个引用指向它
在Java中,使用对象,必须依靠引用
如果一个对象,没有使用引用指向,就可以视为 垃圾 了(引用计数为0)
缺点:
1)比力浪费内存
如果对象很小,计数器占据的空间就难以忽视了
2)存在“循环引用”问题
class Test{
public Test t;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;
此时,a 和 b两个引用已经烧毁了
new出来的两个对象,无法被其他的代码访问到,但是它们的引用计数不是0,所以不能被回收
此时,第一个对象,引用了第二个对象;第二个对象,引用了第一个对象——>雷同死锁
2. 可达性分析
本质上是时间换空间
有一个/一组线程,周期性的扫描代码中的对象
从一些特定的对象出发,尽可能的进行访问的遍历,把所有能访问到的对象,都标志成“可达”;反之,经扫描后,未被标志到的对象,就是 垃圾
此处的遍历大概率是N叉树,看访问的某个对象,里面有多少个引用范例的成员,针对每个引用范例的成员都需要进行进一步的便利
此处的可达性分析都是周期性的——>可达性分析比力斲丧体系资源,开销比力大
垃圾回收算法
三种基本思绪
1)标志清除
总的空闲空间是 2MB,但是申请空间时,只能申请 <= 1MB的空间
把对应的对象,直接释放掉——>会产生许多“碎片”
释放内存,目标是让别的代码能够申请
申请内存,都是申请到“一连”的内存
2)复制算法
把内存分成两份,一次只用其中的一半
通过复制的方式,把有效对象归类到一起,再同一释放剩下的空间
这个方案可以解决内存碎片的问题,但缺点依然很明显:
1. 内存要浪费一半,使用率不高
2. 如果有效对象许多,拷贝开销很大
3)标志整理
既能解决内存碎片的问题,又能处置惩罚重复算法中使用率问题
雷同于序次表删除元素的搬运操纵——>搬运的开销依然很大
详细实现
伊甸区 存放刚 new 出来的对象
履历规律:从对象诞生开始,到第一轮可达性分析的过程中,固然时间不长(每每就是毫秒—秒),但是,在这个时间段内,大部分的对象都会变成垃圾
1)伊甸区—>幸存区 复制算法
每一轮GC扫描之后,都会把有效对象复制到幸存区中,伊甸区就可以整个释放了
由于履历规律,真正需要复制的对象不多,非常得当复制算法
幸存区 分成大小相称的两块,每次只用一块(复制算法的表现)
2)GC扫描线程也会扫描幸存区,会把活过GC扫描的对象(扫描过程中可达)拷贝到幸存区的另一个部分
幸存区之间的拷贝,每一轮会拷贝多个对象,每一轮也会淘汰多个对象(有些对象随着时间的推移,就成垃圾了)
3)当这个对象已经在幸存区存活过许多轮GC扫描后,JVM就认为这个对象,短时间内应该是释放不掉了,就会把这个对象拷贝到 老年代
4)进入老年代的对象,也会被GC扫描,但是频率会比新生代低得多——>减少GC扫描的开销
履历规律,新生代的对象更轻易成为垃圾,老年代的对象更轻易存活
分代回收——> 对象活过的GC扫描越多,就越老 新生代,主要使用 复制算法 老年代,主要使用 标志整理 分代回收,是JVM中主要的回收的思想方法 但是在垃圾回收详细实现的时间,可能会有一些调整和优化
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |