Java虚拟机

概述

Java官方文档 https://docs.oracle.com/en/java/index.html

JVM是一种规范,通过Oracle Java 官方文档找到JVM的规范查阅。Java虚拟机可以看做虚拟出来一台计算机,主体功能字节码指令集(汇编语言)和内存管理(栈、堆、方法区)等

常见的JVM实现

  • Hotspot:目前使用的最多的 Java 虚拟机。
  • Jrocket:原来属于BEA 公司,曾号称世界上最快的 JVM,后被 Oracle 公司收购,合并于 Hotspot
  • J9: IBM 有自己的 java 虚拟机实现,它的名字叫做 J9. 主要是用在 IBM 产品(IBM WebSphere 和 IBM 的 AIX 平台上)
  • TaobaoVM: 只有一定体量、一定规模的厂商才会开发自己的虚拟机,比如淘宝有自己的 VM,它实际上是 Hotspot 的定制版,专门为淘宝准备的,阿里、天 猫都是用的这款虚拟机。
  • LiquidVM: 它是一个针对硬件的虚拟机,它下面是没有操作系统的(不是 Linux 也不是 windows),下面直接就是硬件,运行效率比较高。
  • zing: 它属于 zual 这家公司,非常牛,是一个商业产品,很贵!它的垃圾回收速度非常快(1 毫秒之内),是业界标杆。它的一个垃圾回收的算法后来被 Hotspot 吸收才有了现在的 ZGC。

体系结构

