首页 > 编程语言 >Java 实战虚拟机 进阶 (一 万字)

Java 实战虚拟机 进阶 (一 万字)

时间:2024-10-19 10:19:10浏览次数:9  
标签:Java 进阶 对象 虚拟机 引用 内存 方法 字节

实战 Java 虚拟机-高级篇

什么是 GraalVM

GraalVM 是 Oracle 官方推出的一款 **高性能JDK,**使用它享受比 OpenJDK 或者 OracleJDK 更好的性能。

  • GraalVM 的官方网址:https://www.graalvm.org/
  • 官方标语:Build faster, smaller, leaner applications.
  • 更低的 CPU、内存使用率

image-20241012154553514

  • 官方标语:Build faster, smaller, leaner applications.
  • 更低的CPU、内存使用率
  • 更快的启动速度,无需预热即可获得最好的性能
  • 更好的安全性、更小的可执行文件
  • 支持多种框架Spring Boot、Micronaut、Helidon 和 Quarkus。多家云平台支持。
  • 通过 Truffle框架运行 JS、Python、Ruby等其他语言。

image-20241012154725737

GraalVM 的版本

GraalVM 分为社区版(Community Edition)和企业版(Enterprise Edition)。企业版相比较社区版,在性能上有更多的优化。

image-20241012154932073

GraalVM 社区版环境搭建

image-20241012155004888

GraalVM 的两种运行模式

JIT (Just-In-Time) 模式,即时编译模式

JIT 模式的处理方式与 Oracle JDK类似,满足两个特点:

  • Write Once,Run Anywhere ->一次编写,到处运行。
  • 预热之后,通过 内置的Graal即时编译器 优化热点代码,生成比 Hotspot JIT 更高性能的机器码。

image-20241012155747330

image-20241012155938711

AOT (Ahead-Of-Time)模式,提前编译模式

AOT 编译器通过源代码,为特定平台创建可执行文件。比如,在Windows下编译完成之后,会生成 exe文件。通过这种方式,达到启动之后获得最高性能的目的。但是不具备跨平台特性,不同平台使用需要单独编译。这种模式生成的文件称之为Native Image本地镜像。

image-20241012160426792

image-20241012160506620

官网:https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites

GraalVM 模式和版本的性能对比

社区版的GraalVM 使用本地镜像模式性能不如 HotspotJVM 的 JIT 模式,但是企业版的性能相对会高很多

image-20241012162306816

GraalVM 存在的问题

GraaIVM 的 AOT模式虽 然在启动速度、内存和CPU开销上非常有优势,但是使用这种技术会带来几个问题:

1、跨平台问题,在不同平台下运行需要编译多次。编译平台的依赖库等环境要与运行平台保持一致。
2、使用框架之后,编译本地镜像的时间比较长,同时也需要消耗 大量的CPU 和 内存。
3、AOT编译器 在编译时,需要知道运行时所有可访问的所有类。但是 Java 中有一些技术可以在运行时创建类,例如反射、动态代理等。这些技术在很多框架比如Spring中大量使用,所以框架需要 对AOT编译器进行适配解决类似的问题。

image-20241012162500806

实战案例1:使用 SpringBoot3 搭建 GraalVM 环境

image-20241012162707457

应用场景

image-20241012163559016

GraalVM 企业级应用 - Serverless 架构

传统的系统架构中,服务器等基础设施的运维、安全、高可用等工作都需要企业自行完成,存在两个主要问题:

1、开销大,包括了人力的开销、机房建设的开销。
2、**资源浪费,面对一些突发的流量冲击,比如秒杀等活动,必须提前规划好容量准备好大量的服务器,**这些服务器在其他时候会处于闲置的状态,造成大量的浪费。

image-20241012163832045

随着虚拟化技术、云原生技术的愈发成熟,云服务商提供了一套称为 Serverless无服务器化的 架构。企业无需进行服务器的任何配置和部署,完全由云服务商提供。比较 典型的有亚马逊 AWS、阿里云等 。

image-20241012163946250

Serverless架构 - 函数计算

Serverless架构 中第一种常见的服务是函数计算(Function as a Service),将一个应用拆分成多个函数,每个函数会以事件驱动的方式触发。典型代表有 AWS 的 Lambda、阿里云的 FC。

image-20241012164209647

函数计算主要应用场景有如下几种:

① 小程序、API服务 中的接口,此类接口的调用频率不高,使用常规的服务器架构容易产生资源浪费,使用 Serverless 就可以实现按需付费降低成本,同时支持自动伸缩能应对流量的突发情况。
② 大规模任务的处理,比如 音视频文件转码、审核等,可以利用事件机制当文件上传之后,自动触发对应的任务。
函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式 编译出来的本地镜像可以节省更多的成本。

函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式编译 出来的本地镜像可以节省更多的成本。

image-20241012164714003

实战案例 2:将程序部署到阿里云函数计算

image-20241012164827347

Serverlesss 架构 -Serverless 应用

函数计算的服务资源比较受限,比如 AWS 的Lambda 服务一般 无法支持超过15分钟的函数执行,所以云服务商提供了另外一套方案:基于容器的Serverless 应用,无需手动配置 K8s中 的 Pod、Service 等内容,只需选择镜像就可自动生成应用服务。

同样,Serverless应用 的计费标准中 包含CPU和内存使用量,所以 使用GraaIVM AOT模式 编译出来的本地镜像 可以节省更多的成本。

image-20241012165900643

将程序部署到 阿里云 Serverless 应用

步骤:

1、在项目中编写 Dockerfile 文件。
2、使用服务器制作镜像,这一步会消耗大量的CPU和内存资源,同时 GraaIVM相关 的镜像服务器在国外,**建议使用阿里云的镜像服务器制作Docker镜像。**前两步同实战案例2
3、配置Serverless应用,选择容器镜像、CPU和内存。
4、绑定外网负载均衡并使用 Postman 进行测试。

参数优化和故障诊断

GraalVM 的内存参数

