JVM调优命令、调优思路
创始人
2025-05-29 08:35:40
0

文章目录

  • JVM调优工具
    • jmap命令
    • jstack命令
      • 查线程死锁
      • 查占有cpu资源
    • 远程连接jvisualvm
    • jinfo命令
    • jstat
      • 垃圾回收统计
      • 堆内存统计
      • 新生代垃圾回收统计
      • 新生代内存统计
      • 老年代垃圾回收统计
      • 老年代内存统计
      • 元数据空间统计
      • 各内存区使用比例
    • JVM运行问题排查思路

JVM调优工具

jmap命令

主要查看堆内存中的一些信息,看看哪些类的实例对象占用了多大的堆内存

通过jmap -histo 进程id命令查看堆中实例情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIy61tsH-1678928259819)(picture/性能调优/image-20230316063712977.png)]

文件内容如下

		 实例数		      内存容量   类num     #instances         #bytes  class name
----------------------------------------------1:        614068      142821992  [C2:         33839      109489600  [I3:         40356       18126952  [B4:        387655        9303720  java.lang.String5:         51323        4516424  java.lang.reflect.Method6:         62553        4003392  java.net.URL7:         46750        3351808  [Ljava.lang.Object;8:         96335        2077432  [Ljava.lang.Class;9:         64454        2062528  org.springframework.boot.loader.jar.StringSequence10:         21121        1687120  [S11:         50557        1617824  java.util.concurrent.ConcurrentHashMap$Node12:         19954        1607600  [Ljava.util.HashMap$Node;13:         61256        1470144  java.lang.StringBuffer14:         42761        1368352  java.util.HashMap$Node15:         55896        1341504  org.springframework.boot.loader.jar.JarURLConnection$JarEntryName16:         11999        1329664  java.lang.Class17:         31507        1260280  java.util.LinkedHashMap$Entry
......

通过jmap -heap 进程id命令查看堆信息

C:\Users\Administrator>jmap -heap 9980
Attaching to process ID 9980, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.291-b10using thread-local object allocation.
Parallel GC with 15 thread(s)Heap Configuration:MinHeapFreeRatio         = 0MaxHeapFreeRatio         = 100MaxHeapSize              = 8514437120 (8120.0MB)NewSize                  = 177209344 (169.0MB)MaxNewSize               = 2837970944 (2706.5MB)OldSize                  = 355467264 (339.0MB)NewRatio                 = 2SurvivorRatio            = 8MetaspaceSize            = 21807104 (20.796875MB)CompressedClassSpaceSize = 1073741824 (1024.0MB)MaxMetaspaceSize         = 17592186044415 MBG1HeapRegionSize         = 0 (0.0MB)Heap Usage:
PS Young Generation
Eden Space:capacity = 734527488 (700.5MB)used     = 93223176 (88.90454864501953MB)free     = 641304312 (611.5954513549805MB)12.69158438901064% used
From Space:capacity = 13107200 (12.5MB)used     = 0 (0.0MB)free     = 13107200 (12.5MB)0.0% used
To Space:capacity = 15728640 (15.0MB)used     = 0 (0.0MB)free     = 15728640 (15.0MB)0.0% used
PS Old Generationcapacity = 425721856 (406.0MB)used     = 30535456 (29.120880126953125MB)free     = 395186400 (376.8791198730469MB)7.172630573141164% used28339 interned Strings occupying 3515304 bytes.

通过jmap -dump:format=b,file=导出文件路径 进程id命令导出堆的dump文件

C:\Users\Administrator>jmap -dump:format=b,file=d:\eureka.dump 9980
Dumping heap to D:\eureka.dump ...
Heap dump file created

然后我们可以使用jvisualvm工具来导入dump文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pm8ViDMf-1678928259819)(picture/性能调优/image-20230316065436347.png)]

我们一般会配置发生内存溢出时自动导出dump文件(内存很大时可能导出失败)

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./   (路径)

常用命令总结

# 查看历史生成的实例
jmap -histo 进程id# 查看当前存活的实例,执行过程中可能触发一次FullGC
jmap -histo:live 进程id# 查看堆内存中各个区域总内存、使用内存、剩余内存情况
jmap -heap 进程id# 导出dump文件
jmap -dump:format=b,file=导出文件路径 进程id



jstack命令

一般用来定位线程死锁问题、找出哪些线程占用cpu资源较高



查线程死锁

案例:写一个死锁的程序

public class DeadLockTest {private static Object lock1 = new Object();private static Object lock2 = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (lock1) {try {System.out.println("thread1 begin");Thread.sleep(5000);} catch (InterruptedException e) {}synchronized (lock2) {System.out.println("thread1 end");}}}).start();new Thread(() -> {synchronized (lock2) {try {System.out.println("thread2 begin");Thread.sleep(5000);} catch (InterruptedException e) {}synchronized (lock1) {System.out.println("thread2 end");}}}).start();System.out.println("main thread end");}
}

接下来使用jstack 进程id命令查看

C:\Users\Administrator>jstack 11548

输出结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WGkJe762-1678928259820)(picture/性能调优/image-20230316071413512.png)]

  • Thread-1:线程名
  • prio=5:优先级为5
  • os_prio 操作系统中的优先级
  • tid : 线程id
  • nid :线程对应本地线程标识

