目錄
背景
問題排查
首頁 Java java教程 Java System#exit無法退出程式的問題如何解決

Java System#exit無法退出程式的問題如何解決

Apr 25, 2023 am 09:55 AM
java

背景

有朋友碰到了一個狀況:java.lang.System#exit無法退出應用程式

我聽到這種情況的時候是感覺很驚奇的,這函數還能不起作用?這就好奇不已了呀

接著,朋友繼續給了他的場景描述:在Dubbo應用連接註冊中心的時候,如果連接(超時)失敗,期望調用System#exit退出應用程序,但是程式並沒有依期望退出,JVM進程還有什麼

同時,如果把執行System#exit的程式碼放到另一個線程,程式可以按期望退出,JVM進程結束

用偽代碼描述如下:

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

-----------

Future<Object> 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<Thread, Thread> 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<Thread> 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} // 等待一段时间之后,应用程序仍然没有正常停止,则需要强行关闭应用
登入後複製

以上是Java System#exit無法退出程式的問題如何解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP與Python:核心功能 PHP與Python:核心功能 Apr 13, 2025 am 12:16 AM

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

PHP的影響:網絡開發及以後 PHP的影響:網絡開發及以後 Apr 18, 2025 am 12:10 AM

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP:許多網站的基礎 PHP:許多網站的基礎 Apr 13, 2025 am 12:07 AM

PHP成為許多網站首選技術棧的原因包括其易用性、強大社區支持和廣泛應用。 1)易於學習和使用,適合初學者。 2)擁有龐大的開發者社區,資源豐富。 3)廣泛應用於WordPress、Drupal等平台。 4)與Web服務器緊密集成,簡化開發部署。

PHP與Python:用例和應用程序 PHP與Python:用例和應用程序 Apr 17, 2025 am 12:23 AM

PHP適用於Web開發和內容管理系統,Python適合數據科學、機器學習和自動化腳本。 1.PHP在構建快速、可擴展的網站和應用程序方面表現出色,常用於WordPress等CMS。 2.Python在數據科學和機器學習領域表現卓越,擁有豐富的庫如NumPy和TensorFlow。

See all articles