由于 GraalVM 是一款独立的 JDK,所以大部分 HotSpot 中的虚拟机参数都不适用。常用的参数参考:官方手册

  • 社区版只能使用串行 垃圾回收器(Serial GC),使用串行垃圾回收器的默认 最大Java 堆大小 会设置为物理内存大小的 80%,调整方式为使用 -Xmx最大堆大小。如果希望在编译期就指定该大小,可以在编译时添加参数 -R:MaxHeapSize=最大堆大小。
  • G1垃圾回收器只能在企业版中使用,开启方式为添加 --gC=G1参数,有效降低垃圾回收的延迟。
  • 另外提供一个Epsilon GC,开启方式:–gc=epsilon,它不会产生任何的垃圾回收行为所以没有额外的内存、CPU开销。如果在公有云上运行的程序生命周期短暂不产生大量的对象,可以使用该垃圾回收器,以节省最大的资源。
  • -XX:+PrintGC -XX:+VerboseGC 参数打印垃圾回收详细信息。
实战案例4:内存快照文件的获取

image-20241012171142190

实战案例5:运行时数据的获取

image-20241012172132450

总结

image-20241012172507863

新一代的GC

垃圾回收器的技术演进

image-20241012173037234

不同的垃圾回收器设计的目标是不同的,如下图所示:

image-20241012173146626

Shenandoah GC

Shenandoah 是由 Red Hat 开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并发的整理,堆大小对 STW的时间 基本没有影响。

image-20241012173527082

Shenandoah 的使用方法

1、下载。Shenandoah 只包含在 Open JDK中,默认不包含在内需要单独构建,可以直接下载构建好的。

下载地址: https://builds.shipilev.net/openjdk-jdk-shenandoah/

选择方式如下:

image-20241012173735360

image-20241012173902676

ZGC

ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW 的时间不会超过一毫秒,适合需要低延迟的应用。支持 几百兆 到 16TB 的堆大小,堆大小对 STW 的时间基本没有影响。ZGC 降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以 如果Java服务 比较关注 QPS(每秒的查询次数)那么 G1是 比较不错的选择。

image-20241012174934173

ZGC 的版本更选

image-20241012175136688

OracleJDK 和 OpenJDK 中都支持 ZGC,阿里的 DragonWell 龙井JDK 也支持 ZGC 但属于其自行对 Open JDK 11 的 ZGC 进行优化的版本。
建议使用 JDK17 之后的版本,延迟较低同时无需手动配置并行线程数。

image-20241012175243422

ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化。

  • 自动设置年轻代大小,无需设置 -Xmn参数。
  • 自动晋升阈值(复制中存活多少次才搬运到老年代),无需设置 -XX:TenuringThreshold。
  • JDK17 之后支持自动的并行线程数,无需设置 -XX:ConcGCThreads。
ZGC 的参数设置

image-20241012175603648

ZGC 的调优

ZGC 中可以使用 Linux 的 Huge Page 大页技术优化性能,提升吞吐量、降低延迟。
注意:安装过程需要 root 权限,所以 ZGC 默认没有开启此功能。

操作步骤:

1、计算所需页数,Linuxx86 架构中大页大小为 2MB,根据所需堆内存的大小估算大页数量。比如堆空间需要 16G,预留 2G( JVM 需要额外的一些非堆空间),那么页数就是 18G / 2MB = 9216。
2、配置系统的大页池以具有所需的页数(需要root权限):
$ echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
3、添加参数 -XX:+UseLargePages 启动程序进行测试

实战案例

内存不足时的垃圾回收测试

image-20241012193452255

总结

image-20241012194656868

揭秘 Java 工具

在 Java 的世界中,除了 Java 编写的业务系统之外,还有一类程序也需要 Java程序员 参与编写,这类程序就是 Java工具。
常见的 Java工具 有以下几类:
1、诊断类工具,如 Arthas、VisualVM 等。
2、开发类工具,如 ldea、Eclipse。
3、APM 应用性能监测工具,如 Skywalking、Zipkin 等。
4、热部署工具,如 Jrebel 等。

image-20241012194931547

学习 Java 工具常用技术 Java Agent

Java Agent技术是 JDk 提供的用来编写 Java工具 的技术,使用这种技术生成一种特殊的 jar包,这种 jar包 可以让 Java程序 运行其中的代码。

image-20241012195349431

Java Agent 技术的两种模式-静态加载模式

静态加载模式可以在程序启动的一开始就执行我们需要执行的代码,适合用 APM等性能监测系统 从一开始就监控程序的执行性能。静态加载模式需要在Java Agent的项目中编写一个 premain 的方法,并打包成jar包。

image-20241012195559233

搭建 java agent静态加载模式的环境

111

maven 环境配置Maven重点学习笔记(包入门 2万字)

image-20241012200142572

Java代码
import java.lang.instrument.Instrumentation;

public class AgentMain {
    // premain 方法
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain执行了");
        
    }
}
Java Agent技术的两种模式-动态加载模式

动态加载模式可以随时让 java agent代码执行,适用于 Arthas等诊断系统。动态加载模式需要在 Java Agent的项目中编写一个 agentmain 的方法,并打包成 jar包。

image-20241012195756051

搭建 java agent 动态加载模式的环境

image-20241012201851700

Java 代码
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        // 获取进程虚拟对象
        VirtualMachine vm = VirtualMachine.attach("37632");
        // 执行Java agent里面的 agentmain 方法
        vm.loadAgent("D:/jvm/javaagent/itheima-agent/target/itheima-agent-1.0-SNAPSHOT-jar-with-dependencide");
        
        
    }
}

实战案例

实战案例1:简化版的 Arthas
需求:

编写一个简化版的 Arthas程序,具备以下几个功能:
1、查看内存使用情况
2、生成堆内存快照
3、打印栈信息
4、打印类加载器
5、打印类的源码
6、打印方法执行的参数和耗时

该程序是一个 独立的Jar包,可以应用于 任何Java编写 的系统中具备以下特点:代码无侵入性、操作简单、性能高。

image-20241012202538085

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        
        // 获取进程列表,让用户手动进行输入
        // 1.执行 jps 命令,打印所有进程列表
        Process jps = Runtime.getRuntime().exec("jps");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jsp.getInputStream()));
        
        try {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close();
            }
        }
        
        // 2. 输入进程 id
        Scanner scanner = new Scanner(System.in);
        String processId = scanner.next();
        
        // 获取进程虚拟对象
        VirtualMachine vm = VirtualMachine.attach("processId");
        // 执行Java agent里面的 agentmain 方法
        vm.loadAgent("D:/jvm/javaagent/itheima-agent/target/itheima-agent-1.0-SNAPSHOT-jar-with-dependencide");
      
    }
}
获取运行时信息 - JMX 技术

