JVM虚拟机,Java头等功臣!

Java概述

Java概述

图片.png 排名变动暂且不论,Java语言在编程中的地位常年蝉联前三,被称为服务器端最好的编程语言;

Java 是 Sun Microsystems 于 1995 年首次发布的一种编程语言和计算平台。是一种面向对象的编程语言,包括以下特性。

  1. 平台独立性

Java 应用程序被编译成字节码,存储在类文件中并加载到 JVM 中。由于应用程序在 JVM 中运行,因此它们可以在许多不同的操作系统和设备上运行。 2. 面向对象 Java 是一种面向对象的语言,它采用了 C 和 C++ 的许多特性并对其进行了改进。 3. 自动垃圾收集 Java 自动分配和释放内存,因此程序不会承担该任务。 4. 丰富的标准库 Java 包含大量可用于执行诸如输入/输出、网络和日期操作等任务的预制对象。

Java语言的跨平台的特性头等功臣就是----JVM,只要平台装了jvm,就可以运行java程序,今天我们就来了解一下jvm;

JVM的整体组成

我们知道java会编译成class文件, jvm需要类加载器去加载class文件到运行时数据区, 但是class文件只是一套jvm的指令规范,是不能够直接执行的,所以需要执行引擎去解析为底层系统指令交给cpu执行,在这个过程中需要调用其他语言接口,也就是本地库接口来实现程序的整体功能, 所以jvm组成大致分为三部分 ​

  1. 类加载器
  2. 运行时数据区
  3. 执行引擎

图片.png

类加载器

首先让我们看看类加载器,类加载器顾名思义就是读取.class文件把类加载到 jvm 中,转换成一个实例存储在(方法区)元空间中. 类的完整生命周期包含了: 加载>验证>准备>解析>初始化>使用>卸载

图片.png 而我们说的类加载过程正是前面五部分

图片.png

  1. 加载:

当使用一个类时,会根据全限定名去硬盘上加载这个类的字节码文件,在方法区解析生成运行时数据,即instanceKlass实例,在堆生成代表该类的Java.lang.class对象,即instanceMirrorKlass实例,暴露给程序使用,也就是代码中使用的对象 2. 验证: 验证字节码文件是否符合JVM规范,文件格式验证/元数据验证/字节码验证/符号引用验证 3. 准备: 为静态变量分配内存、赋初值, 比如int类型的初值会被设为0; 4. 解析: 把符号引用转为直接引用(内存地址引用) 5. 初始化: 为静态变量或者静态代码块赋值 (准备赋初值.此处真正赋值)

首先了解一下类加载器的分类

图片.png 如图: JVM为我们提供了三大类加载器,分别对应不同的职责

启动类加载器Bootstrap ClassLoader:

是由C/C++编写的,负责加载JDK核心类库 ,只加载特定包名的类,如java、javax、sun

拓展类加载器Extension ClassLoader:

负责加载扩展类库,如JAVA_HOME/jre/lib/ext/下的所有jar包,放入这个目录下的jar包对应用程序类加载器是可见的,因为他们是逻辑上的父子关系,需要处理委托机制;

应用程序类加载器Application ClassLoader:

也称系统类加载器,负责在JVM启动时,加载来自当前应用中的类或者CLASSPATH操作系统属性所指定路径的jar

当然我们也可以自定义类加载器

图片.png 不同的类加载加载的类是不相同的:

图片.png 如果是不同的加载器加载同一个类,那么这个类显然是不相同的,因为类加载器加载的类的存储空间是不一样的,加载器在方法区按照加载器的类型分开存储,所以同一个类可能会被加载到好几个地方. 由此我们也就知道了双亲委派机制的重要性

双亲委派机制

上面我们知道,各种类加载器之间存在逻辑上的父子关系:

BootStrapClassLoader>ExtensionClassLoader>ApplicationClassLoader

这就为偷懒提供了便利: 有活我先不干, 看老父亲去不去干,他不干我再去干

所以如果一个类加载器收到了类加载请求,它并不会自己立刻加载,而是把这个请求委托给父加载器去加载,如果父加载器本身还没有加载,但是能在它管理的路径下找到,父类就会去加载,如果路径下找不到,父加载器的上层还存在父加载器,则继续向上委托,如果请求最终转到了BootStrapClassLoader启动类加载器,同样也无法加载,看来靠天靠地不如靠自己,就自己加载一次;

双亲委派的优点是避免了类的重复加载, 缺点是无法向下委派/或者不委派

打破双亲委派机制

在某些情况下,由于受到加载范围的限制,我们需要打破双亲委派去实现特定的功能,父加载器无法加载到所需的文件,比如我们想象一下Driver接口

图片.png 很明显,Driver接口是需要BootStrap ClassLoader去加载的,DriverManager类也是BootStrap ClassLoader去加载的,Driver接口的实现类是数据库服务厂商提供的, DriverManager需要加载各个实现了Driver接口的实现类,然后进行管理, 按照双亲委派机制,是应该由自己加载,可是实现类并不在自己的加载范围之内,很明显是应该由它的Application ClassLoader去加载,如此便有了 在这里插入图片描述ClassLoader cl = Thread.currentThread().getContextClassLoader(); 这句代码获取当前线程的类加载器, 而当前线程默认的类加载器是ApplicationClassLoader,如此便破坏了双亲委派机制;

再试想我们知道tomcat中是可以部署多个应用的, 每个应用依赖的版本可能是不相同的,但是类的全限定名是一样的,如果遵循双亲委派机制的话,会出现大问题,所以tomcat会自动生成不同的类加载器去加载不同war包内的文件,不会进行双亲委派,同样打破了双亲委派机制;

