JAVA虚拟机,
JAVA虚拟机,
JAVA虚拟机
一:JAVA虚拟机
JAVA虚拟机也叫JVM(java virtual machine),是抽象的计算机,也就是模仿计算机的
各种功能来实现的,JAVA虚拟机有自己的堆栈、寄存器、指令代码。
二:运行原理
首先JAVA编译器javac将JAVA源文件(.JAVA文件)编译成字节码文件(.class文件),
然后由类加载器将.class文件加载到虚拟机中,分配给jvm的内存区域,jvm解释器
解释.class文件成本地计算机CPU的机器码,最后由计算机CPU执行这些机器码。
加载过程:
模式是委派模式(保证核心库安全,比如在用Object时,最开始没有加载,在后面
的加载过程中很可能存在重复的版本,所以统一加载出来然后调用,保证了核心库的安全,使
用的安全)
使用Classloder类加载器加载到jvm中。
类加载器是一种层级关系的提现:
bootstrap classloader
(原始类(引导)加载器,加载java核心类,是用c/c++写的
JRE\lib下的包以及选项指定的jar包)
|
extension classloader
(扩展类,加载java的扩展目录
JRE\lib\ext下的包)
|
system classloader
(应用程序类,一般来说,Java 应用的类都是由它来完成加载的。)
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存
起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会
尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一
次,即 loadClass方法不会被重复调用。
全盘负责机制:若类A调用了类B,则类B和类B所引入的所有jar包,
都由类A的类加载器统一加载。
委托机制:类加载器在加载类A时,会优先让父加载器加载,当父加载
器加载不到,再找父父加载器,一直找到bootstrap,
classloader都找不到,才自己去相关的路径去寻找加载。
后两个ExtClassLoader AppClassLoader都是由Bootstrap
Loader 所加载
1,判断是否已经加载过,在cache里面查找,若有,跳7;否则下一步
2,判断当前加载器是否有父加载器,若无,则当前为bootstrap
classloader,跳去4;否则下一步
3,请求父加载器加载该类,若加载成功,跳7;若不成功,即父加载器不能
找到该类,跳2
4,请求jvm的bootstrap classloader加载,若加载成功,跳7;若失
败,跳5
5,当前加载器自己加载,若成功,跳7;否则,跳6
6,抛出ClassNotFoundException
7,返回Class
总结来说就是:
先检测class是否已经加载,如果已经加载,直接返回Class;
如果没有加载,考虑父节点,加载,直至自己,加载成功返Class,
否则返回ClassNotFoundException异常。字节码由执行引擎来执
行
当虚拟机启动后,会为虚拟机分配一块内存空间,jvm在运行时将数据划分
为5个区域来存储:
一:PC寄存器(程序计数器):
程序计数器存储每个线程下一步将要执行的jvm指令,在任何一个确定的时
刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需有一个独立的程序
计数器,各条线程之间的计数器互不影响,如果是本地方法就不在PC寄存器中存储
任何信息。可以看作是当前线程所执行的字节码文件(class)的行号指示器。在
虚拟机的世界中,字节码解释器就是通过改变计数器的值来选取下一条执行的字节
码指令,分支、循环、跳转、异常处理、线程恢复都需要它来实现,这个区域没有
内存溢出等异常情况。
2:JVM栈:
每个线程创建的同时都会创建栈,栈与程序计数器一样,也是线程私有的,每
个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链
接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧
在虚拟机栈中从入栈到出栈的过程。
栈中存放基本类型的变量原始值,引用类型对象存放的是堆中的对象的引用,
而不是存放的对象本身。这个区域当线程请求的栈深度大于虚拟机所允许的深度,
将抛出StackOverflowError 异常,这个异常是由于当前线程的栈满了,比如死
递归等,如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出
OutOfMemoryError 异常。
JVM栈解决的是运行事的问题,就是程序如何执行或者数据如何处理,代表了
程序处理逻辑。
栈有一个很重要的特殊性,就是存在栈中的数据可以共享。例:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有
没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指
向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3
这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同
时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个
对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会
导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b
的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;
时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;
如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
3:java堆:
是Java 虚拟机所管理的内存中最大的一块,由虚拟机启动时创建,是被所有
线程共享的一块区域,堆用来存放new 关键字对象实例以及数组对象实例,如果没
有栈指针指向,就会在一段时间后被GC回收,堆有新生代、旧生代之分,新产生的
对象放到新生代,在程序中经过几个周期GC没有回收到的和缓存的对象都放到旧生
带,这个区域也会抛出内存溢出,当内存不足以完成实例分配的时候。
JVM堆解决的问题是如何处理数据,解决数据存储问题。
4:方法区:
方法区也是所有线程共享的一块区域,存放加载类的信息
1:完整有效名
2:直接父类的完整有效名
3:修饰符
4:直接接口的一个有序列表
5:域(Field)信息
6:方法(Method)信息
7:除了常量外的所有静态(static)变量
8:类的常量池
方法区有三个常量池
类的常量池
jvm为每个已加载的类型都维护一个常量池。常量池在数据编译期间(编
译成class)就被创建了,常量池就是这个类型用到的常量的一个有序集合包
括代码中所定义的各种基本类型、对象型的常量值,和对类型、域、方法的符
号引用。
运行时常量池
我们知道,Class文件中除了有类的版本、字段、方法、接中等描述信
息外,还有一项信息是常量池:用于存放编译期生成的各种常量字面量和符号
引用。而这些字面量和符号引用会在类加载后存放到方法区的运行时常量池
中,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置Class
文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常
量放入池中
字符窜常量池
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优
化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,
每当代码创建字符串常量时,JVM会首先检查字符串常量池。
创建字符串有两种方式:
1:采用字面值的方式赋值
String str = "aaa";
使用这种方式创建,JVM首先会去字符串池中查找是否存在"aaa"这个
对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将中"aaa"这
个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字
符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址
返回,赋给字符串常量,也就只创建了一个对象。
2:采用new关键字新建一个字符串对象
String str = new String("aaa");
String str1 = new String("aaa");
两句代码,第一句创建了两个对象,第二句创建了一个对象。
JVM首先会在字符串池中查找aaa,如果不存在aaa,就先在字符串池中创建
一个aaa对象,因为是new,同时也会在堆上创建一个aaa对象,然后把堆上
的aaa对象地址返回赋值给引用str,所以创建了两个对象。然后jvm继续去
字符串池中查找aaa对象,发现存在,不创建,但是是new,所以还要在堆上
创建一个aaa对象,然后返回堆上的aaa对象地址返回赋值给引用str1,创建
了一个对象。
很多人愿意把方法区称为“永久代”(Permanent Generation),本质上
两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展
至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA
JRockit、IBM J9 等)来说是不存在永久代的概念的。GC对方法区的的收集主
要是针对常量池的回收和对类型的卸载,以上所述都是JDK1.7、1.8之前的版本,
JDK1.7常量池(永久区)还是存在,只是转移了部分到了本地内存和java堆,比如
符号引用转移到了本地堆,字面量(internedstrings)转移到了Java heap;
类的静态变量(classstatics)转移到了ava heap。从jdk1.8开始,永久区被
删除,而是使用元空间,元空间不再虚拟机中,在本地内存,因此,元空间的大小
受制本地内存的限制。上述虚拟机为Hotspot 虚拟机。
5:本地方法栈:
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的
作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方
法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的
Native方法服务。
三:生命周期(类加载机制)
一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
1: 装载
通过该类型的完全限定名,产生一个代表该类型的二进制数据流;解析二进制数据流为方法区的内部数据结构,创建一个表示该类型的java.lang.Class类的实例
2:验证
确认java类型数据格式正确且适于java虚拟机使用
3:准备
准备阶段是正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里所说的初始值“通常情况”是数据类型的零值,假如:
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行
4:解析
在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类
的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
5:初始化
对类的静态变量,静态代码块执行初始化操作
6:使用
7:卸载
相关文章
- 暂无相关文章
用户点评