JDK 从 1.5 开始提供了 Java Management Extensions(JMX) 技术,通过 Mbean 对象的写入和获取,实现:

  • 运行时配置的获取和更改
  • 应用程序运行信息的获取(线程栈、内存、类信息等)

image-20241012203813856

image-20241012203854237

image-20241012205231141

获取类和类加载器的信息 - Instumentation 对象

Oracle官方手册: https://docs.oracle.com/javase/17/docs/api/java/lang/instrument/Instrumentation.html

image-20241013233358454

打印类的源码

image-20241013235026834

image-20241013235153242

这里我们会使用 jd-core 依赖库 来完成,github地址:https://github.com/java-decompiler/id-core

image-20241014002051571

import java.util.List;
public class MemoryCommand {
    
    // 打印所有内存信息
    public static void printMemory() {
        List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
        
        // 堆内存
        memoryPoolMXBeans.stream().filter(x -> x.getType().equals(MemoryType.HEAP))
            			.forEach(x -> {
                            StringBuilder sb = new StringBuilder();
                            sb.append("name")
                                .append(x.getName())
                                .append(" used:")
                                .append(x.getUsage().getUsed() / 1024 / 1024)
                                .append("m")
                                
                                .append(" committed:")
                                .append(x.getUsage().getUsed() / 1024 / 1024)
                                .append("m")
                                
                                .append(" max:")
                                .append(x.getUsage().getUsed() / 1024 / 1024)
                                .append("m")
                               
                        });      
        				 
    }

}
Spring AOP是不是也可以实现类似的功能呢?

image-20241014090150083

打印方法执行的参数和耗时

打印方法执行的参数和耗时需要对原始类的方法进行增强,可以使用类似于 Spring AOP这类 面向切面编程 的方式,但是考虑到并非每个项目都使用了Spring 这些框架,所以我们选择的是最基础的字节码增强框架。字节码增强框架是在当前类的字节码信息中插入一部分字节码指令,从而起到增强的作用。

image-20241014090352278

打印方法执行的参数和耗时 - ASM

ASM是一个通用的 Java字节码 操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。ASM重点关注性能。让操作尽可能小且尽可能快,所以它非常适合在动态系统中使用。ASM的缺点是代码复杂。

官网:ASM的官方网址:https://asm.ow2.io/

image-20241014090609928

操作步骤

image-20241014091009882

image-20241014091056899

示例
// ASM 入门案例,向每个方法添加一行字节码指令
public class ASMDemo {
    public static void main(String[] args) throws IOException {
        // 1.从本地读取一个字节码文件, byte[]
        byte[] bytes = FileUtils.readFileToByteArray(new File("D\\jvm\\AttahMain.class"));
        
        // 2.通过ASM修改字节码文件
        // 将二进制数据转换成可以解析内容
		ClassReader classReader = new ClassReader(bytes);
        
        // 创建visitor对象,修改字节码信息
		ClassWriter classWriter = new ClassWriter(0);
        
        ClassVisitor classVisitor = new ClassVisitor(ASM7, new ClassWriter(0)) {
            @Override
            public MethodVisitor visitMethod(int access, String name, string descriptor, String signature, String[] exception) {
                
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exception);
                // 返回自定义的MethodVisitor
                MethodVisitor methodVisitor = new MethodVisitor(this.api.mv) {
                    // 修改字节码指令
					@Override
                    public void visitCode() {
                        // 插入一行代码指令 ICOST_0
                        visitInsn(ICONST_)
                    }
                }
                return methodVisitor;
            }
        };
        
        classReader.accept(classVisitor, 0);
            
        // 将修改完的字节码信息写入文件中,进行替换
        FileUtils.writeByteArrayToFile(new File("D\\jvm\\AttahMain.class"), classWriter.toByteArray());
    }
    
}

打印方法执行的参数和耗时 - Byte Buddy

Byte Buddy 是一个代码生成和操作库,用于在 Java应用程序 运行时创建和修改 Java类,而无需编译器的帮助。Byte Buddy底层基于ASM,提供了非常方便的 APl。

image-20241014093907551

Byte Buddy官网:https://bytehuddy.net/

image-20241014094038195

// 使用 bytebyddy 增强类
new AgentBuilder.Default()
    // 禁止byte buddy处理时修改类名
    .disableClassFormatChanges() // AgentBuilder
    .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) // AgentBuilder.RedefinitionListenable.With
    // 打印出错误日志
	.with(new AgentBuilder.Listener.WithTransformationsOnly(AgentBuilder.Listener.StreamWriting
                                                           .toSystemOut())) // AgentBuilder
    // 匹配哪些类
    .type(ElementMatchers.named(className)) // AgentBuilder.Identified.Narrowable
    
    // 增强,使用MyAdvice通知,对所有方法都进行增强
    .transform(builder, typeDescription, classLoader, module, pretectionDomain) ->
    	builder.visit(Advice.to(MyAdvice.class).on(ELementMathcers.any())) // AgentBuilder.Identify
    .installOn(inst);

image-20241014094155076

class MyAdvice {
    @Advice.OnMethodEnter
    static long enter(@Advice.AllArguments Object[] ary) {
        if (ary != null) {
            for (int i = 0; i < ary.length; i++) {
                System.out.println("Argument: " + i + " is " + ary[i]);
            }
        }
        
        return System.nanoTime();
    }
    
    @Advice.OnMethodExit
     static void exit(@Advice.Enter long value) {
        System.out.println("耗时为:" + (System.nanoTime() - value) + "纳秒");
    }
}

最后将整个 简化版的arthas进行打包,在服务器上进行测试。使用 maven-shade-plugin插件 可以将所有依赖打入同一个 jar包 中并指定 入口main方法。

实战案例 2:APM系统 的数据采集
Application performance monitor (APM) 系统