我们通常所说的JDK,其实是指Java开发包,里面包含Java开发用到的工具集。

  • JDK体系结构

    • Java运行环境(JRE)和开发工具(编译器,调试器,javadoc等)。

      • JRE:由JVM,Java运行时类库,动态链接库等组成;它为Java提供了运行环境,其中重要的一环就是通过JVM将字节码解释成可执行的机器码。
      • JDK的编译器Javac[.exe],会将Java代码编译成字节码(.class文件)。编译出的字节码在任何平台上都一样的内容,所以我们说Java语言是门跨平台语言;Writeonce, run anywhere。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vmjOtk2-1644747481396)(http://www.itxiaoshen.com:3001/assets/1644552030402KQTxi6TP.png)]****

  • JVM和操作系统关系

    • 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
    • 在运行时环境,JVM会将Java字节码解释成机器码。机器码和平台相关的(不同硬件环境、不同操作系统,产生的机器码不同),所以JVM在不同平台有不同的实现。目前JDK默认使用的实现是Hotspot VM。

  • JVM架构:Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ut6GY2lN-1644747481400)(http://www.itxiaoshen.com:3001/assets/16445520369994chXCWt4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TtOAZ9M-1644747481409)(http://www.itxiaoshen.com:3001/assets/1644641759500Hs1z7mMd.png)]

逃逸分析(Escape Analysis)

概述

  • 逃逸分析的是一个对象的动态作用域,2种情况

    • 方法逃逸:对象通过参数传递传给了另一个方法。
    • 线程逃特性逸:对象有另外的线程访问。
  • 逃逸分析的目的是确认一个对象是否只可能当前线程能访问。
  • 逃逸分析可以带来一定程度上的性能优化。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
  • JIT(Just In Time Compiler)即时编译技术,在即时编译过程中JVM可能会对我们的代码做一些优化如逃逸分析等。

优化策略

逃逸分析的优化策略主要有三个:对象可能分配在栈上、分离对象或标量替换、消除同步锁。

  • 对象可能分配在栈上:JVM通过逃逸分析,分析出新对象的使用范围,就可能将对象在栈上进行分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力。
  • 分离对象或标量替换:当JVM通过逃逸分析,确定要将对象分配到栈上时,即时编译可以将对象打散,将对象替换为一个个很小的局部变量,我们将这个打散的过程叫做标量替换。将对象替换为一个个局部变量后,就可以非常方便的在栈上进行分配了。所谓标量就是不能再分割的变量,如Java基本数据类型。
  • 同步锁消除:如果JVM通过逃逸分析,发现一个对象只能从一个线程被访问到,则访问这个对象时,可以不加同步锁。如果程序中使用了synchronized锁,则JVM会将synchronized锁消除。

逃逸分析开关

逃逸分析其实并不是新概念,早在1999年就有论文提出了该技术。但在Java中算是新颖而前言的优化技术,从 JDK1.6才开始引入该技术,JDK1.7开始默认开启逃逸分析,也可通过开关控制

  • -XX:+DoEscapeAnalysis开启逃逸分析
  • -XX:-DoEscapeAnalysis 关闭逃逸分析
  • -XX:+EliminateAllocations开启标量替换
  • -XX:-EliminateAllocations 关闭标量替换
  • -XX:+EliminateLocks开启锁消除
  • -XX:-EliminateLocks 关闭锁消除
  • 开启标量替换或锁消除 必须打开逃逸分析开关

常见面试问题

  • 是不是所有的对象和数组都会在堆内存分配空间?不一定,只要掌握了逃逸分析的原理,那就很清楚知道Java中的对象不一定是在堆上分配的,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。

  • 加了锁的代码锁就一定会生效吗?不一定

JVM运行模式

  • 解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码
  • 编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有的JVM字节码一次编译为机器码,然后一次性执行所有机器码
  • 混合模式(Mixed Mode):(-Xmixed 设置JVM使用混合模式)依然使用解释模式执行代码,但是对于一些“热点”代码采取编译器模式执行,这些热点代码对应的机器码会被缓存起来,下次执行无需再编译。JVM一般采用混合模式执行代码
JVM运行模式 优点 适用场景
解释模式 启动快 只需要执行部分代码,且大多数代码只会执行一次的情况
编译模式 启动慢,但是后期执行速度快,比较占用内存1 适合代码可能会被反复执行的场景
混合模式 一般JVM所默认的模式

Java启动参数分类

  • 标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
  • 非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;jvm参数调优重点
  • 非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;jvm参数调优重点。可以通过java -XX:+PrintFlagsFinal -version 获取支持参数选项。

类加载

类加载过程

  • 类加载:类加载器将class文件加载到虚拟机的内存

  • 类的7个生命周期:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

    • 加载:在硬盘上查找并通过IO读入字节码文件

    • 连接:执行验证、准备、解析步骤

      • 验证:校验字节码文件的正确性,不是必要的,可以关闭校验提高虚拟机加载类的速度
      • 准备:给类的静态变量分配内存,并赋予默认值
      • 解析:将符号引用替换为直接引用;通过javap -v将字节码文件反汇编为更可读的文件,像类,方法等一切的字面量都转换为符号引用#1...N。包括类解析,字段解析,方法解析,接口解析。即将字节码的静态字面关联(字符串,静态)转换为JVM内存中的动态指针关联(指针,动态)。

    • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

    • 使用:new出对象程序中使用
    • 卸载:执行垃圾回收

类加载器

概述

  • 启动类加载器:C语言开发,加载Java核心类库。基于沙箱机制,只加载java、javax、sun包开头的类。
  • 扩展类加载器:Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,上级加载器为启动类加载器,负责加载jre/lib/ext扩展目录中的jar类包。
  • 应用程序类加载器:Java语言编写,由sun.misc.Launcher$AppClassLoader实现,上级加载器为扩展类加载器,是默认的类加载器。负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
  • 自定义类加载器:负责加载用户自定义路径下的类包
    • 使用场景:字节码二进制流来自于网络,字节码文件不在指定的lib、ext、classpath路径下,需要对二进制流加工后才能得到字节码。
    • 不同的类加载器加载同一个Class字节码文件后,在JVM中产生的类对象是不同的。同一个类加载器,Class实例在JVM中才是全局唯一。
    • 在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

类加载器获取示例

JVM预定义的类加载器为启动类加载器、扩展类加载器、应用程序类加载器。

package cn.itxs.classloader;

import cn.itxs.entity.User;

public class ClassLoaderMain {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESedeKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(User.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}

自定义类加载器示例

除了启动类加载器,所有类加载器都是ClassLoader的子类,所以我们可以通过继承ClassLoader来实现自己的类加载器。

package cn.itxs.entity;

public class Goods {
public void sayHello(){
System.out.println("hello goods!");
}
}

生成字节码文件Goods.class后剪切(不是拷贝,删掉类路径下的字节码文件)到D:\temp\cn\itxs\entity目录下

package cn.itxs.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel; public class ItxsClassLoader extends ClassLoader{
private String classPath; public ItxsClassLoader(String classPath) {
this.classPath = classPath;
} private byte[] loadBytes(String name) throws IOException {
name = name.replaceAll("\\.","/");
FileInputStream fis = new FileInputStream(classPath+"/"+name+".class");
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true){
int i = fc.read(by);
if (i == 0 || i == -1){
break;
}
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
} @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadBytes(name);
return defineClass(name,bytes,0, bytes.length);
}catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
} public static void main(String[] args) throws Exception {
ItxsClassLoader itxsClassLoader = new ItxsClassLoader("D:/temp");
Class clazz = itxsClassLoader.loadClass("cn.itxs.entity.Goods");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sayHello", null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}

双亲委派机制

  • 双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
  • 当进行类加载的时候,虽然用户自定义类不会由Bootstrap ClassLoader或是Extension ClassLoader加载(由类加载器的加载范围决定),但是代码实现还是会一直委托到Bootstrap ClassLoader, 上层无法加载,再由下层是否可以加载,如果都无法加载,就会触发findClass,抛出ClassNotFoundException。
  • 类加载器之间的层级关系并不是以继承的方式存在的,而是以组合的方式处理的。

  • 双亲委派机制是在ClassLoader里的loadClass方法里实现的,如果想自己实现类加载器的话,可以继承ClassLoader后重写findClass方法,加载对应的类。源码实现流程简单如下:

    • 首先判断该类是否已经被加载。
    • 该类未被加载,如果父类不为空,交给父类加载。
    • 如果父类为空,交给Bootstrap Classloader 加载。
    • 如果类还是无法被加载到,则触发findClass,抛出ClassNotFoundException。
    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
} if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
  • 双亲委派机制作用

    • 可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
    • 保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,这样可以有效的防止核心Java API被篡改。
  • 打破双亲委派

    • 双亲委派机制也非必然。比如Tomcat web容器里面部署了很多的应用程序,这些应用程序对于第三方类库的依赖版本却不一样,且通常这些第三方类库的路径又是一样的,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。所以Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
    • Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,不遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载。

**本人博客网站 **IT小神 www.itxiaoshen.com

JVM性能调优与实战基础理论篇-上的更多相关文章

  1. JVM性能调优与实战进阶篇-上

    ZGC 诞生原因 Java生态非常强大,但还不够,有些场景仍处于劣势,而ZGC的出现可以让Java语言抢占其他语言的某些特定领域市场.比如 谷歌主导的Android手机系统显示卡顿. 证券交易市场,实 ...

  2. JVM性能调优与实战基础理论篇-中

    JVM内存模型 概述 我们所说的JVM内存模型是指运行时数据区,用New出来的对象放在堆中,如每个线程中局部变量放在栈或叫虚拟机栈中,下图左边区域部分为栈内存的结构.如main线程包含程序炯酸器.线程 ...

  3. JVM性能调优与实战基础理论篇-下

    JVM内存管理 JVM内存分配与回收策略 对象优先在Eden分配,如果Eden内存空间不足,就会发生Minor GC.虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在 ...

  4. JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码

    本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍) ...

  5. JVM 性能调优实战之:一次系统性能瓶颈的寻找过程

    玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈.性能优化分为好几个层次,比如系统层次.算法层次.代码层次…JVM 的性能优化被认为是底层优化,门槛较高, ...

  6. JVM性能调优实践——JVM篇

    前言 在遇到实际性能问题时,除了关注系统性能指标.还要结合应用程序的系统的日志.堆栈信息.GClog.threaddump等数据进行问题分析和定位.关于性能指标分析可以参考前一篇JVM性能调优实践-- ...

  7. 使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码 (jvm性能调优)

    技术交流群:233513714 本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 ...

  8. JVM性能调优(3) —— 内存分配和垃圾回收调优

    前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 一.内存调优的目标 新生代的垃圾回收是比较简单的,Eden区满了无法分配新对象 ...

  9. JVM性能调优(4) —— 性能调优工具

    前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 JVM性能调优(3) -- 内存分配和垃圾回收调优 一.JDK工具 先来看看有 ...

