浅谈JVM(上)

一:JVM的位置所在

img

 

JVM的位置是在操作系统和java程序之间,所以JVM并不是直接操作底层硬件系统的。所以插入一个话题,如果在新买的电脑上有java的环境,那应该已经是被用过的,除非商家安装的java环境,但是这种几率应该不高,所以说大概率应该是被坑了,别人用过的电脑(可以在cmd中,使用命令java -verison来查看)。

1.1:JVM的基本概念

JVM是可运行Java的假想计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法。JVM是运行在操作系统之上的,它与硬件没有直接的交互。

1.2:JVM运行的过程

java源文件------->编译器--------->字节码文件------------>JVM-------------->机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够 跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不 能共享。

1.3:java线程与原生操作系统的线程

JVM允许一个应用并发执行多个线程。HotSpot JVM中的Java线程与原生操作系统系统有直接的映射关系。当本地存储,缓冲区分配,同步对象,栈,程序计数器等准备好以后,就会创建一个操作系统原生系统。java线程结束,原生线程随之被回收。操作系统负责调度所有的线程,并把他们分配到任何可用的CPU上。当原生线程初始化完毕后,就会调用java线程的run()方法。当线程结束时,会释放原生线程和java线程的所有资源。

Hotspot JVM后台主要的系统线程主要有以下几个:

虚拟机线程:这个线程等待JVM到安全点出现,这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。
周期性任务线程:主要负责定时器时间(也就是中断)用来调度周期性操作的执行。
GC线程:垃圾回收的机制
编译器进程:这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程:这个线程接受到JVM的信号并调用适当的JVM方法处理

 

二:JVM的内存结构

 

img

 

(1)首先将.java源程序编译成.class文件(二进制)

(2) 然后将.class文件加载到类加载器中,这个class文件可能是本地的.class文件,也可能是从网络上获取过来的.class文件(实现序列化接口)

(3) 类加载器对class文件进行加载=>连接=>初始化的操作

(4) 下面则是到了JVM的运行数据区中(堆,栈,方法区,本地方发栈以及程序计数器)

类加载器(ClassLoader):附一张图

img

 

 

1:加载:是将本地的.class文件(或者网络节点的.class文件)中的二进制读入到内存中,把它放到运行时数据区的方法中,然后再堆区创建一个Class对象,用于封装类在方法区中的数据结构。class封装了类在方法区中的结构,并且向java程序提供了类在方法区中的数据结构接口。类的加载是由加载器完成的,加载器有:启动类加载器、扩展类加载器、系统类加载器、用户自定义类加载器。这里就牵扯到了java的双亲委派机制。

双亲委派机制:当某个类加载器需要加载某个.class文件,他首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

类加载器的类别:

(1)BootstrapLoader(启动加载器)c++编写,加载java核心库 java.*,构造ExtClassLoaderAppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

(2)ExtClassLoader (标准扩展类加载器)java编写,加载扩展库,如classpath中的jrejavax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。 ``

(3)AppClassLoader(系统类加载器)java编写,加载程序所在的目录,如user.dir所在的位置的class

(4)CustomClassLoader(用户自定义类加载器)java编写,用户自定义的类加载器,可加载指定路径的class文件

 

作用: 1:防止重复加载同一个.class,通过委托到上面问一问,加载过了,就不用在加载一遍。保证数据安全。

2:保证核心.class不被篡改,通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

 

2:连接:

  • 验证:保证被加载的类的正确性;

  • 准备:给类静态变量分配内存空间,赋值一个默认的初始值;

  • 解析:把类中的符号引用转换为直接引用

3:初始化:给类的静态变量赋值正确的值。

 

运行时数据区:

(1)本地方法栈(线程私有):是为本地方法服务(native)

(2)虚拟机栈(线程私有):是描述java方法执行的内存模型,每个方法在执行的同时对创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口信息等。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本数据类型(boolean,byte,char,int,short,int,float,long,double)对象的引用(reference类型)。

操作数栈: 和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

动态链接:每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。

C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。

当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

方法出口信息:方法的返回值信息等,例如return

 

(3):是被线程共享的一块区域,创建的对象和数组都保存在java内存堆中,也是垃圾收集器进行垃圾收集的最重要的区域。现在JVM主要采用分代收集算法。

(4)程序计数器(线程私有):线程私有,当前线程执行字节码的行号指示器,正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是native方法则为空。

(5)方法区:Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!

JDK1.7之前:永久代是用于存储一些虚拟机加载类信息,常量,字符串,静态变量等等,永久代是有限制的,如果满了会报OutofMemoryError:PerGen

JDK1.8之后:彻底将永久代移除HotPot Jvm,java Heap 中或者MetaSpace(Native Heap)空间

元空间就是方法区在 HotSpot jvm 的实现;

方法区重要就是来存:类信息,常量,字符串、静态变量、符号引用、方法代码。。。。。。

元空间和永久代,都是对JVM规范中方法区的实现。

元空间和永久代最大的区别:==元空间并不在Java虚拟机中,使用的是本地内存!==

    -XX:MetasapceSize10m

运行时常量池:是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一个常量池,用于存放编译器生成的各种字面和符号引用,这部分内容将在类加载后存房到方法区的运行时常量池中。

 

参考链接:

https://www.liangzl.com/get-article-detail-132885.html

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/clover-forever/p/12492255.html