Application performance monitor(APM) 应用程序性能监控系统是采集运行程序的实时数据并使用可视化的方式展示,使用 APM 可以确保 系统可用性,优化服务性能 和 响应时间,持续改善用户体验。常用的 APM系统 有Apache Skywalking、Zipkin等。

Skywalking官方网站: https://skywalking.apache.org/

image-20241014100657506

image-20241014101258488

image-20241014102202700

ByteBuddy 参数的传递

在 Java Agent 中如果需要传递参数到 ByteBuddy,可以采用如下的方式:

1、绑定 KeyValue,Key 是一个自定义注解,Value 是参数的值。

image-20241014102351597

image-20241014102420446

image-20241014103621224

image-20241014103730183

实战 Java虚拟机 - 原理篇

说服自己学习

image-20241014104006737

栈上的数据存储

这里的内存占用,指的是堆上或者数组中内存分配的空间大小,栈上的实现更加复杂。

image-20241014104137435

image-20241014104452454

Java 中的 8大数据类型 在虚拟机中的实现:

image-20241014104525223

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

案例:验证 boolean 在栈上的存储方式

image-20241014104747743

image-20241014105601023

栈中的数据要保存到堆上或者从堆中加载到栈上时怎么处理?

image-20241014105659195

image-20241014105744512

image-20241014105807732

案例:验证 boolean 从栈保存到堆上只取最后一位

image-20241014105908654

对象在堆上是如何存储的?

对象在堆中的内存布局

image-20241014111629715

对象在堆中的内存布局-标记字段

标记字段相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的,同时在64位(又分为是否开启指针压缩)、32位虚拟机中的布局都不同。以 64位 开启指针压缩为例:

image-20241014111850463

JOL 打印内存布局

JOL 是用于分析 JVM 中对象布局的一款专业工具。工具中使用 Unsafe、JVMTI 和 Serviceability Agent(SA) 等虚拟机技术来打印实际的对象内存布局。
使用方法:

image-20241014112022841

Klass pointer元数据的指针指向方法区中保存的 lnstance Klass对象:

image-20241014112458483

指针压缩

在 64位 的 Java虚拟机中,Klass Pointer 以及对象数据中的对象引用都需要占用 8个字节,为了减少这部分的内存使用量,64位 Java 虚拟机使用指针压缩技术,将堆中原本 8个字节的 指针压缩成4个字节,此功能默认开启
可以使用 -XX:-UseCompressedOops 关闭。

image-20241014112644207

指针压缩的思想是将寻址的单位放大,比如原来按1字节去寻址,现在可以 按8字节寻址。如下图所示,原来按1去寻址,能拿到1字节开始的数据,现在按1去寻址,就可以拿到8个字节开始的数据。

image-20241014112956554

这样将编号当成地址,就可以用更小的内存访问更多的数据。但是这样的做法有两个问题:

1、需要进行内存对齐,指的是将对象的内存占用填充至8字节的倍数。存在空间浪费(对于Hotspot来说不存在,即便不开启指针压缩,也需要进行内存对齐)

2、寻址大小仅仅能支持2的35次方个字节(32GB,如果超过32GB指针压缩会自动关闭)。不用压缩指针,应该是2的64次方=16EB,用了压缩指针就变成了8(字节)=2的3次方*2的32次方=2的35次方

案例:在 hsdb工具 中验证 klass pointer 正确性

image-20241014113327011

内存对齐

内存对齐主要目的是为了解决并发情况下CPU缓存失效的问题:

image-20241014114412821

内存对齐之后,同一个缓存行中不会出现不同对象的属性。在并发情况下,如果让A对象一个缓存行失效,是不会影响到B对象的缓存行的。

image-20241014114500565

在 Hotspot 中,要求每个属性的偏移量 Offset(字段地址-起始地址)必须是字段长度的 N倍。比如下图中,Student类 中的 id属性类型 为long,那么偏移量就必须 是8的倍数。

image-20241014114708875

内存对齐-字段重排列

如果不满足要求,**会尝试使用内存对齐,**通过在属性之间插入一块对齐区域达到目的。
如下图中,name字段 是引用占用8个字节(关闭了指针压缩),所以 Offset 必须是8的倍数,在age和name之间插入了4个字节的空白区域。

image-20241014114844517

子类和父类的偏移量

image-20241014114952459

总结

image-20241014115308698

方法调用的原理

方法调用的本质是通过字节码指令的执行,能在栈上创建栈帧,并执行调用方法中的字节码执行。

以 invoke 开头的字节码指令的作用是执行方法的调用

image-20241014115757559

在 JVM 中,一共有五个字节码指令可以执行方法调用:

1、**invokestatic:**调用静态方法
2、invokespecial: 调用 对象的private方法、构造方法,以及使用 super关键字 调用父类实例的方法、构造方法,以及所实现接口的默认方法。
3、**invokevirtual:**调用对象的 非private方法。
4、**invokeinterface:**调用接口对象的方法。
5、**invokedynamic:**用于调用动态方法,主要应用于 lambda 表达式中,机制极为复杂了解即可。

Invoke 方法的核心作用就是找到字节码指令并执行

Invoke指令执行时,需要找到方法区中 instanceKlass 中保存的方法相关的字节码信息。但是方法区中有很多类每一个类又包含很多个方法,怎么精确地定位到方法的位置呢?

静态绑定

1、编译期间,invoke指令 会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名+方法名+返回值+参数。
2、在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定。静态绑定适用于处理静态方法、私有方法、或者使用 final 修饰的方法,因为这些方法不能被继承之后重写。
invokestatic
invokespecial
final 修饰的 invokevirtual

动态绑定

对于 非static、非private、非final的方法,有可能存在子类重写方法,那么就需要通过 动态绑定 来完成方法地址绑定的工作。比如在这段代码中,调用的其实是 Cat类对象的eat方法,但是编译完之后虚拟机指令中调用的是 Animal类的 eat方法。这就需要在运行过程中通过 动态绑定找到cat类的eat方法,这样就实现了多态。

image-20241014121605362

