当前位置:

首页 > 编程开发 > Java System#exit无法退出程序的问题如何解决

Java System#exit无法退出程序的问题如何解决

背景有朋友碰到了一个情况:java.lang.System#exit无法退出应用程序。我听到这种情况的时候是感觉很惊奇的,这函数还能不起作用?这就好奇不已了呀接着,朋友继续给出了他的场景描述:在Dubbo应用连接注册中心的时候,如果连接(超时)失败,期望调用System#exit退出应用程序,但是程序并没有按期望退出,JVM进程还存在与此同时,如果把执行System#exit的代码放到另一个线程,程序可以按期望退出,JVM进程结束用伪代码描述如下:Futurefuture=连接注册中心的Future;tr

本文目录

    背景

    有朋友碰到了一个情况:java.lang.System#exit无法退出应用程序。

    我听到这种情况的时候是感觉很惊奇的,这函数还能不起作用?这就好奇不已了呀

    接着,朋友继续给出了他的场景描述:在Dubbo应用连接注册中心的时候,如果连接(超时)失败,期望调用System#exit退出应用程序,但是程序并没有按期望退出,JVM进程还存在

    与此同时,如果把执行System#exit的代码放到另一个线程,程序可以按期望退出,JVM进程结束

    用伪代码描述如下:

    Future future = 连接注册中心的Future;
    try {
        Object o = future.get(3, TimeUnit.SECONDS);
    } catch (Exception e) {
    	log.error("connect failed xxxx");
        System.exit(1); // 程序无法退出
    }
    
    -----------
    
    Future future = 连接注册中心的Future;
    try {
        Object o = future.get(3, TimeUnit.SECONDS);
    } catch (Exception e) {
    	log.error("connect failed xxxx");
        new Thread(() -> System.exit(1)).start(); // 程序能按期望退出
    }

    朋友面临的场景比伪代码描述的情况复杂的多,但所面临的本质问题是一样的。

    更一般化地问题,在Dubbo的org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry构造函数中,直接执行System.exit(1);程序无法退出,放在异步线程中执行却可以按期望退出

    即:

    // org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry
    
    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        super(url);
        System.exit(1); //JVM进程无法退出
        // ...(省略)
    }
    
    -----------
    // org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry
    
    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        super(url);
        new Thread(() -> {System.exit(1);}).start(); //JVM进程正常退出
        // ...(省略)
    }

    这就更令人惊奇了!

    问题排查

    要找出问题产生的原因,首先得有一些预备知识,否则会茫然无措,感觉无从下手

    • java.lang.System#exit 方法是Java提供的能够停止JVM进程的方法

    • 该方法被触发时,JVM会去调用Shutdown Hook(关闭勾子)方法,直到所有勾子方法执行完毕,才会关闭JVM进程

    由上述第2点猜测:是否存在死循环的勾子函数无法退出,以致JVM没有去关闭进程?

    举个例子:

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            while (true) {
                try {
                    System.out.println("closing...");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
            }
        }));
    
        System.out.println("before exit...");
        System.exit(0); 
        System.out.println("after exit..."); //代码不会执行
    }

    如上,在main方法里先注册了一个shutdown hook,该勾子函数是个死循环,永远也不会退出,每3秒打印一次"closing…"

    接着执行System.exit(0);方法,期望退出JVM进程

    before exit...
    closing...
    closing...
    closing...
    closing...
    closing...

    ...

    结果是控制台不断打印"closing…",且JVM进程没有退出

    原因正是上述第二点储备知识提到的:JVM会等待所有勾子执行完毕之后,才关闭进程。而示例中的shutdown hook 永远也不会执行完毕,因此JVM进程也不会被关闭

    尽管有了储备知识,仍然很疑惑:如果存在死循环的shutdown hook,那么System.exit无论是在主线程中调用,还是在异步线程中调用,都应该不会关闭JVM进程;反之,如果不存在死循环的shutdown hook,无论是在哪个线程调用,都应该会关闭JVM进程。为什么在背景的伪代码中,却是因为不同的调用线程执行System.exit,导致不一样的结果呢?

    这时候只好想办法,看看shutdown hook们都在偷摸干啥事,为什么未执行完毕,以致JVM进程不能退出

    恰好对Dubbo的源码也略有研究,很容易就找到org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry的构造函数,并在其中加上一行代码,如下所示,改完之后重新编译源码,并引入自己的工程中进行Debug

    注:本次使用的Dubbo版本为2.7.6

    // org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry
    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        super(url);
        System.exit(1); // 新增加的一行代码
        // ...(省略)
    }

    启动工程,熟悉Dubbo的朋友应该会知道,应用启动的过程中会去注册中心(这儿是Zookeeper)注册或者订阅,因为启动的是消费者,因此应用会尝试连接注册中心Zookeeper,会走到ZookeeperRegistry的构造函数,由于构造函数第二行是新增的代码System.exit(1);按照背景的说法,JVM不会退出,且会卡死,这时候,借助IDEA的"快照"功能,可以"拍"下Java线程栈的运行情况,功能上相当于执行jstack命令

    Java System#exit无法退出程序的问题如何解决

    Java System#exit无法退出程序的问题如何解决

    从线程栈中看出一个可疑的线程:DubboShutdownHook

    从名字上可以看出是一个Dubbo注册的一个shutdown hook,其主要目的是为了关闭连接、做一些资源的回收等工作

    从图中也可以看出,线程阻塞在org.apache.dubbo.registry.support.AbstractRegistryFactory第83行

    public static void destroyAll() {
        if (!destroyed.compareAndSet(false, true)) {
            return;
        }
    
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close all registries " + getRegistries());
        }
        // Lock up the registry shutdown process
        LOCK.lock(); // 83行,DubboShutdownHook线程阻塞在此处
        try {
            for (Registry registry : getRegistries()) {
                try {
                    registry.destroy();
                } catch (Throwable e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
            REGISTRIES.clear();
        } finally {
            // Release the lock
            LOCK.unlock();
        }
    }

    从代码中很显然可以看出,因为获取不到锁,因此线程阻塞在第83行,等待获取锁,也就是说,有别的线程持着这把锁,但还没释放,DubboShutdownHook不得不等待着

    通过IDEA,查看有哪些地方获取了这把锁,如下,找到了

    org.apache.dubbo.registry.support.AbstractRegistryFactory#getRegistry(org.apache.dubbo.common.URL)

    会获取锁

    // org.apache.dubbo.registry.support.AbstractRegistryFactory
    
    public Registry getRegistry(URL url) {
        // ...(省略)
        LOCK.lock(); // 获取锁
        try {
            // ...(省略)
            // 创建Registry,由于我们选用的注册中心是Zookeeper,因此通过SPI选择了ZookeeperRegistryFactory对ZookeeperRegistry进行创建,最终会调用到我们添加过一行System.exit的ZookeeperRegistry构造函数中
            
            registry = createRegistry(url); 
            
            // ...(省略)
        } finally {
            // Release the lock
            LOCK.unlock(); // 创建完registry,与注册中心连上之后,才会释放锁
        }
    }
    // org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory
    
    public Registry createRegistry(URL url) {
    	// 调用修改过源码的ZookeeperRegistry构造函数
        return new ZookeeperRegistry(url, zookeeperTransporter);
    }

    如此,System.exit无法退出JVM进程的问题总算真相大白了:

    1.Dubbo启动过程中会先获取锁,然后创建registry与注册中心进行连接,在ZookeeperRegistry中调用了java.lang.System#exit方法,程序转而执行"唤起shutdown hook"的代码并阻塞等待所有勾子函数执行完毕,而此时,之前持有的锁并没有释放

    2.所有勾子函数(每个勾子函数都对应一个线程)被唤醒并执行,其中有一个Dubbo的勾子函数在执行的过程中,需要获取步骤1中的锁,由于获取锁失败,就阻塞等待着

    3.由于1没有释放锁的情况下等待2执行完,而2的执行需要等待1释放锁,这样就形成了一个类似"死锁"的场景,因此也就导致了程序卡死,而JVM进程还存活的现象。之所以称为"类似"死锁,是因为1中执行System.exit的线程,也即持有锁的线程,永远不会走到释放锁的代码:一旦程序进入System.exit的世界里,就像进了一个单向虫洞,只能进不能出,如果勾子函数执行完毕,JVM进程接着就会被关闭,不会有机会再释放锁

    那么,为什么在异步线程中执行System.exit,却能够正常退出JVM?

    那是因为:"唤起shutdown hook"并阻塞等待所有勾子函数执行完毕的线程是其它线程(此处假设是线程A),该线程在阻塞时并未持有任何锁,而主线程会继续往下执行并接着释放锁。一旦锁释放,Shutdown hook就有机会持有该锁,并且执行其它资源的回收操作,等到所有的shutdown hook执行完毕,A线程就能从阻塞中返回并执行halt方法关闭JVM,因此能够正常退出JVM进程

    深入学习

    以上是对java.lang.System#exit 无法退出程序问题的分析,来龙去脉已经阐述清楚,受益于对Dubbo源码的了解以及正确的排查思路和排查手段,整个问题排查过程其实并没有花太多时间,但可以趁着这个机会,把java.lang.System#exit系统学习一下,或许会对以后问题排查、基础组件设计提供一些思路

    System#exit

    // java.lang.System
    
    public static void exit(int status) {
        Runtime.getRuntime().exit(status);
    }

    Terminates the currently running Java Virtual Machine. The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination.
    This method calls the exit method in class Runtime. This method never returns normally.
    The call System.exit(n) is effectively equivalent to the call:
    Runtime.getRuntime().exit(n)

    这个方法实现非常简单,是Runtime#exit的一个简便写法,其作用是用来关闭JVM进程,一旦调用该方法,永远也不会从该方法正常返回:执行完该方法后JVM进程就直接关闭了。

    入参status取值分两类:0值与非0值,0值意味着正常关闭,非0值意味着异常关闭。

    传入0值[有可能]会去执行所有的finalizer方法,非0值则一定不会执行(都不正常了,还执行啥finalizer呢?)。这儿提及[有可能]是因为,默认并不会执行finalizers,需要调用java.lang.Runtime#runFinalizersOnExit方法开启,而该方法早被JDK标识为Deprecated,因此通常情况下是不会开启的

    // java.lang.Runtime
    
    @Deprecated
    public static void runFinalizersOnExit(boolean value) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            try {
                security.checkExit(0);
            } catch (SecurityException e) {
                throw new SecurityException("runFinalizersOnExit");
            }
        }
        Shutdown.setRunFinalizersOnExit(value);
    }

    接着看java.lang.Runtime#exit,可以看到,最终调用的是Shutdown.exit(status);,该方法是个包级别可见的方法,外部不可见

    // java.lang.Runtime
    
    public void exit(int status) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkExit(status);
        }
        Shutdown.exit(status);
    }
    // java.lang.Shutdown
    
    static void exit(int status) {
        // ...(省略)
        synchronized (Shutdown.class) {
            /* Synchronize on the class object, causing any other thread
             * that attempts to initiate shutdown to stall indefinitely
             */
            // 执行shutdown序列
            sequence();
            // 关闭JVM
            halt(status);
        }
    }
    // java.lang.Shutdown
    
    private static void sequence() {
        // ...(省略)
        runHooks();
        // ...(省略)
    }
    // java.lang.Shutdown
    
    private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // 这个锁很重要,目的是通过Happens-Before保证内存的可见性
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run(); //执行勾子函数
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }

    java.lang.Shutdown#runHooks有两个点需要注意,第一点MAX_SYSTEM_HOOKS(hooks)这个并不是我们注册的shutdown hooks,而是按顺序预定义的系统关闭勾子,目前JDK源码(JDK8)预定义了三个:

    • Console restore hook

    • Application hooks

    • DeleteOnExit hook

    其中,Application hooks才是我们应用程序中主动注册的shutdown hook。

    在java.lang.ApplicationShutdownHooks类初始化时,会执行static代码块,并在其中注册了Application hooks

    // java.lang.ApplicationShutdownHooks
    
    class ApplicationShutdownHooks {
        /* The set of registered hooks */
        // 这个才是我们应用程序代码中注册的shutdown hook
        private static IdentityHashMap hooks; 
        static {
            try {
                Shutdown.add(1 /* shutdown hook invocation order */,
                    false /* not registered if shutdown in progress */,
                    new Runnable() {
                        public void run() {
                            runHooks();
                        }
                    }
                );
                hooks = new IdentityHashMap<>();
            } catch (IllegalStateException e) {
                // application shutdown hooks cannot be added if
                // shutdown is in progress.
                hooks = null;
            }
        }

    其次要注意的点是,给hook变量赋值的时候进行了加锁

    Runnable hook;
    synchronized (lock) {
        currentRunningHook = i;
        hook = hooks[i];
    }

    一般而言,给局部变量赋值是不需要加锁的,因为局部变量是栈上变量,而线程栈之间数据是隔离的,不会出现线程安全的问题,因此不需要靠加锁来保证数据并发访问的安全性。

    而此处加锁也并非为了解决线程安全问题,其真正的目的在于,通过Happens-Before规则来保证hooks的内存可见性:An unlock on a monitor happens-before every subsequent lock on that monitor。

    如果不加锁,有可能导致从hooks数组中读取到的值并不是内存中最新的变量值,而是一个旧值

    上面是读取hooks数组给hook变量赋值,为了满足HB(Happens-Before)原则,需要确保写操作中同样对hooks变量进行了加锁,因此我们看一下写hooks数组的地方,如下:

    // java.lang.Shutdown
    
    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
        synchronized (lock) {
        		// ...(省略)
            hooks[slot] = hook;
        }
    }

    写 操作确实加了锁,这样才能让接下来的 读 操作的加锁行为满足HB原则

    由于篇幅原因,就不展开具体的HB介绍,相信了解过HB原则的朋友一下就能明白其中的原理

    这个点个人感觉很有意思,因为锁的作用不单是为了保证线程安全,还可以用来做为内存通信、保证内存可见性的手段,因此可以当作面试的一个点,当下次面试官问到:你写的代码中用过锁(synchronized)吗?什么场景用到锁?都集群部署了,单机锁还有意义吗? 我们就可以回答:为了保证内存的可见性,balabalaba

    所以你瞧,这个点其实也给我们设计基础组件带来很大的启发,synchronized在当今集群、分布式环境下并非一无是处,总有合适的地方在等待着它发挥光和热

    注:JDK源码中真处处是宝藏,很多地方隐藏着巧妙而不可缺少的设计

    在给hook变量赋值之后,就执行 if (hook != null) hook.run();,其中会执行到Application hooks,即上面提到的在ApplicationShutdownHooks类初始化时注册的勾子,勾子内部调用了java.lang.ApplicationShutdownHooks#runHooks方法

    // java.lang.ApplicationShutdownHooks
    
    Shutdown.add(1 /* shutdown hook invocation order */,
        false /* not registered if shutdown in progress */,
        new Runnable() {
            public void run() {
                runHooks();
            }
        }
    );
    // java.lang.ApplicationShutdownHooks
    
    static void runHooks() {
        Collection threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet(); // hooks才是应用程序真正注册的shutdown hook
            hooks = null;
        }
    		// 每一个shutdown hook都对应一个thread,由此可见是并发执行关闭勾子函数
        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join(); // 死等到hook执行完毕
                    break;
                } catch (InterruptedException ignored) {
                    // 即便被唤醒都不搭理,接着进行下一轮循环,继续死等
                }
            }
        }
    }

    上面的hooks才是应用程序真正注册的shutdown hook,由源码可以看出,每一个hook都对应着一个thread,且调用了它们的start方法,即开启thread,意味着shutdown hook是并发、无序地执行

    接着,唤起shutdown hook的线程,会通过死循环和join死等到所有关闭勾子都执行完毕,且忽略任何 唤醒异常。也即是说,如果勾子们不执行完,唤醒线程是不会离开的

    等所有的Application hooks执行完毕,接下来会执行DeleteOnExit hook(如果存在),等所有system hooks执行完毕,也基本意味着sequence方法执行完毕,接下来就执行halt方法关闭JVM虚拟机

    synchronized (Shutdown.class) {
        sequence();
        halt(status);
    }

    这里额外还有一个知识点,上文只是提了一嘴,可能会容易忽略,此处拿出来解释一下:执行java.lang.System#exit永远也不会从该方法正常返回,也即是说,即便System#exit后边跟着的是finally,也不会执行 。一不注意就容易掉坑里

    try {
        // ...
        System.exit(0);
    } finally {
        // 这里的代码永远执行不到
    }

    java.lang.Runtime#addShutdownHook

    聊完System#exit方法,接着来聊聊注册shutdown hook的方法。该方法本身实现上很简单,如下示:

    // java.lang.Runtime
    public void addShutdownHook(Thread hook) {
        // ...(省略)
        ApplicationShutdownHooks.add(hook);
    }
    
    // java.lang.ApplicationShutdownHooks
    static synchronized void add(Thread hook) {
        // ...(省略)
        hooks.put(hook, hook);
    }

    需要注意的是,注册的关闭勾子会在以下几种时机被调用到

    程序正常退出

    • 最后一个非守护线程执行完毕退出时

    • System.exit方法被调用时

    程序响应外部事件

    • 程序响应用户输入事件,例如在控制台按ctrl+c(^+c)

    • 程序响应系统事件,如用户注销、系统关机等

    除此之外,shutdown hook是不会被执行的

    Shutdown hook存在的意义之一,是能够帮助我们实现优雅停机,而优雅停机的意义是:应用的重启、停机等操作,不影响业务的连续性

    以Dubbo Provider的视角为例,优雅停机需要满足两点基本诉求:

    • Consumer不应该请求到已经下线的Provider

    • 在途请求需要处理完毕,不能被停机指令中断

    Dubbo注册了Shutdown hook,JVM在收到操作系统发来的关闭指令时,会执行关闭勾子

    • 在勾子中停止与注册中心的连接,注册中心会通知Consumer某个Provider已下线,后续不应该再调用该Provider进行服务。此行为是断掉上游流量,满足第一点诉求

    • 接着,勾子执行Protocol(Dubbo相关概念)的注销逻辑,在其中判断server(Dubbo相关概念)是否还在处理请求,在超时时间内等待所有任务处理完毕,则关闭server。此行为是处理在途请求,满足第二点述求

    因此,一种优雅停机的整体方案如下:

    $pid = ps | grep xxx // 查找要关闭的应用
    kill $pid // 发出关闭应用指令
    sleep for a period of time // 等待一段时间,让应用程序执行shutdown hook进行现场的保留跟资源的清理工作
    
    $pid = ps | grep xxx // 再次查找要关闭的应用,如果还存在,就需要强行关闭应用
    if($pid){kill -9 $pid} // 等待一段时间之后,应用程序仍然没有正常停止,则需要强行关闭应用
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

    为什么要购买正版软件?
    01 官方授权

    授权来源透明,订单与许可信息可随时查询。

    02 即时交付

    付款成功后自动发放激活信息,支持订单追踪。

    03 中文协助

    安装、激活、迁移与续费问题都有专人响应。

    04 长期可用

    提供升级提醒与授权管理,换机时也能轻松迁移。

    网站备案号:苏ICP备2026018738号-1 联系邮箱:bd@zhengruan.com 网站地图

    Copyright ©2018-2026