运行时数据区

程序计数器:

程序计数器是线程私有的,每个线程都有自己的程序计数器,这样才能保证获取时间片时,知道这个线程执行到了什么位置。通过程序计数器我们知道下一条要执行什么指令,保存的是下一条指令的地址。

也是线程私有的,栈中用来存放一些基本数据类型的值和引用类型的对象引用(String,数组.对象等等)生命周期和线程一致。用完内存立即被释放;

虚拟机栈:

存储方法的(局部变量表,操作数栈,动态链接,方法出口信息),方法的调用和完成,代表着入栈和出栈;

本地方法栈:

与虚拟机栈作用相似,执行的是native方法入栈和出栈,java调用c/c++的动态链接库,运行动态链接库里的函数;

存放new关键字创建的对象,数组, 静态变量static,运行时常量池,字符串常量池

图片.png 堆是线程共享的区域,也是虚拟机里面最大的一块内存,这里的内存是用来存放实例化对象,所有的实例化对象都存在这里,最简单的理解方式就是只要看到new,就应该知道new出来的实例对象在堆里面,

为了进行垃圾回收,堆一般又分为新生代和老年代,新生代与老年代的比例为1:2,而新生代又分为 Eden区、From Survivor区和To Survivor区,也称作s0和s1区,他们的内存比例为8:1:1,毫无疑问,堆是GC回收的重点区域;

图片.png

新生代:

Eden区:新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁,在eden区经历了好几次小的Gc还在存活的对象,就会进入存活区(s0 / s1)

s0和s1区:值得一提的是s0和s1在from Survival 和 to Survival 两个角色之间反复横跳, 当发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够久的对象移至年老代

老年代: 要么活得久,要么体型大

  1. 15 次GC:

依然存活的对象, 可以向下调,因为约束在4bit范围内也就是(0-15) 2. 大对象: 超过Eden区一半大小的对象 (eden区在gc后会动态调整的) 3. 空间担保: GC以后 eden区剩下的对象太大,from和to区放不下,进入老年代 4. 动态年龄判断: eden+from区经行GC以后,所有年龄的对象相加1+2+3+...+N的对象加起来的空间,大于to区域的一半,就会让年龄N以及年龄N以上的对象进入老年代;

方法区: (元空间)

属于共享内存区域,存储已被虚拟机加载的类信息Klass、JIT编译后的代码缓存等数据。所以方法区并不能简单理解为元空间,其中还包括codecache等数据

元空间是JDK8以及之后版本的实现,在直接内存即操作系统物理内存

为什么元空间取代了永久代?

1.GC 以前类的元信息(InstanceKlass)(字面量:类名、属性名、属性签名、方法名),字符串等全部放在永久代,存在堆区.堆区要把对象,字符串,元信息区分GC标记,现在存放在方法区元空间,和堆区分开更方便GC 2.生成类OOM: 运行时会动态生成类(cglib)的元信息,如果控制不好极易导致无限创建永久代内存溢出。 3、硬件发展: 之前内核层和应用层的内存比较小,因为内存限制放到一起管理,如今硬件的发展为分离带来了基础条件; 4、字符串OOM: 在1.6之前的版本中,字符串常量池存在于永久代中,在大量使用字符串的情况下,非常容易出现OOM的异常 5.orecal把hotspot 和JRokit合二为一 由于合并,并且JRokit没有永久代,永久代已经不适用了

所以大概能用一张图来描述JVM内存模型:

图片.png

执行引擎

我们前面提到,类加载器把字节码文件加载到虚拟机,但是很显然字节码文件是无法让操作系统执行的, 需要让字节码指令转换成机器码指令,执行引擎扮演的就是这个角色;

执行字节码转换有两种方式: 编译执行&&解释执行 ,而JVM是两者兼顾的,这也是为什么java被称为半编译半解释性语言;

图片.png JVM有两种解释器,两种执行方式,三种运行模式:

字节码解释器,解释执行, 解释执行,把字节码指令逐行进行解释成机器码执行 模板解释器, 编译执行 把所有的字节码指令直接解释成机器码后再执行,所以模板解释器的效率比较快,但是一次性转换的过程太耗时

并且JVM可以选择三种运行模式 -Xint 只用字节码解释器 -Xcomp 只用模板解释器 -Xmixed 混合使用字节码+模板解释器(默认)

对于编译执行,依赖的是即时编译技术JIT: 编译热点代码, 热点代码缓存区在方法区Codecache(不要习惯性的把方法区和元空间对等)

即时编译器分类:

C1编译器 / C2编译器 / 分层编译器(c1+c2); 这里不再赘述,简单理解C1会对代码进行浅优化,C2会进行更深度的优化,分层编译动态的选择深浅优化, 并且分层优化是默认开启的优化方式,我们可以通过java -version查看

图片.png 触发即时编译的条件:

触发即时编译的是方法或者代码段执行的次数,用方法调用计数器和回边计数器分别管理,Client模式下,方法调用次数为1500才触发;Server模式下,方法调用次数为10000触发;代码段执行次数C1默认为13995,C2 默认为 10700;

至于C1编译器的优化手段: 方法内联/去虚拟化/冗余消除; 还有C2编译器的优化手段: 栈上分配/标量替换/锁消除; 以及垃圾回收原理,感兴趣的话,我们另起一篇;或关注同名公众号:XoneCoding

图片.png

Guess you like

Origin juejin.im/post/7069613491757269028