动态绑定是基于 方法表 来完成的,invokevirtual 使用了虚方法表(vtable),invokeinterface 使用了接口方法表(itable),整体思路类似。所以接下来使用 invokevirtual 和 虚方法表 来解释整个过程。
每个类中都有一个虚方法表,本质上它是一个数组,记录了方法的地址。子类方法表中包含父类方法表中的所有方法;
子类如果重写了父类方法,则使用自己类中方法的地址进行替换。

image-20241014121819582

  • 产生 invokevirtual 调用时,先根据对象头中的类型指针找到方法区中 InstanceClass对象,获得虚方法表。再根据虚方法表找到对应的对方,获得方法的地址,最后调用方法。

image-20241014121951828

总结

image-20241014123643373

异常捕获的原理

在 Java 中,程序遇到异常时会向外抛出,此时可以使用 try-catch 捕获异常 的方式将异常捕获并继续让程序按程序员设计好的方式运行。比如如下代码:在 try代码块 中如果抛出了 Exception对象 或者 子类对象,则会进入 catch分支。异常捕获机制的实现,需要借助于编译时生成的异常表。

image-20241015220030147

异常捕获的原理

异常表在编译期生成,存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
image-20241015220202107

在 位置2到 4字节码指令 执行范围内,如果出现了 Exception对象 的异常或者子类对象异常,直接跳转到 位置7的指令。也就是 i = 2代码 位置。

程序运行中触发异常时,Java虚拟机 会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java虚拟机 会判断所抛出的异常和该条目想要捕获的异常是否匹配。
1、如果匹配,跳转到“跳转PC"对应的字节码位置。
2、如果遍历完都不能匹配,说明异常无法在当前方法执行时被捕获,此方法栈帧直接弹出,在上一层的栈帧中进行异常捕获的查询。

image-20241015220521697

多个 catch分支情况下,异常表会从上往下遍历,先捕获 RuntimeException,如果捕获不了,再捕获 Exception。

image-20241015220623394

同理,multi-catch 的写法也是一样的处理过程,多个 catch分支情况下,异常表会从上往下遍历,先捕获 RuntimeException,如果捕获不了,再捕获 lOException。

image-20241015220717750

finally 的处理方式就相对比较复杂一点了,分为以下几个步骤:

1、finally 中的字节码指令会插入到 try 和 catch代码块中,保证在 try 和 catch 执行之后一定会执行 finally 中的代码。
2、如果抛出的异常范围超过了 Exception,比如 Error 或者 Throwable,此时也要执行 finally,所以异常表中增加了两个条目。覆盖了 try 和 catch 两段字节码指令的范围,any代表可以捕获所有种类的异常。

image-20241015221103172

JIT 即时编译器

在 Java中,JIT即时编译器是一项用来提升应用程序代码执行效率的技术。字节码指令被 Java虚拟机 解释执行,如果有一些指令执行频率高,称之为 热点代码,这些字节码指令则被 JIT即时编译器 编译成机器码同时进行一些优化,最后保存在内存中,将来执行时直接读取就可以运行在计算机硬件上了。

image-20241015223143520

在 HotSpot 中,有三款即时编译器,C1、C2 和 Graal,其中 Graal 在 GraalVM 章节中已经介绍过。****
C1 编译效率比 C2 快,但是优化效果不如 C2。所以 C1 适合优化一些执行时间较短的代码,C2 适合优化服务端程序中长期执行的代码。

image-20241015223320889

JDK7 之后,采用了分层编译的方式,在 JVM 中 C1 和 C2 会一同发挥作用,分层编译将整个优化级别分成了 5个等级。

image-20241015223515138

C1即时编译器 和 c2即时编译器 都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务。

一般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计。

image-20241015223650162

详细来看看 c1 和 c2 是如何进行协作的:

image-20241015223726748

image-20241015223741126

案例:测试 JIT 即时编译器的优化效果

image-20241015223850072

import java.util.concurrent.TimeUnit;

// 执行 5轮 预测,每次持续 1 秒
@Warmup(interations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
// 执行一次测试
@Fork(value = a, jvmArgsAppend = {"-Xmslg", "-Xmx1g"})
// 显示平均时间,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class MyJITBenchmark {
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int jitTest() {
        int sum = 0;
        for (int i = 0; i < 10000000; i++) {
            sum = add(sum, 100);
        }
        
        return sum;
    }
    
    // 禁用 JIT
	@Benchmark
    @Fork(value=1, jvmArgsAppend={"-Xint"})
    public void testNoJit(Blackhole blackhole) {
        int i = jitTest();
        blackhole.consume(i);
    }
    
    // 只使用 c1 1层
    @Benchmark
    @Fork(value = 1, jvmArgsAppend = {"-XX:TieredStopAtLevel=1"})
    public void testC1(Blackhole blackhole) {
        int i = jitTest();
        blackhole.consume(i);
    }
    
    @Benchmark
    public void testMethod(Blackhole blackhole) {
        int i = jitTest();
        blackhole.consume(i);
    }
    
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            		.include(MyJITBenchmark.class.getSimpleName())
            		.forks(1)
            		.build();
        		
    }
}
JIT 编译器 主要优化手段是 方法内联 和 逃逸分析。

方法内联(Method Inline):方法体中的字节码指令直接复制到调用方的字节码指令中,节省了创建栈帧的开销。

image-20241015225953182

案例:使用JIT Watch工具查看方法内联的优化结果

需求:
1、安装 JIT Watch 工具,下载源码:https://github.com/AdoptOpenJDK/jitwatch/tree/1.4.2
2、使用资料中提供的脚本文件直接启动。
3、添加源代码目录,点击沙箱环境 RUN:

image-20241015230152978

image-20241015230747636

方法内联的限制

并不是所有的方法都可以内联,内联有一定的限制:
1、方法编译之后的字节码指令总大小 < 35字节,可以直接内联。。(通过 -XX:MaxlnlineSize=值 控制)
2、方法编译之后的字节码指令总大小 < 325字节,并且是一个热方法。。(通过 -XX:FreqlnlineSize=值 控制)
3、方法编译生成的机器码不能大于 1000字节。(通过-XX:InlineSmallCode = 值控制)
4、一个接口的实现必须小于 3 个,如果大于三个就不会发生内联。

image-20241015231042030

案例:String 的 toUpperCase 方法性能优化