中间部分表示Thread-1线程再等待0x0000000716edac88锁对象,现在自己占有了0x0000000716edac98锁对象

另一部分截图:前一部分是发现了一个线程死锁,Thread-1在等待某个锁对象,这个锁对象目前被Thread-0占用,Thread-0在等待某个锁对象,这个锁对象目前被Thread-1占用。下面一部分就是指出了在哪个类哪个位置出现了死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0Jm21xm-1678928259820)(picture/性能调优/image-20230316070831294.png)]

还可以用jvisualvm自动检测死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-79F5bmLh-1678928259820)(picture/性能调优/image-20230316071229719.png)]



查占有cpu资源

案例,模拟占用cpu资源代码

package com.tuling.jvm;/*** 运行此代码,cpu会飙高*/
public class Math {public static final int initData = 666;public static User user = new User();public int compute() {  //一个方法对应一块栈帧内存区域int a = 1;int b = 2;int c = (a + b) * 10;return c;}public static void main(String[] args) {Math math = new Math();while (true){math.compute();}}
}
  • 首先使用top命令找到占用cpu较高的进程id

  • 使用命令top -p 进程id ,显示你的java进程的内存情况

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HAU3qLwi-1678928259820)(picture/性能调优/96310)]

  • 然后按 shift+h 键,查看该进程中的线程信息,找到占用cpu资源较高的线程

    找到内存和cpu占用最高的线程tid,比如19664

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YmpZA7z3-1678928259820)(picture/性能调优/96324)]

  • 将十进制转换为十六进制得到 0x4cd0,因为jstack命令中线程id是用十六进程显示的

  • 使用jstack 进程id | grep -A 10 十六进程线程id,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KwuF1zOY-1678928259821)(picture/性能调优/96326)]



远程连接jvisualvm

一般生产环境是不会开放端口让我们远程连接jvisualvm的,了解即可

启动普通的jar程序JMX端口配置:

java -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.65.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar microservice-eureka-server.jar
  • -Dcom.sun.management.jmxremote.port 为远程机器的JMX端口

  • -Djava.rmi.server.hostname 为远程机器IP

tomcat的JMX配置:在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行

JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.50.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

连接时确认下端口是否通畅,可以临时关闭下防火墙



jinfo命令

主要是查看当前JVM运行参数和系统参数

jinfo -flags 进程id 命令查看当前JVM运行参数

[root@VM-8-7-centos ~]# jps
14164 BrokerStartup
14088 NamesrvStartup
11897 Jps
26238 jar[root@VM-8-7-centos ~]# jinfo -flags 14088
Attaching to process ID 14088, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09
Non-default VM flags: -XX:CICompilerCount=2 -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:CompressedClassSpaceSize=327155712 -XX:GCLogFileSize=31457280 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:MaxMetaspaceSize=335544320 -XX:MaxNewSize=268435456 -XX:MetaspaceSize=134217728 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=268435456 -XX:NumberOfGCLogFiles=5 -XX:OldPLABSize=16 -XX:OldSize=268435456 -XX:-OmitStackTraceInFastThrow -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8 -XX:+UseCMSCompactAtFullCollection -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseGCLogFileRotation -XX:-UseLargePages -XX:-UseParNewGC 
Command line:  -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC -verbose:gc -Xloggc:/dev/shm/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m -XX:-OmitStackTraceInFastThrow -XX:-UseLargePages -Djava.ext.dirs=/usr/local/java/jdk1.8.0_201/jre/lib/ext:/root/software/rocketMQ/rocketmq-all-4.9.0-bin-release/bin/../lib:/usr/local/java/jdk1.8.0_201/lib/ext

jinfo -sysprops 进程id 查看系统参数

