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. Jenkins_忘记管理员密码的处理方法

    1.查看jenkins配置存放目录 2.修改config.xml的useSecurity的true为flase 3.重启jenkins服务 4.进入jenkins,不输入密码直接就进入了jenkins ...

  2. 关闭SpringBoot logo图标

    public static void main(String[] args) {// SpringApplication.run(LicenseApp.class, args); //关闭Spring ...

  3. spring security 继承 WebSecurityConfigurerAdapter 的重写方法configure() 参数 HttpSecurity 常用方法及说明

    HttpSecurity 常用方法及说明 方法 说明 openidLogin() 用于基于 OpenId 的验证 headers() 将安全标头添加到响应 cors() 配置跨域资源共享( CORS ...

  4. 微信小程序css继承

    在微信小程序里写的全局样式,pages里的组件是可以继承的,但是components里只能继承font和color属性.

  5. Word2010发布博客

    原文链接: https://www.toutiao.com/i6488986125292536334/ 选择"文件按钮","保存并发送"菜单项,"发布 ...

  6. JAVA多线程之并发编程三大核心问题

    概述 并发编程是Java语言的重要特性之一,它能使复杂的代码变得更简单,从而极大的简化复杂系统的开发.并发编程可以充分发挥多处理器系统的强大计算能力,随着处理器数量的持续增长,如何高效的并发变得越来越 ...

  7. leetcode 33. 搜索旋转排序数组 及 81. 搜索旋转排序数组 II

    33. 搜索旋转排序数组 问题描述 假设按照升序排序的数组在预先未知的某个点上进行了旋转. ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] ). 搜索一个给定 ...

  8. 【笔记】对golang的大量小对象的管理真的是无语了……

    业务中有这样一个struct: type bizData struct{ A uint64 B uint64 C int32 D uint32 } 虽然没有实测,但我猜测这样的对齐方式,这个struc ...

  9. 记录一个问题:macos High Sierra 10.13.6 内核内存泄漏,导致内存满而不得不重启

    kernel_task进程占用内存10g以上,使用中突然提示内存不足,要求杀死工作进程,不得不强按电源键来关机重启. 升级之前,版本大约是macos High Sierra 10.13.4, 系统频繁 ...

  10. 【记录一个问题】用毫无用处的方法解决了libtask的asm.S在ndk下编译的问题

    昨天提到,libtask中的asm.S使用的是ARM 32位的语法,因此在ARM 64下无法编译通过. 于是查了一下资料,改写了一下汇编代码,使得可以在64位下编译通过.源码如下 #if define ...