需求:
1、String 的 toUpperCase 为了适配很多种不同的语言导致方法编译出来的字节码特别大,通过编写一个方法只处理 a-z 的大写转换提升性能。
2、通过 JITWatch 观察方法内联的情况。

逃逸分析

逃逸分析指的是如果 JIT 发现在方法内创建的对象不会被外部引引用**,那么就可以采用锁消除、标量替换等方式进行优化。**

image-20241015231927503

逃逸分析-锁消除

image-20241015232059726

逃逸分析-标量替换

逃逸分析真正对性能优化比较大的方式是标量替换,在 Java 虚拟机中,对象中的基本数据类型称为标量,引用的其他对象称为聚合量标量替换指的是如果方法中的对象不会逃逸,那么其中的标量就可以直接在栈上分配。

image-20241015232308732

案例:逃逸分析的优化测试

image-20241015232416994

JIT 优化的几点建议

根据 JIT 即时编器优化代码的特性,在编写代码时注意以下几个事项,可以让代码执行时拥有更好的性能:
1、尽量编写比较小的方法,让方法内联可以生效。
2、高频使用的代码,特别是第三方依赖库甚至是 JDK 中的,如果内容过度复杂是无法内联的,可以自行实现一个特定的优化版本。
3、注意下接口的实现数量,尽量不要超过2个,否则会影响内联的处理。
4、高频调用的方法中创建对象临时使用,尽量不要让对象逃逸。

垃圾回收器原理

G1 垃圾回收器原理

G1 垃圾回收器原理
G1 垃圾回收有两种方式:

  • 1.年轻代回收(Young GC)
  • 2、混合回收 (Mixed GC)
G1 垃圾回收器原理-年轻代回收

年轻代回收只扫描年轻代对象(Eden + Survivor),所以从 GC Root 到年轻代的对象或者年轻代对象引用了其他年轻代的对象都很容易扫描出来。

image-20241015233939109

这里就存在一个问题,年轻代回收只扫描年轻代对象(Eden+Survivor),如果有老年代中的对象引用了年轻代中的对象,我们又如何知道呢?

方案1:从 GC Root开始,扫描所有对象,如果年轻代对象在引用链上,就标记为存活

image-20241015234119553

方案2:维护一个详细的表,记录哪个对象被哪个老年代引用了。在年轻代中被引用的对象,不进行回收。

image-20241015234238874

方案2 的第一次优化:只记录 Region 被哪些对象引用了。这种引用详情表称为 记忆集 RememberedSet(简称 RS 或 RSet):是一种记录了从非收集区域对象引用收集区域对象的这些关系的数据结构。扫描时将记忆集中的对象也加入到 GC Root中,就可以根据引用链判断哪些对象需要回收了。

image-20241015234445733

方案2 的第二次优化:将所有区域中的内存按一定大小划分成很多个,每个块进行编号。记忆集中只记录对块的引用关系。如果一个块中有多个对象,只需要引用一次,减少了内存开销。

image-20241015234819744

G1 垃圾回收器原理-卡表(Card Table)

每一个 Region 都拥有一个自己的卡表,如果产生了跨代引用(老年代引用年轻代),此时这个 Region 对应的卡表上就会将字节内容进行修改,JDK8 源码中0代表被引用了称为脏卡。这样就可以标记出当前 Region 被老年代中的哪些部分引用了。那么要生成记忆集就比较简单了,只需要遍历整个卡表,找到所有脏卡。

image-20241015235135215

image-20241015235157695

image-20241015235242617

G1 垃圾回收器原理- 写屏障

JVM 使用写屏障(Write Barrier)技术,在执行引用关系建立的代码时,可以在代码前和代码后插入一段指令,从而维护卡表。
记忆集中不会记录新生代到新生代的引用,同一个 Region 中的引用也不会记录。

image-20241015235510531

G1 垃圾回收器原理-记忆集的生成流程

记忆集的生成流程分为以下几个步骤:
1、通过写屏障获得引用变更的信息。
2、将引用关系记录到卡表中,并记录到一个脏卡队列中。
3、JVM 中会由 Refinement 线程定期从脏卡队列中获取数据,生成记忆集。不直接写入记忆集的原因是避免过多线程并发访问记忆集。

image-20241015235712546

image-20241015235735518

image-20241015235823060

image-20241015235839581

总结

image-20241015235926024

G1垃圾回收器原理-混合回收

image-20241016000332947

G1垃圾回收器原理-初始标记

初始标记会暂停所有用户线程,只标记从 GC Root 可直达的对象,所以停顿时间不会太长。采用三色标记法进行标记 三色标记法在原有双色标记(黑也就是1代表存活,白 0 代表可回收)增加了一种灰色,采用队列的方式保存标记为灰色的对象。
**黑色:存活,**当前对象在 GC Root 引用链上,同时他引用的其他对象也都已经标记完成。
**灰色:待处理,**当前对象在 GC Root 引用链上,他引用的其他对象还未标记完成。
**白色:可回收,**不在 GC Root 引用链上。

image-20241016000743962

接下来进入并发标记阶段,继续进行未完成的标记任务。此阶段和用户线程并发执行。
从灰色队列中获取 尚未完成标记的 对象B。标记 B 关联的 A 和 C 对象,由于 A对象 并未引用其他对象,可以直接标记成黑色,
而 B 也完成了所有引用对象的标记,也标记为黑色。C对象 有引用对象E,所以先标记成灰色。所以剩余 对象F就是 白色,可回收。

image-20241016000928085

三色标记存在一个比较严重的问题,由于用户线程可能同时在修改对象的引用关系,就会出现错标的情况,比如:
这个案例中正常情况下,B 和 c 都会被标记成黑色。但是在 BC标记前,用户线程执行了B.c = nuLl;将 B 到 c 的引用去除了。同时执行了A.c = C;添加了 A 到 C 的引用。此时会出现错标的情况,C是白色可回收。

image-20241016001113597

G1 为了解决这个问题,使用了 SATB技术(Snapshot At The Beginning,初始快照)。SATB技术 是这样处理的:
1、标记开始时创建一个快照,记录当前所有对象,标记过程中新生成的对象直接标记为黑色,