[root@VM-8-7-centos ~]# jinfo -sysprops 14088
Attaching to process ID 14088, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.201-b09
sun.boot.library.path = /usr/local/java/jdk1.8.0_201/jre/lib/amd64
rocketmq.remoting.version = 393
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = :
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level = unknown
sun.java.launcher = SUN_STANDARD
user.country = US
user.dir = /root/software/rocketMQ/rocketmq-all-4.9.0-bin-release
java.vm.specification.name = Java Virtual Machine Specification
java.runtime.version = 1.8.0_201-b09
java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
os.arch = amd64
java.endorsed.dirs = /usr/local/java/jdk1.8.0_201/jre/lib/endorsed
java.io.tmpdir = /tmp
line.separator = 
......

jstat

jstat命令可以查看堆内存各部分使用量、加载类数量等等

jstat [-选项] 进程id [间隔时间 毫秒] [查询次数]



垃圾回收统计

# 评估程序内存使用以及GC压力整体情况
jstat -gc 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6OS8T6S2-1678928259821)(picture/性能调优/image-20230316074411582.png)]

  • S0C :S0区总内存容量
  • S1C :S1区总内存容量
  • S0U : S0区使用了多少内存
  • S1U: S1区使用了多少内存
  • EC : Eden区总内存容量
  • EU : Eden区使用了多少内存
  • OC : Old区总内存容量
  • OU : Old使用了多少内存
  • MC : 方法区大小
  • MU : 方法区使用情况
  • CCSC : 压缩类空间大小
  • CCSU: 压缩类空间使用情况
  • YGC : MinorGC总次数
  • YGCT :MinorGC总耗时,单位秒
  • FGC : FullGC中次数
  • FGCT:FullGC总耗时,单位秒
  • GCT: GC中耗时,为YGCT + FGCT



堆内存统计

jstat -gccapacity 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bm9SkxXT-1678928259821)(picture/性能调优/image-20230316075101354.png)]

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0C:第一个幸存区大小
  • S1C:第二个幸存区的大小
  • EC:伊甸园区的大小
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:当前老年代大小
  • MCMN:最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC:年轻代gc次数
  • FGC:老年代GC次数



新生代垃圾回收统计

jstat -gcnew 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Khf6xZD4-1678928259821)(picture/性能调优/image-20230316075238915.png)]

  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • TT:对象在新生代存活的次数
  • MTT:对象在新生代存活的最大次数
  • DSS:期望的幸存区大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间



新生代内存统计

jstat -gcnewcapacity 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IDlbzw54-1678928259821)(picture/性能调优/image-20230316075511229.png)]

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0CMX:最大幸存1区大小
  • S0C:当前幸存1区大小
  • S1CMX:最大幸存2区大小
  • S1C:当前幸存2区大小
  • ECMX:最大伊甸园区大小
  • EC:当前伊甸园区大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代回收次数



老年代垃圾回收统计

jstat -gcold 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGu3azEH-1678928259821)(picture/性能调优/image-20230316075648880.png)]

  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间



老年代内存统计

jstat -gcoldcapacity 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iTTuRtt7-1678928259821)(picture/性能调优/image-20230316075744521.png)]

  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:老年代大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间



元数据空间统计

jstat -gcmetdcapacity 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgCxIfJE-1678928259822)(picture/性能调优/image-20230316075925959.png)]

  • MCMN:最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间



各内存区使用比例

jstat -gcutil 进程id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A5lFrN0k-1678928259822)(picture/性能调优/image-20230316080043030.png)]

  • S0:幸存1区当前使用比例
  • S1:幸存2区当前使用比例
  • E:伊甸园区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间



JVM运行问题排查思路

总体优化思路:让每次MinorGC存活对象小于Survivor区容量的50%,将对象都留在年轻代中,尽量别进入老年代中,减少FullGC频率

  1. 查看当前JVM运行的参数

    或者是直接使用gcinfo -flags命令查看运行参数。从而了解到堆中各区域的内存容量,比如堆内存大小、年轻代大小、Eden区和Survivor区比例、老年代大小、大对象阀值、分代年龄阀值相关信息

  2. 使用jstat -gc 进程id 间隔时间 统计次数命令统计Eden区对象增长速率

    间隔时间使用每秒打印、每分钟打印都行,如果系统负载不高,可以把频率1秒换成1分钟

  3. 统计MinorGC触发频率、每次耗时

    知道了Eden区容量大小、每秒Eden区增长多少对象,那么我们就可以估算出大概多久触发一次MinorGC了。耗时可以通过 YGCT/YGC 公式算出

    根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

  4. 统计每次MinorGC后有多少对象存活进入Old区

    知道了MinorGC的频率,假设5分钟一次,那么可以就可以jstat -gc 进程id 300000 10观察每次Eden、Survivor、old使用的变化。

    每次MinorGC后Eden区使用一般会大幅减少,Survivor区和Old区使用情况会增加,这些增加的对象就是存活的对象。进而我们就能大致估算出每次MinorGC后有多少对象会进入到老年代中,推算出老年代对象增长速率

  5. 统计FullGC触发频率与耗时

    知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