随机推荐

  1. 字符串的展开expand

    A. 字符串的展开(expand.cpp) 内存限制:64 MiB 时间限制:1000 ms 标准输入输出 题目类型:传统 评测方式:文本比较 题目描述 在初赛普及组的"阅读程序写结果&qu ...

  2. linux(CentOS7) 之 克隆虚拟机并配置网络(固定ip)

    克隆机器 原机关机状态下,克隆. 下一步 选择当前状态,下一步 选择创建完整克隆,下一步 设置虚拟机名称(完成后可以修改).克隆机安装位置,下一步 等待克隆完成 克隆完成 配置网络 添加网卡(因为物理 ...

  3. unittest_测试报告(6)

    用例执行完成后,执行结果默认是输出在屏幕上,其实我们可以把结果输出到一个文件中,形成测试报告. unittest自带的测试报告是文本形式的,如下代码: import unittest if __nam ...

  4. Python 国内镜像源

    让 python pip 使用国内镜像源 国内镜像源: 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:http://mirrors.aliyun.co ...

  5. js对cookie的操作:读、写、删

    js读写cookie //JS操作cookies方法!//写cookiesfunction setCookie(name,value){var Days = 30;var exp = new Date ...

  6. 正则验证&模态框

    在日常生活中,凡是需要表单验证的都会用到正则验证.下面拿一个简单的带有模态框的正则验证的小demo看一下     <style>         /* 遮罩层 */         .ma ...

  7. Mysql实训任务书

    注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6635189537079296526/ 什么是数据库:数据库(Database)是按照数据结构来组织.存储和管理数据 ...

  8. vue-json-editor可视化编辑器的介绍与应用

    vue-json-editor可视化编辑器 最近项目中有用到json编辑器,我选用了这款vue的编辑器,看起来也是比较简洁,接下来就具体介绍一下它,以及内部属性. 一.vue-json-editor的 ...

  9. JS调用堆栈

    调用栈 JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事.如果我们运行到一个函数,它就会将其放置到栈顶.当从这个函数返回的时候,就会将这个函数从栈顶弹出 ...

  10. 微服务架构 | 12.1 使用 Apache Dubbo 实现远程通信

    目录 前言 1. Dubbo 基础知识 1.1 Dubbo 是什么 1.2 Dubbo 的架构图 1.3 Spring Cloud 与 Dubbo 的区别 1.4 Dubbo 的特点 1.5 Dubb ...