JVM | 基于类加载的一次完全实践

引言

我在上篇文章:JVM | 类加载是怎么工作的 中为你介绍了Java的类加载器及其工作原理。我们简单回顾下:我用一个易于理解的类比带你逐步理解了类加载的流程和主要角色:引导类加载器扩展类加载器应用类加载器。并带你深入了解了这些“建筑工人”如何从底层工作,搬运原材料(类)并将其完整地构建在Java虚拟机(JVM)的“建筑工地”上。然后,我们跟随一个具体的Building类,亲眼目睹了其在JVM中的生命周期。我在文章末尾留了几个问题,你还记得吗?

本篇文章,我将带你了解自定义类加载器的创建和使用。我们还将探索Java的SPI机制,了解它如何利用类加载器实现服务的动态发现和加载。接着,我们再来看下Tomcat的类加载机制,尤其是它的热部署多版本共存的实现,了解类加载机制在现实世界中的高级应用。


自定义类加载器的创建和使用

当我们的类涉及到一些安全的操作,或者我们想从网络或者其它地方加载类。这种情况,我们就会创建自定义的类加载器,重写findClass方法来完成这个特殊的加载逻辑。

沿用上篇文章的例子:假如工地来活了,要求建造一个复杂的建筑物,这个建筑物不仅包括了普通的房间(普通的类),还包括了一些特殊设计的房间(特殊的类)。
在这个情况下,你可能会需要一位专门的工人来处理这些特殊的房间。这位工人需要有特殊的技能和工具,才能按照设计图纸(类的字节码)正确地建造出房间。
接下来,我们来看下类加载器怎么创建与使用的。


创建类加载器

我们来实现一个类加载器,代码如下:


public class CustomClassLoader extends ClassLoader {private String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {// 先检查类是否已经被加载Class<?> cls = findLoadedClass(name);if (cls != null) {return cls;}try {// 如果类还未被加载,尝试使用父类加载器加载(不破坏双亲委派机制)cls = getParent().loadClass(name);} catch (ClassNotFoundException e) {// 父类加载器无法加载该类,那么就调用 findClass 尝试自己加载cls = findClass(name);}return cls;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 你的类加载逻辑...byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String className) {String fileName = getFileName(className);try {InputStream is = new FileInputStream(fileName);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead;while ((bytesNumRead = is.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}private String getFileName(String name) {int index = name.lastIndexOf('.');// 如果没有找到'.'则直接在classPath中查找if (index == -1) { return classPath + name + ".class";} else {return classPath + name.substring(index + 1) + ".class";}}
}

上面的类加载器CustomClassLoader 通过构造的方式传入文件路径。当我们要加载类时,它会调用loadClass方法从我们定义的类路径下读取字节流。好,
接下来,我们来使用我们自己定义的类加载器。

使用类加载器

代码如下:

// 把上篇文章的Building类放在桌面
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\xxx\\Desktop\\");try {// 使用自定义类加载器加载 Building 类,若你的包名不叫这个,请更换。Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);// 创建类的实例cls.newInstance();System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");} catch (Exception e) {e.printStackTrace();
}

执行!

Connected to the target VM, address: '127.0.0.1:4706', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:sun.misc.Launcher$AppClassLoader@3f3fdda9创建Disconnected from the target VM, address: '127.0.0.1:4706', transport: 'socket'
Process finished with exit code 0

类被AppClassLoader加载了,为啥? 原因是我的项目中有Building类, 这个类可以被应用类加载器加载,因此就轮不到·CustomClassLoader 加载了。我们把项目内的Building先改名一下。然后执行!

Connected to the target VM, address: '127.0.0.1:5032', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:org.kfaino.jvm.CustomClassLoader@6379b5ed创建
Disconnected from the target VM, address: '127.0.0.1:5032', transport: 'socket'Process finished with exit code 0

这次,我们的类成功被CustomClassLoader加载,并且加载的是我们桌面上的字节码文件。


使用Java自带的类加载器工具类

当然,如果你想要从外部加载字节码文件,可以不必这么繁琐。JDK提供了一个功能更强大的URLClassLoader。我们一起来看下它怎么用:

        // 把Building放在桌面URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};URLClassLoader customClassLoader = new URLClassLoader(urls);try {// 使用自定义类加载器加载 Building 类Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);// 创建类的实例cls.newInstance();System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");} catch (Exception e) {e.printStackTrace();}

我们执行看下:

Connected to the target VM, address: '127.0.0.1:9734', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:java.net.URLClassLoader@28634811创建
Disconnected from the target VM, address: '127.0.0.1:9734', transport: 'socket'Process finished with exit code 0

没有问题,本地的字节码文件Building 被成功读取。因此,当你有从外部读取字节码文件的需求,可以试试用JDK自带的·URLClassLoader类加载器。同时,它还提供了其它更强大的功能。

从网络URL加载类和资源

若你想从网络加载字节码文件,你可以这么做:

URL url = new URL("http://www.github.com/xxx/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

更多的URL加载类和资源

细心的你肯定发现URLClassLoader的构造入参是数组类型,也就意味着可以传入多个URL,具体用法如下:

URL url1 = new URL("http://www.github.com/xxx1/");
URL url2 = new URL("http://www.github.com/xxx2/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url1, url2});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

从JAR文件加载类和资源

它可以从完整的jar包中读取字节码文件,代码如下:

File file = new File("/xxx/jarfile.jar");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

加载外部配置文件

它可以从外部读取配置文件,代码如下:

File file = new File("/xxx/resources/");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
URL resourceUrl = urlClassLoader.getResource("xxx.properties");
InputStream stream = resourceUrl.openStream();
Properties properties = new Properties();
properties.load(stream);

自定义类加载器的注意事项

类加载器在类的加载过程中起着至关重要的作用。因此,在使用时,我们必须倍加警惕。接下来,让我们来看下哪些需要注意的问题:

内存泄漏

长期存活的类加载器持有类的引用就会导致内存泄露。为了避免这个问题,我建议你在关键代码处适当使用如下代码,让旧的类加载器和类实例进行解绑:

// 释放对 ClassLoader 的引用,使其有可能被垃圾回收
classLoader = null;
System.gc();

当然,每个问题都需要我们针对性地分析。我这里只是提供可能导致内存泄漏的一个说法。实际上,引发内存泄漏的原因有很多,如果你在工作中遇到了这个问题,可以使用一些可视化分析工具来综合性的分析。

不要轻易破坏双亲委派机制

双亲委派模型是为了保证Java核心类库的安全性。当然,我们也可以选择破坏双亲委派模型,前提是,你已考虑好这些风险并规避。

在上述代码中,我们没有违背双亲委派模型的原则。回顾一下我们在之前文章中提到的双亲委派模型的概念:在类加载的过程中,我们首先会让父类加载器进行加载,只有在父类加载器无法加载的情况下,我们才会使用自定义的类加载器进行加载。顺便我把上篇缺少的自定义类加载器也补充进去,你可以看下:
在这里插入图片描述

线程安全问题

如果我们在多线程中使用类加载器,可能会导致类被重复加载多次。除了会浪费资源外,还会导致我们一些静态初始化代码被执行多次,造成一些诡异的问题。我在上篇专栏中说到,解决线程安全的方式有多种。为了保险起见,你可以采用同步方案来解决它。


自定义类加载器使用场景

在上面的例子中,我为你展示如何从外部加载字节码文件。接下来,我们来看下还有哪些使用场景:

安全检查

安全,是软件工程中永恒的话题。为了防止第三方的潜在干扰,我们通常在获取外部文件的同时,做一些过滤的机制。你看代码:

public class SecurityCheckingClassLoader extends ClassLoader {private static final String CLASS_NAME_PREFIX = "Safe";@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {// 加上你想要的安全校验逻辑if (!name.startsWith(CLASS_NAME_PREFIX)) {throw new ClassNotFoundException("不安全的类: " + name);}return super.loadClass(name);}
}

上面,我为你举了一个简单的例子。我在加载类方法loadClass前校验类名的前缀,如果你不是Safe开头的类,我们就不予放行。

解密加密的类文件

网络环境充满不确定性,如果你选择从网络获取字节码文件,我建议你首先做好加密工作。既然是从外部获取文件,我们可以通过继承URLClassLoader来实现。代码如下:

import java.net.URL;
import java.net.URLClassLoader;public class DecryptingURLClassLoader extends URLClassLoader {public DecryptingURLClassLoader(URL[] urls) {super(urls);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 获取字节码文件byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();}byte[] decryptedClassData = decrypt(classData);return defineClass(name, decryptedClassData, 0, decryptedClassData.length);}private byte[] loadClassData(String className) {// 从网络中获取字节码文件}private byte[] decrypt(byte[] classData) {// 解密字节码文件}
}

上面两个是工作中相对比较容易用到的两种场景,还有没有其它类加载器的优秀案例呢?


基于自定义类加载器的其它实现

我们从官网文档中得知,Tomcat会为每个web应用创建类加载器。图片如下:
在这里插入图片描述
现在,我们来看下Tomcat用自定义加载器做了哪些事情。

Tomcat中的热部署

先来解释下什么是热部署? 热部署是指我们的应用在运行过程中,可以在不关闭应用的前提下更新应用。
假如你想开启热部署,你可以在context.xml里面设置reloadable="true”。限于篇幅有限,我在这里只是为你说明Tomcat热部署到底是怎么实现的,如果你感兴趣,建议您亲自动手实操。

热部署实现原理

Tomcat通过一个BackgroundProcessor 后台线程周期性的检查web 应用的 WEB-INF/classes 和 WEB-INF/lib 目录下的 class 文件或 jar 文件是否有变化。具体做法是比较文件的最后修改时间和上次记录的最后修改时间是否一致。如果有变化,就触发 web 应用的重载。
Tomcat重新加载一个web应用时,会创建一个新的WebappClassLoader实例,并使用这个新的类加载器来加载web应用的类。这样,新的类加载器就会加载最新版本的类,而旧的类加载器加载的旧版本的类会在它们不再被引用时被垃圾回收。这就是Tomcat的热部署。

Tomcat中的多版本共存

那什么是多版本共存? 我们在上面说到,每一个web应用都有自己独立的类加载器,这就意味着每一个web应用都有自己的类和库的命名空间。即使同一Tomcat实例中运行的多个web应用使用了同名的类和库,它们也不会相互干扰。
也就是说Tomcat的多版本共存关键也在于每个应用都有不同的类加载器。
限于篇幅有限,更多细节,建议你移步到官方文档,我在文末参考文献中为你贴出官方地址。


ServiceLoader和SPI

我们经常会听到许多Java框架包括Dubbo、Spring等都使用了SPI这个机制,SPI究竟是什么东西?ServiceLoader和我们今天讲的类加载器又有什么关系?

SPI(Service Provider Interface)是什么?

我从服务提供方和服务调用方两个视角来为你讲解:

服务提供方

对于服务提供方而言,它只需要根据接口实现对应方法。并且把配置文件放在META-INF/services/ 告知服务调用方。

服务调用方

服务调用方根据约定,使用ServiceLoader来遍历实现该约定的实现类。加载进内存中供服务提供方调用。

为了加深理解,我为你画了一张图:
在这里插入图片描述

ServiceLoader和类加载器的关系

它和类加载器又有什么关系?我通过代码为你分析,你看:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}

如果你不指定类加载器,load方法默认获取当前线程的类加载器去加载该类。它通过扫描META-INF/services/目录下的配置文件,找到服务接口的实现类的全限定名。有了全限定名就等于掌握了花名册,当我们遍历ServiceLoader时,服务加载器会通过类加载器将这些服务提供者实例化,这样我们就可以使用这些服务了。


文中重要部分解析

命名空间

我在上面Tomcat多版本共存中提到命名空间,什么是命名空间?每个类加载器实例其实就是一个命名空间。也就是说,在一个应用程序中允许一个类被多个类加载器实例加载,并且共存于应用程序中。暂停30秒,思考一下,这样会出现什么问题。
好,在回答这个问题之前,我为你展示一个代码:

try {URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};URLClassLoader customClassLoader1 = new URLClassLoader(urls);URLClassLoader customClassLoader2 = new URLClassLoader(urls);Class cls1 = customClassLoader1.loadClass("org.kfaino.jvm.Building");Class cls2 = customClassLoader2.loadClass("org.kfaino.jvm.Building");Object obj1 = cls1.newInstance();Object obj2 = cls2.newInstance();System.out.println("obj1 class: " + obj1.getClass());System.out.println("obj2 class: " + obj2.getClass());System.out.println("obj1 class loader: " + obj1.getClass().getClassLoader());System.out.println("obj2 class loader: " + obj2.getClass().getClassLoader());// Building对象已经重写hashCode和equals方法System.out.println("obj1 equals obj2: " + obj1.equals(obj2));} catch (Exception e) {e.printStackTrace();
}

在Building已经重写hashCode和equals方法的前提下,obj1 equals obj2: 会是true吗?我们看下结果:

建筑蓝图已被创建!
建筑蓝图已被创建!
obj1 class: class org.kfaino.webTemplate.jvm.Building
obj2 class: class org.kfaino.webTemplate.jvm.Building
obj1 class loader: java.net.URLClassLoader@da236ecf
obj2 class loader: java.net.URLClassLoader@8e02f9da
obj1 equals obj2: falseProcess finished with exit code 0

结果是否定的,和我之前说的吻合。也就是说使用不同的类加载器,不同类加载器的对象(命名空间不同),在JVM中就是类型不一致的。

生产环境中的热部署

BackgroundProcessor 后台线程,需要周期性地检查(checkResources())文件的状态。处于对性能方面的考虑,在生产环境中,通常会关闭 Tomcat 的热部署功能。

SPI配置文件存放位置META-INF/services/可以更改吗?

查阅官方文档,我们可以知道SPI是JDK内置的一种服务提供发现机制。在SPI机制中,服务提供者的配置文件默认放在META-INF/services/目录下。这是Java SPI规范的一部分,无法更改。


总结

至此,本篇完结。我们来回顾下:首先,我带你创建并使用了类加载器完成从本地文件夹下加载自己的类。这些工作我们可以通过Java自带的类加载器来简化,我也为你演示其用法。当然,我们在使用自定义类加载器要格外注意,因为涉及到类初始化往往你会碰到一些不可预见的诡异BUG。然后,我为你介绍自定义类加载器场景的使用场景。顺便看一下Tomcat和Java是怎么用自定义类加载器的特性实现高级功能的。


常见面试题

如何自定义类加载器?

在什么情况下会需要自定义类加载器?

Tomcat的类加载器有什么特点?如何实现热部署和多版本共存?#### 什么是ServiceLoader和SPI,它们如何利用类加载器?

类加载器可能存在的问题有哪些?


参考文献

  1. Java Guide SPI机制详解
  2. class-loader-howto
  3. baeldung-java-classloaders
  4. Inside the Java Virtual Machine
  5. 老大难的 Java ClassLoader 再不理解就老了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/13505.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

剑指offer12 矩阵中的路径 13 机器人的运动范围 34.二叉树中和为某一值得路径

class Solution { public:bool exist(vector<vector<char>>& board, string word) {int rowboard.size(),colboard[0].size();int index0,i0,j0;if(word.size()>row*col) return 0;//vector<vector<int>> visit[row][col];//标记当前位置有没有…

算法之归并排序算法

归并&#xff08;Merge&#xff09;排序法是将两个&#xff08;或两个以上&#xff09;有序表合并成一个新的有序表&#xff0c;即把待排序序列 分为若干个子序列&#xff0c;每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 public class MergeSortTest {public …

到底什么是前后端分离

目录 Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 总结 Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 理解它们的区别有助于我们进行对应产品的测试工作。 前后端不分离 在早期&#xff0c;Web 应用开发主要采用前后端不…

linux系统下(centos7.9)安装Jenkins全流程

一、卸载历史版本 # rpm卸载 rpm -e jenkins# 检查是否卸载成功 rpm -ql jenkins# 彻底删除残留文件 find / -iname jenkins | xargs -n 1000 rm -rf二、环境依赖安装 yum -y install epel-releaseyum -y install daemonize三、安装Jenkins Jenkins官网传送带&#xff1a; …

《零基础入门学习Python》第071讲:GUI的终极选择:Tkinter8

虽然我们能用 tkinter 设计不少东西了&#xff0c;但是不少同学还是感觉对这个界面编程掌控得还不够多&#xff0c;说白了&#xff0c;就是我们现在还没办法随心所欲的去绘制我们想要的界面&#xff0c;但是不瞒你说&#xff0c;今天的这一节课将会给你的人生乃至人生观带来翻天…

苍穹外卖-day07

苍穹外卖-day07 本项目学自黑马程序员的《苍穹外卖》项目&#xff0c;是瑞吉外卖的Plus版本 功能更多&#xff0c;更加丰富。 结合资料&#xff0c;和自己对学习过程中的一些看法和问题解决情况上传课件笔记 视频&#xff1a;https://www.bilibili.com/video/BV1TP411v7v6/?sp…

Rust vs Go:常用语法对比(五)

题图来自 Rust vs Go 2023[1] 81. Round floating point number to integer Declare integer y and initialize it with the rounded value of floating point number x . Ties (when the fractional part of x is exactly .5) must be rounded up (to positive infinity). 按规…

KWP2000协议和OBD-K线

KWP2000最初是基于K线的诊断协议&#xff0c; 但是由于后来无法满足越来越复杂的需求&#xff0c;以及自身的局限性&#xff0c;厂商又将这套应用层协议移植到CAN上面&#xff0c;所以有KWP2000-K和KWP2000-CAN两个版本。 这篇文章主要讲基于K线的早期版本协议&#xff0c;认…

零售企业信息化系统建设与应用解决方案

导读&#xff1a;原文《零售企业信息化系统建设与应用解决方案ppt》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 完整版领取方式 如需获取完整的电子版内容参考学习…

基于Kaggle训练集预测的多层人工神经网络的能源消耗的时间序列预测(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f308;3 Matlab代码实现 &#x1f389;4 参考文献 &#x1f4a5;1 概述 本文为能源消耗的时间序列预测&#xff0c;在Matlab中实现。该预测采用多层人工神经网络&#xff0c;基于Kaggle训练集预测未来能源消耗。 &am…

使用镜像搭建nacos集群

安装并配置 docker 1 先安装docker //1.查看操作系统的发行版号 uname -r//2.安装依赖软件包 yum install -y yum-utils device-mapper-persistent-data lvm2//3.设置yum镜像源 //官方源&#xff08;慢&#xff09; yum-config-manager --add-repo http://download.docker.co…

第十二章:priority_queue类

系列文章目录 文章目录 系列文章目录前言priority_queue的介绍priority_queue的使用容器适配器什么是容器适配器STL标准库中stack和queue的底层结构 总结 前言 priority_queue是容器适配器&#xff0c;底层封装了STL容器。 priority_queue的介绍 priority_queue文档介绍 优先…

IDEA中文UT方法执行报错问题、wps默认保存格式

wps默认保存格式、IDEA中文UT方法执行报错问题 背景 1、wps修改文件后&#xff0c;编码格式从UTF-8-bom变成UTF-8&#xff08;notepad可以查看&#xff09;&#xff1b; 2、IDEA中文UT执行报错&#xff1a; 解决方案 1、语言设置中不要勾选 “Beta版。。。。” 2、cmd中执…

layui框架学习(33:流加载模块)

Layui中的流加载模块flow主要支持信息流加载和图片懒加载两部分内容&#xff0c;前者是指动态加载后续内容&#xff0c;示例的话可以参考csdn个人博客主页&#xff0c;鼠标移动到页面底部时自动加载更多内容&#xff0c;而后者是指页面显示图片时才会延迟加载图片信息。   fl…

RocketMQ 行业分享

5.0的架构发生了重大调整&#xff0c;添加了一层rocketmq-proxy,可以通过grpc的方式接入。 参考 https://juejin.cn/post/7199413150973984827

UE5 关于MRQ渲染器参数

最佳参数&#xff1a; Spatial Sample Count&#xff1a;使用奇数样本时临时抗锯齿会收敛 Temporal Sample Count&#xff1a;超过2之后&#xff0c;采样过大会造成TAA效果不佳 TSR&#xff1a;UE5最好的抗锯齿方案

AI聊天GPT三步上篮!

1、是什么&#xff1f; CHATGPT是OpenAI开发的基于GPT&#xff08;Generative Pre-trained Transformer&#xff09;架构的聊天型人工智能模型。也就是你问它答&#xff0c;根据网络抓去训练 2、怎么用&#xff1f; 清晰表达自己诉求&#xff0c;因为它就是一个AI助手&#…

CF1837 A-D

A题 题目链接&#xff1a;https://codeforces.com/problemset/problem/1837/A 基本思路&#xff1a; 要求计算蚂蚱到达位置 x最少需要多少次跳跃&#xff0c;并输出蚂蚱的跳跃方案。因为每次可以向左或向右跳跃一定距离&#xff08;距离必须为整数&#xff09;&#xff0c;但是…

Web自动化测试高级定位xpath

高级定位-xpath 目录 xpath 基本概念xpath 使用场景xpath 语法与实战 xpath基本概念 XPath 是一门在 XML 文档中查找信息的语言XPath 使用路径表达式在 XML 文档中进行导航XPath 的应用非常广泛XPath 可以应用在UI自动化测试 xpath 定位场景 web自动化测试app自动化测试 …

联想拯救者笔记本切换独显直连游戏体验翻倍、火力全开“嗨”起来

最早的游戏本是由独显负责图形运算&#xff0c;然后直接向屏幕输出所有画面的。但独显负责所有工作&#xff0c;无时无刻都在耗电&#xff1b;撇开游戏模式下高负载的功耗不谈&#xff0c;即便在省电模式下功耗也比核显高得多。 英伟达发布的Optimus混合输出技术&#xff0c;在…