目前我们就能画出类似于下面的一种模型图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QCDuknR9-1678928259822)(picture/性能调优/96307)]

接下来我们需要知道一般有哪些情况会触发FullGC

  • 方法区元空间放满

    可以使用jstat命令按一定评率查看元空间使用过情况,然后排除这种可能性

  • 显示调用System.gc()

    可以使用-XX:+DisableExplicitGC参数禁用,一般这种情况也很少出现

  • Old区容量使用率达到设定的阀值

  • 老年代空间分配担保机制

大概率是进入老年代的对象过多导致FullGC。我们接下来需要知道哪些情况下对象会进入到老年代

  • 大对象
  • 分代年龄达到阀值
  • 对象动态年龄判断机制

对业务的熟悉程度我们先排查大对象的情况。

一般的对象都是朝生夕死的对象,再排查分代年龄达到阀值的情况。

比较大的可能性是对象动态年龄判断机制导致的,我们可以尝试增大年轻代内存容量,然后再进行测试。

调优,增加年轻代内存空间后,可能频繁产生FullGC的情况就不会出现了,也有可能还会更加频繁,这种情况的出现我们还是一样慢慢排查触发FullGC的各种情况,最大的可能性是老年代空间分配担保机制的原因,因为年轻代现在增大了,老年代减少了。

我们可以使用jmap命令查看一下当前堆中有没有哪个业务对象的实例很多,占用空间很多。如果找到了我们就去代码中找一下哪些地方创建了这个对象进而优化代码,如果创建对象的地方非常多,不太好定位问题那么我们可以尝试使用stack命令找一下占用cpu较多的线程,根据输出提示找到具体的java代码。一般创建较多的大对象也同时会占用较多的cpu资源,所以我们也可以从这方面定位到出现问题的代码进行优化

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
JAVA多线程知识整理 Java多线程基础 线程的创建和启动 继承Thread类来创建并启动 自定义Thread类的子类&#...
【洛谷 P1090】[NOIP... [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G ...
国民技术LPUART介绍 低功耗通用异步接收器(LPUART) 简介 低功耗通用异步收发器...
城乡供水一体化平台-助力乡村振... 城乡供水一体化管理系统建设方案 城乡供水一体化管理系统是运用云计算、大数据等信息化手段࿰...
程序的循环结构和random库...   第三个参数就是步长     引入文件时记得指明字符格式,否则读入不了 ...
中国版ChatGPT在哪些方面... 目录 一、中国巨大的市场需求 二、中国企业加速创新 三、中国的人工智能发展 四、企业愿景的推进 五、...
报名开启 | 共赴一场 Flu... 2023 年 1 月 25 日,Flutter Forward 大会在肯尼亚首都内罗毕...
汇编00-MASM 和 Vis... Qt源码解析 索引 汇编逆向--- MASM 和 Visual Studio入门 前提知识ÿ...
【简陋Web应用3】实现人脸比... 文章目录🍉 前情提要🌷 效果演示🥝 实现过程1. u...
前缀和与对数器与二分法 1. 前缀和 假设有一个数组,我们想大量频繁的去访问L到R这个区间的和,...
windows安装JDK步骤 一、 下载JDK安装包 下载地址:https://www.oracle.com/jav...
分治法实现合并排序(归并排序)... 🎊【数据结构与算法】专题正在持续更新中,各种数据结构的创建原理与运用✨...
在linux上安装配置node... 目录前言1,关于nodejs2,配置环境变量3,总结 前言...
Linux学习之端口、网络协议... 端口:设备与外界通讯交流的出口 网络协议:   网络协议是指计算机通信网...
Linux内核进程管理并发同步... 并发同步并发 是指在某一时间段内能够处理多个任务的能力,而 并行 是指同一时间能够处理...
opencv学习-HOG LO... 目录1. HOG(Histogram of Oriented Gradients,方向梯度直方图)1...
EEG微状态的功能意义 导读大脑的瞬时全局功能状态反映在其电场结构上。聚类分析方法一致地提取了四种头表面脑电场结构ÿ...
【Unity 手写PBR】Bu... 写在前面 前期积累: GAMES101作业7提高-实现微表面模型你需要了解的知识 【技...