2、采用前置写屏障技术,在引用赋值前比如 B.c = nuLl 之前,将之前引用的 对象c 放入 SATB 待处理队列中。SATB队列 每个线程都有一个,最终会汇总到一个大的 SATB队列中。

image-20241016001425653

G1 垃圾回收器原理 - 最终标记

最终标记会暂停所有用户线程,主要是为了处理 SATB 相关的对象标记。这一步中,将所有线程的 SATB队列 中剩余的数据合并到总的 SATB队列 中,然后逐一处理。
SATB队列 中的对象,默认按照存活处理,同时要处理他们引用的对象。SATB的缺点是在本轮清理时可能会将不存活的对象标记成存活对象,产生了一些所谓的浮动垃圾,等到下一轮清理时才能回收

image-20241016001706007

练习题

image-20241016001727348

G1 垃圾回收器原理-转移

转移的步骤如下:
1、根据最终标记的结果,可以计算出每一个区域的垃圾对象占用内存大小,根据停顿时间,选择转移效率最高(垃圾对象最多)的几个区域。
2、转移时先转移 GC Root直接引用的对象,然后再转移其他对象。

image-20241016001924689

转移的步骤如下:
1、根据最终标记的结果,可以计算出每一个区域的垃圾对象占用内存大小,根据停顿时间,选择转移效率最高(垃圾对象最多)的几个区域。
2、转移时先转移 GC Root 直接引用的对象,然后再转移其他对象。
3、回收老的区域,如果外部有其他区域对象引用了转移对象,也需要重新设置引用关系。

image-20241016002019881

ZGC 原理

什么是 ZGC?

image-20241016002147948

image-20241016002321801

G1 转移时需要停顿的主要原因

在转移时,能不能让用户线程和 GC 线程同时工作呢?考虑下面的问题:
转移完之后,需要将 A 对对象的引用更改为新对象的引用。但是在更改前,执行 A.c.count = 2,此时更改的是转移前对象中的属性

更改引用之后,A 引用了转移之后的对象,此时获取 A.c.count 发现属性值依然是 1。这样就产生了问题,所以 G1 为了解决问题,在转移过程中需要进行用户线程的停止。ZGC 和 Shenandoah 解决了这个问题,让转移过程也能够并发执行。

image-20241016002609574

ZGC 的解决方案

在 ZGC 中,使用了读屏障 Load Barrier 技术,来实现转移后对象的获取。当获取一个对象引用时,会触发读后的屏障指令,如果对象指向的不是转移后的对象,用户线程会将引用指向转移后的对象。

image-20241016002818562

着色指针 (Colored Pointers)

访问对象引l用时,使用的是对象的地址。在 64位虚拟机中,是8个字节可以表示接近无限的内存空间所以一般内存中对象,高几位都是 0 没有使用。着色指针就是利用了这多余的几位,存储了状态信息。

image-20241016002932791

着色指针将原来的 8字节 保存地址的指针拆分成了三部分:
1、最低的 44位,用于表示对象的地址,所以最多能表示 16TB 的内存空间。
2.,中间4位是颜色位,每一位只能存放 0 或者 1,并且同一时间只有其中一位是 1

image-20241016003149476

正常应用程序使用8个字节去进行对象的访问,现在只使用了44位,不会产生问题吗?
应用程序使用的对象地址,**只是虚拟内存,操作系统会将虚拟内存转换成物理内存。**而 ZGC 通过操作系统更改了这层逻辑。所以不管颜色位变成多少,指针指向的都是同一个对象。

image-20241016003241063

ZGC 的内存划分

在 ZGC 中,与 G1垃圾回收器一样将堆内存划分成很多个区域,这些内存区域被称之为 Zpage。
Zpage 分成 三类大中小,管控粒度比 G1 更细,这样更容易去控制停顿时间。
小区域:2M,只能保存 256KB 内的对象。
中区域:32M,保存 256KB-4M 的对象。
大区域:只保存一个大于 4M 的对象。

初始标记阶段

image-20241016003400443

并发标记阶段

遍历所有对象,标记可以到达的每一个对象是否存活**,用户线程使用读屏障,如果发现对象没有完成标记也会帮忙进行标记**

image-20241016003456813

并发处理阶段

选择需要转移的 Zpage,并创建转移表,用于记录转移前对象和转移后对象地址,

image-20241016003621934

转移开始阶段

转移 GC Root 直接关联的对象,不转移的对象 remapped值设置成1,避免重复进行判断。转移之后将两个对象的地址记入转移映射表

image-20241016003814901

并发转移阶段

将剩余对象转移到新的 ZPage 中,转移之后将两个对象的地址记入转移映射表。

image-20241016003851202

转移完之后,转移前的 Zpage 就可以清空了,转移表需要保留下来。

image-20241016003927973

此时,如果用户线程访问4 对象引用的5对象,会通过读屏障,将4 对5 的引用进行重置,修改为对5 的引用,同时将 remap 标记为 1 代表已经重新映射完成

image-20241016004020422

并发转移阶段结束之后,这一轮的垃圾回收就结束了,但其实并没有完成所有指针的重映射工作,这个工作会放到下一阶段,与下一阶段的标记阶段一起完成(因为都需要遍历整个对象图)。

image-20241016004109629

第二次垃圾回收的初始标记阶段

第二次垃圾回收的初始标记阶段,沿着 GC Root 标记对象。

image-20241016004234489

image-20241016004306910

image-20241016004329666

并发转移阶段 并发问题

image-20241016004451528

分代 ZGC 的设计

在 JDK21 之后,ZGC 设计了年轻代和老年代,这样可以让大部分对象在年轻代回收,减少老年代的扫描次数,同样可以提升一定的性能。同时,年轻代和老年代的垃圾回收可以并行执行。

image-20241016004534366

分代 ZGC 的设计

image-20241016004618455

ZGC 核心技术

1、着色指针(Colored Pointers)
着色指针将原来的 8字节保存地址的指针拆分成了三部分,不仅能保存对象的地址,还可以保存当前对象所属的
状态。 不支持32位系统、不支持指针压缩
2、读屏障(Load Barrier)
在获取对象引I用判断对象所属状态,如果所属状态和当前 GC阶段 的颜色状态不一致,由用户线程完成本阶段的
工作。会损失一部分的性能,大约在5%~10%之间。

