热部署为什么是一个“难题”?

46

在工作中,我们一直遇到"PermGen内存溢出"的问题。团队领导认为这是JVM的一个bug,与代码的热部署相关。他没有详细解释,只是指出热部署是个“难题”,甚至.NET框架也未能解决。

我找到了很多解释热部署的文章,但都缺乏技术细节。有没有人可以给我提供一份详细的技术解释,同时解释一下为什么热部署是一个“难题”?


顺便问一下,为什么这是社区维基? - Eddie
2
认为这可能有帮助,以防我有什么表述不清的地方……那样做是默认的吗?这算是不好的行为规范吗? - Andrey Fedorov
有趣:刚刚重新阅读时发现了一个笔误,谢谢! - Andrey Fedorov
4个回答

60

当一个类被加载时,该类的各种静态数据会被存储在PermGen中。只要这个Class实例还存在活动引用,该实例就不能被垃圾回收。

我认为问题的一部分与GC是否应该从perm gen中删除旧的Class实例有关。通常情况下,每次热部署都会向PermGen内存池添加新的Class实例,而旧的实例则通常不会被删除。默认情况下,Sun JVM不会在PermGen中运行垃圾回收,但可以通过可选的"java"命令参数启用。

因此,如果你经常进行热部署,最终将会耗尽您的PermGen空间。

如果您的Web应用程序没有完全关闭(例如,它保留了某个线程),则该Web应用程序使用的所有Class实例都将被锁定在PermGen空间中。您重新部署,现在在PermGen中加载了另一个完整副本的所有这些Class实例。然后您将其卸载,线程继续运行,将另一个集合的类实例固定在PermGen中。您重新部署并加载一整组新的副本…… 最终,您的PermGen将填满。

您可以尝试以下方法来修复此问题:

  • 为最近的Sun JVM提供命令参数,以启用PermGen和类的GC。即:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled
  • 使用不使用固定大小PermGen或对已加载的类执行GC的其他JVM

但是,这只有在您的Web应用程序完全干净地关闭时,没有对该Web应用程序的类装入器所加载的任何类实例进行活动引用时才有效。

即使如此,由于类装入器泄漏(以及在某些情况下过多的字符串内部化),这也不一定会修复问题。

请查看以下链接以获取更多信息(两个加粗的链接有很好的图表来说明问题的一部分)。


1
Eddie,保留这些类的原因不是因为已经存在着使用“旧”类定义的对象吗? - Neil Coffey
当然,保留这些类的原因是因为活动对象仍然引用它们。这就是问题的根源!不应该有任何活动引用。如果您完美地实现了您的Web应用程序,那么这将不会成为一个问题。但你必须做到完美。 - Eddie
@OscarRyz:是的,这些链接与PermGen内存有关,这就是OP所说的耗尽的原因。内存问题不仅仅是解决方法的结果,或者至少不仅仅是如此。您必须确保完全停止所有线程,释放所有静态引用等等,以便您的应用程序能够卸载而不会留下对PermGen中类的引用,否则这些类将无法被垃圾回收。 - Eddie
看着这个在2015年的问题,我想知道Java 8中的挑战如何改变。据我所知,在这个版本中,permgen空间不再是一个问题。 - IcedDante
@IcedDante:我还没有仔细调查Java 8,但我想这个问题在不同的形式下仍然存在。如果您在Web应用程序关闭时没有完全释放所有资源,则无法GC类对象的根本问题仍将存在。只是,“泄漏”的对象不再位于PermGen中(已不存在),而在堆中。 - Eddie
显示剩余3条评论

6
一般来说,Java的安全模型试图防止已经加载的类再次被加载。当然,从一开始,Java就支持动态类加载,困难在于类的重新加载。注入恶意代码的新类(例如从互联网上获取的java.lang.String破解实现)可能会对正在运行的Java应用程序造成危害,而不是创建字符串。因此,Java的设计方式(我推测.NET CLR也是如此,因为它受到JVM的极大“启发”)是防止已经加载的类再次在同一个VM中加载。他们提供了一个机制来覆盖这个“特性”,即类加载器,但是类加载器的规则是,在尝试加载新类之前,它们应该向“父”类加载器请求权限,如果父级已经加载了该类,则忽略新类。例如,我已经使用过从LDAP或RDBMS加载类的类加载器。当应用服务器成为Java EE的主流时(同时也创造了微容器(如Spring)以避免这种负担),热部署成为Java世界中的必要条件。

每次编译后重新启动整个应用服务器会让任何人都疯狂。因此,应用服务器提供商提供了这些“自定义”类加载器来帮助热部署,并使用配置文件,在生产环境中设置时应禁用该行为。但是,折衷的方法是在开发过程中需要使用大量内存。因此,最好的方法是每3-4次部署重新启动一次。

其他从一开始就设计为加载其类的语言不会出现这种情况。

例如,在Ruby中,您甚至可以向正在运行的类添加方法,在运行时覆盖方法或甚至向唯一特定对象添加单个方法。

这些环境的折衷方案当然是内存和速度。

我希望这有所帮助。

编辑

我以前找到过这个产品,它承诺重新加载尽可能简单。我在最初撰写此答案时没有记住链接,现在我记得了。

它是JavaRebel from ZeroTurnaround


你脑袋里乱糟糟的,是吗?所以Java不是为了在运行时加载类而设计的,对吗?这就是为什么从一开始就有Class.forName()的原因? - Vladimir Dyuzhev
那不是奥斯卡写的弗拉基米尔。他写的是Java不是为了在运行时替换类而设计的。 - Zan Lynx
ClassLoader类从版本1开始存在。它并不是用来在运行时替换类的。此外,安全性与PermGen OOM无关。PermGen的限制只是Sun JVM的一个不良实现细节。 - Vladimir Dyuzhev
Java有一个安全模型,可以选择防止一些你谈论的内容,但Java始终能够动态加载类。 - Eddie
好的,明白了。但从文本中并不是很清楚。 :-) 你从安全开始好像它是问题的主要/唯一原因。 - Vladimir Dyuzhev
显示剩余2条评论

3

太阳JVM的PermGen空间是固定的,最终它会全部被消耗(是的,显然由于与类加载器相关的代码中的错误)=> OOM。

如果您可以使用另一个供应商的JVM(例如Weblogic),它会动态扩展PermGen空间,因此您将永远不会遇到与PermGen相关的OOM问题。


等一下。这并不是100%的真相。Perm gem问题不仅与JVM及其库有关。糟糕的应用程序也可能导致PermGen错误。此外,动态扩展PerGen与解决它完全不同。在任何VM中等待足够的时间,问题都会发生。 - Antonio
1
正确。但是,动态PermGen允许您在更多的热部署时间中存活。在Sun JVM中,它通常发生在重新部署#1上:-E。 - Vladimir Dyuzhev

0
你使用的是哪个版本的Java?早期的Sun 1.4.2存在错误,但已经工作了很长时间。
顺便问一下,你要如何向你的团队领导传递这个消息?你是团队领导吗?

这其实是自1.5版本以来的新问题。或者至少我只在1.5中遇到了它。 - Vladimir Dyuzhev
我们在1.6中遇到了这个问题。我可能会通过向我的团队负责人发送此页面的链接来“打破这个消息” :-P - Andrey Fedorov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接