ShenandoahGC 原理

ShenandoahGC 的设计

ShenandoahGC 和 ZGC 不同,ShenandoahGC 很多是使用了 G1源代码改造而成,所以在很多算法、数据结构的定义上,与 G1 十分相像,而 ZGC 是完全重新开发的一套内容。

1、ShenandoahGC 的区域定义与 G1 是一样的。
2、没有着色指针,通过修改对象头的设计来完成并发转移过程的实现。
3**、ShenandoahGC 有两个版本,1.0版本存在于 JDK8 和 JDK11 中,后续的 JDK 版本中均使用2.0版本。**

ShenandoahGC 的设计-1.0版本

如果转移阶段未完成,此时转移前的对象和转移后的对象都会存活。如果用户去访问数据,需要使用转移后的数据。ShenandoahGC 使用了读前屏障,根据对象的前向指针来获取到转移后的对象并读取。

image-20241016005402854

写入数据时,也会使用写前屏障,判断 Mark Word 中的 GC 状态,如果 GC 状态为0证明没有处于 GC过程中,直接写入,如果不为0则根据 GC状态值确认当前处于垃圾回收的哪个阶段,让用户线程执行垃圾回收相关的任务。

image-20241016005430844

ShenandoahGc的设计 - 2.0版本
1.0版本的缺点:
1、对象内存大大增加,每个对象都需要增加8个字节的前向指针,基本上会占用5%-10%的空间。
2、读屏障中加入了复杂的指令,影响使用效率。
2.0版本 优化了前向指针的位置,仅转移阶段将其放入了Mark Word中。

image-20241016005204607

ShenandoahGC 的执行流程

image-20241016005244643

并发转移阶段 并发问题

如果用户线程在帮忙转移时,ShenandoahGC 线程也发现这个对象需要复制,那么就会去尝试写入前向指针,使用了类似 CAS 的方式来实现,只有一个线程能成功修改,其他线程会放弃转移的操作。

image-20241016005336818

标签:Java,进阶,对象,虚拟机,引用,内存,方法,字节
From: https://blog.csdn.net/2301_79083000/article/details/143055939

相关文章

  • Java 初学 day15
    java151、CollectionsCollections:是java针对集合操作专门提供的一个工具类静态方法publicstatic<T>voidsort(List<T>list)publicstatic<T>intbinarySearch(List<?>list,Tkey)publicstatic<T>Tmax(Collection<?>coll)publicstaticvoi......
  • 基于Java大数据背景下求职信息的推荐系统设计与实现
    基于Java大数据背景下求职信息的推荐系统设计与实现计算机毕业设计案例基于Java的食谱/菜谱管理系统基于Java的老年人健康管理系统基于Java的课程评价系统基于微信小程序的充电桩管理系统基于ASP.NET的OA系统基于PHP的在线学习网基于Java的老年人健康管理系统基于S......
  • 使用AES 128位加解密,加解密模式采用CBC,填充模式采用PKCS5Padding的Java工具方法示例
    importjavax.crypto.Cipher;importjavax.crypto.spec.IvParameterSpec;importjavax.crypto.spec.SecretKeySpec;importjava.util.Base64;publicclassAESUtils{privatestaticfinalStringAES_ALGORITHM="AES/CBC/PKCS5Padding";private......
  • java_day17_JDBC、登录注册修改案例
    一、JDBCJDBC编写六步走:1、注册驱动,告诉java程序我们要链接什么数据库【mysql为案例】5.1.x驱动包中的驱动类路径:【com.mysql.jdbc.Driver】8.x.x驱动包中的驱动类路径:【com.mysql.cj.jdbc.Driver】2、创建与数据库的链接对象......
  • 【Java系列】基于SSM框架的房屋中介服务平台设计与实现(源码+文档+部署讲解等)
    文章目录1.前言2.详细视频演示3.程序运行示例图4.文档参考5.技术框架5.1后端采用SpringBoot框架5.2前端框架Vue5.3程序操作流程6.选题推荐7.原创毕设案例8.系统测试8.1系统测试的目的8.2系统功能测试9.代码参考10.为什么选择我?11.获取源码1.前言......
  • java中的类型转换
    一、自动类型转换1.在java中,变量参与运算的时候会自动提升数据类型byte,short,char->int->long->float->double二、强制类型转换1.语句格式:目标数据类型变量名=(目标数据类型)要转型的变量值或者表达式publicclassDataTypeDemo2{publicstaticvoidmain(Str......
  • 【Java系列】基于Javaweb的在线餐饮管理系统设计与实现(源码+文档+部署讲解等)
    文章目录1.前言2.详细视频演示3.程序运行示例图4.文档参考5.技术框架5.1后端采用SpringBoot框架5.2前端框架Vue5.3程序操作流程6.选题推荐7.原创毕设案例8.系统测试8.1系统测试的目的8.2系统功能测试9.代码参考10.为什么选择我?11.获取源码1.前言......
  • python+uniapp微信小程序线上点餐管理信息系统java+nodejs-毕业设计
    前端开发框架:vue.js数据库mysql版本不限后端语言框架支持:1java(SSM/springboot)-idea/eclipse2.Nodejs+Vue.js-vscode3.python(flask/django)--pycharm/vscode4.php(thinkphp/laravel)-hbuilderx数据库工具:Navicat/SQLyog等都可以 随着科技的不断发展,移动互联网......
  • python+vue基于django/flask的在线投票管理系统java+nodejs-计算机毕业设计
    目录技术栈和环境说明具体实现截图预期达到的目标系统设计详细视频演示技术路线解决的思路性能/安全/负载方面可行性分析论证python-flask核心代码部分展示python-django核心代码部分展示研究方法感恩大学老师和同学源码获取技术栈和环境说明本系统以Python开发语言......
  • python+vue基于django/flask的美食分享推荐系统Java+nodejs-计算机毕业设计
    目录技术栈和环境说明具体实现截图预期达到的目标系统设计详细视频演示技术路线解决的思路性能/安全/负载方面可行性分析论证python-flask核心代码部分展示python-django核心代码部分展示研究方法感恩大学老师和同学源码获取技术栈和环境说明本系统以Python开发语言......