【译】Java 17的特点:版本8和17之间的比较,这些年来有什么变化?


原文:Java 17 features: A comparison between versions 8 and 17. What has changed over the years?

新的Java版本每年发布两次,但每一次新的迭代似乎都只是在前一次的基础上有小的改进。虽然这对Java 17来说可能也是如此,但这个版本具有更深的意义,因为Java 8(目前最常用的Java版本)失去了Oracle高级支持。在这篇文章中,我们将探讨最重要的Java 17功能,这两个版本之间的差异,以及它们对Java软件的影响。你应该把你的应用程序从Java 8迁移到17吗?让我们拭目以待。

免责声明:这篇文章最初发表于2021年10月22日。然而,在2022年12月,它被更新了关于Java 8的Oracle企业性能包的新信息。

2022年3月,Java 8失去了Oracle高级支持。这并不意味着它不会收到任何新的更新,但甲骨文投入到维护它的努力可能会比现在小得多。

这意味着有充分的理由转移到新的版本。特别是在2021年9月14日,Java 17被发布。这是新的长期支持版本,Oracle高级支持将持续到2026年9月(至少)。Java 17会带来什么?迁移会有多困难?它值得吗?我将在本文中尝试回答这些问题。

同样值得注意的是,Java 8仍在得到一些扩展–尽管只针对Oracle Java及其昂贵的Java SE订阅。2022年7月19日,一个针对Java 8的Oracle企业性能包被发布。版本号是8u345-PERF-b31。在文章后面比较Java 8和17的特定功能时,会提到这个版本的新增功能。

Java 8的普及 – 历史的小插曲

2014年3月发布的Java 8,目前有69%的程序员在其主要应用中使用。为什么经过7年多的时间,它仍然是最常用的版本?这有很多原因。

Java 8提供了很多语言功能,使开发者愿意从以前的版本转换过来。Lambdas、流、函数式编程、广泛的API扩展 – 更不用说MetaSpace或G1扩展。这是一个值得使用的Java版本。

3年后的2017年9月,Java 9出现了,对于一个典型的开发者来说,它几乎没有什么变化。一个新的HTTP客户端、进程API、小的钻石运算符和try-with-resources改进。

当然,Java 9确实带来了一个重大的变化,甚至是突破性的–Jigsaw项目。它改变了很多,非常多的东西–但在内部。Java模块化带来了巨大的可能性,解决了很多技术问题,适用于每个人,但实际上只有相对较少的用户群需要深入了解这些变化。由于Jigsaw项目引入的变化,很多库需要额外的修改,新的版本被发布,其中一些不能正常工作。

Java 9的迁移–特别是对于大型的企业应用–往往是困难的、耗时的,并引起回归问题。那么,如果没有什么收获,而且要花费大量的时间和金钱,为什么要这样做呢?

Java开发工具包17(JDK 17)于2021年10月发布。现在是不是从8岁的Java 8转移的好时机?首先,让我们看看Java 17里有什么。与Java 8相比,它能给程序员和管理员或SRE带来什么?

Java 17与Java 8的对比 – 变化

这篇文章只涵盖了我认为足够重要或足够有趣的变化。它们并不是 Java 多年来的所有变化、改进和优化。如果你想看JDK的完整变化列表,你应该知道它们是作为JEP(JDK增强建议)被跟踪的。该列表可以在JEP-0中找到。

另外,如果您想比较不同版本的 Java API,有一个很好的工具叫 Java Version Almanac。Java API有许多有用的、小的补充,如果有人想了解所有这些变化,查看这个网站可能是最好的选择。

至于现在,让我们分析一下Java每次迭代中的变化和新功能,从我们大多数Java开发者的角度来看,这些变化和新功能是最重要的。

新的var关键字

增加了一个新的var关键字,允许以一种更简洁的方式声明局部变量。考虑一下这段代码:

// java 8 way
Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
List<MyDomainObjectWithLongName> myList = aDelegate.fetchDomainObjects();
// java 10 way
var myMap = new HashMap<String, List<MyDtoType>>();
var myList = aDelegate.fetchDomainObjects()

当使用var时,声明要短得多,而且,也许比以前更有可读性。我们必须确保首先考虑到可读性,所以在某些情况下,向程序员隐藏类型可能是错误的。注意正确命名变量。

不幸的是,不可能使用var关键字将lambda分配给一个变量:

// causes compilation error: 
//   method reference needs an explicit target-type
var fun = MyObject::mySpecialFunction;

然而,在lambda表达式中使用var是可能的。请看下面的例子:

boolean isThereAneedle = stringsList.stream()
  .anyMatch((@NonNull var s) -> s.equals(“needle”));

在lambda参数中使用var,我们可以给参数添加注解。

Records

纪录岛
人们可以说Records是Java对Lombok的回应。至少有一部分是这样的。记录是一个用来存储一些数据的类型。让我引用JEP 395中的一个片段,它很好地描述了它。

[……]一个记录会自动获得许多标准成员。

  • 为状态描述的每个组件提供一个私有的最终字段。
  • 为状态描述的每个组件提供一个公共的读访问器方法,其名称和类型与组件相同。
  • 一个公共构造函数,其签名与状态描述相同,它从相应的参数初始化每个字段。
  • equals和hashCode的实现,如果两条记录的类型相同且包含相同的状态,则这两条记录是相等的;
  • 以及toString的实现,包括所有记录组件的字符串表示,以及它们的名称。

换句话说,它大致上相当于Lombok的@Value。就语言而言,它有点类似于一个枚举。然而,你不是声明可能的值,而是声明字段。Java根据该声明生成一些代码,并能够以更好的、优化的方式处理它。像枚举一样,它不能扩展或被其他类扩展,但它可以实现一个接口并拥有静态字段和方法。与枚举相反,记录可以用new关键字进行实例化。

一个记录可能看起来像这样:

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {}

而这就是它。很简洁。简洁是优秀的!

任何自动生成的方法都可以由程序员手动声明。一组构造函数也可以被声明。此外,在构造函数中,所有肯定未被赋值的字段都被隐式地赋值给它们相应的构造函数参数。这意味着,在构造函数中可以完全跳过赋值!

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {
  public BankAccount { // <-- this is the constructor! no () !
    if (accountNumber == null || accountNumber.length() != 26) {
      throw new ValidationException(“Account number invalid”);
    }
    // no assignment necessary here!
  }
}

对于所有的细节,如正式的语法,使用和实现的注意事项,请务必参考JEP 359。你也可以查看StackOverflow上关于Java记录的最多投票的问题

扩展的开关表达式

很多语言中都有switch,但由于它的局限性,多年来它的作用越来越小。Java的其他部分在增长,switch却没有。现在,switch案例可以更容易地分组,而且更容易阅读(注意,没有中断!),switch表达式本身实际上返回一个结果。

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
    case SATURDAY, SUNDAY -> true;
};

新的yield关键字可以实现更多的功能,它允许从代码块内返回一个值。它实际上是一个在案例块内工作的返回,并将该值设置为其开关的结果。它也可以接受一个表达式而不是一个单一的值。让我们来看看一个例子:

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
      System.out.println("Work work work");
      yield false;
    }
    case SATURDAY, SUNDAY -> {
      System.out.println("Yey, a free day!");
      yield true;
    }
};

Instanceof模式匹配

虽然不是一个突破性的变化,但在我看来,instanceof解决了Java语言中一个比较恼人的问题。你是否曾经不得不使用这样的语法?

if (obj instanceof MyObject) {
  MyObject myObject = (MyObject) obj;
  // … further logic
}

现在,你不必这样做了。Java现在可以在if里面创建一个局部变量,像这样:

if (obj instanceof MyObject myObject) {
  // … the same logic
}

这只是删除了一行,但就代码流程而言,这是完全不必要的一行。此外,声明的变量可以在同一个if条件下使用,像这样:

if (obj instanceof MyObject myObject && myObject.isValid()) {
  // … the same logic
}

封闭类

这是个很难解释的问题。让我们从这个开始–switch中的 “无默认 “警告是否曾经让你感到恼火?你覆盖了领域所接受的所有选项,但警告仍然存在。封闭类让你摆脱了对instanceof类型检查的这种警告。

如果你有一个像这样的层次结构:

public abstract sealed class Animal
    permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}

你现在将能够做到这一点:

if (animal instanceof Dog d) {
    return d.woof();
} 
else if (animal instanceof Cat c) {
    return c.meow();
}

而且你不会得到一个警告。好吧,让我重新表述一下:如果你得到一个类似序列的警告,那么这个警告将是有意义的!这就是为什么你会得到警告。而更多的信息总是好的。

我对这个变化的感觉很复杂。引入一个循环引用似乎不是一个好的做法。如果我在我的生产代码中使用这个,我会尽力把它藏在一个很深的地方,并且永远不向外界展示它–我的意思是,永远不通过API暴露它,而不是说我会为在有效情况下使用它而感到羞耻。

文本块

在Java编程中,声明长字符串的情况并不常见,但一旦发生,就会让人感到厌烦和困惑。Java 13为此提出了一个修复方案,并在以后的版本中进一步改进。现在,一个多行文本块可以按如下方式声明:

String myWallOfText = ”””
______         _   _           
| ___ \       | | (_)          
| |_/ / __ ___| |_ _ _   _ ___ 
|  __/ '__/ _ \ __| | | | / __|
| |  | | |  __/ |_| | |_| \__ \
\_|  |_|  \___|\__|_|\__,_|___/
”””

不需要转义引号或换行。可以转义换行并保持字符串为单行,像这样:

String myPoem = ”””
Roses are red, violets are blue - \
Pretius makes the best software, that is always true
”””

这就相当于:

String myPoem = ”Roses are red, violets are blue - Pretius makes the best software, that still is true”.

文本块可以用来在你的代码中保持一个合理的可读的json或xml模板。外部文件仍然可能是一个更好的主意,但如果有必要,用纯Java来做仍然是一个不错的选择。

更好的NullPointerExceptions

所以,我的应用程序中曾经有这样一连串的呼叫。我想你也可能对它感到熟悉:

company.getOwner().getAddress().getCity();

我得到了一个NPE,它准确地告诉我在哪一行遇到了null。是的,就是那一行。没有调试器,我无法知道哪个对象是空的,或者说,哪个调用操作实际上导致了这个问题。现在消息会很具体,它会告诉我们,JVM “无法调用Person.getAddress()”。

实际上,这更像是JVM的变化,而不是Java的变化–因为构建详细消息的字节码分析是在运行时JVM进行的–但它确实对程序员有很大的吸引力。

新的HttpClient

有很多库可以做同样的事情,但在Java中拥有一个合适的HTTP客户端是很好的。你可以在Baeldung中找到关于新的API的一个很好的介绍。

新增Optional.orElseThrow()方法

一个关于Optional的get()方法被用来获取Optional下的值。如果没有值,这个方法会抛出一个异常。就像下面的代码:

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .get();

Java 10在Optional中引入了一个新方法,叫做orElseThrow()。它的作用是什么?完全一样! 但是考虑到程序员的可读性变化。

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .orElseThrow();

现在,程序员清楚地知道当对象未被找到时将会发生什么。事实上,我们推荐使用这个方法,而不是简单的、无处不在的get()。

JVM 17与JVM 8的变化

Project Jigsaw

JDK 9的Project Jigsaw大大改变了JVM的内部结构。它改变了JLS和JVMS,增加了几个JEP(可在上面的Project Jigsaw链接中找到列表),最重要的是,引入了一些破坏性的变化,这些变化与以前的Java版本不兼容。

Java 9模块被引入,作为一个额外的、最高级别的jar和类组织。关于这个话题有很多介绍性的内容,比如Baeldung上的这个,或者Yuichi Sakuraba的这些幻灯片。

收益很大,虽然肉眼看不出来。所谓的JAR地狱已经不存在了(你去过吗? 我去过……而且真的是一个地狱),尽管现在模块地狱也是一种可能。

从一个典型的程序员的角度来看,这些变化现在几乎是看不见的。只有最大和最复杂的项目可能会受到某种程度的影响。几乎所有常用的库的新版本都遵守新的规则,并在内部考虑到这些规则。

垃圾回收

从Java 9开始,G1是默认的垃圾收集器。与Parallel GC相比,它减少了暂停时间,尽管它的总体吞吐量可能较低。自从成为默认的垃圾收集器后,它经历了一些变化,包括将未使用的承诺内存返回给操作系统的能力(JEP 346)。

在Java 11中引入了ZGC垃圾收集器,并在Java 15中达到产品状态(JEP 377)。它的目的是进一步减少停顿。从Java 13开始,它还能够将未使用的已承诺内存返回给操作系统(JEP 351)。

在JDK 14中引入了Shenandoah GC,并在Java 15中达到产品状态(JEP 379)。它的目的是保持较低的暂停时间,并且与堆的大小无关。

请注意,在Java 8中,你的选择要少得多,如果你没有手动改变你的GC,你仍然使用并行GC。简单地切换到Java 17可能会使你的应用程序工作得更快,方法运行时间更一致。切换到,然后不可用的ZGC或Shenandoah可能会得到更好的结果。

最后,还有一个新的No-Op垃圾收集器可用(JEP 318),尽管它是一个实验性的功能。这个垃圾收集器实际上不做任何工作–因此允许你精确测量你的应用程序的内存使用情况。如果你想尽可能地保持你的内存操作吞吐量,那就很有用。

如果你想了解更多关于可用选项的信息,我推荐你阅读Marko Topolnik的一系列伟大的文章,对GCs进行了比较。

前面提到的G1垃圾收集器是在Oracle企业性能包8u345版本中添加到Oracle Java 8的。与Compact Strings一起,它可以对Java应用程序的内存消耗产生重大影响。

Container感知

如果你不知道,曾经有一段时间,Java不知道它是在一个容器中运行。它没有考虑到容器的内存限制,而是读取可用的系统内存。因此,当你有一台拥有16GB内存的机器,将你的容器的最大内存设置为1GB,并在上面运行一个Java应用程序时,应用程序往往会失败,因为它将试图分配比容器上可用的更多的内存。Carlos Sanchez的一篇好文章更详细地解释了这一点。

这些问题现在已经成为过去。从Java 10开始,容器集成被默认启用。然而,这对你来说可能不是一个明显的改进,因为在Java 8的更新131中也引入了同样的变化,尽管它需要启用实验性选项并使用-XX:+UseCGroupMemoryLimitForHeap。

PS:使用-Xmx参数指定Java的最大内存通常是个好主意。在这种情况下,问题就不会出现。

CDS档案

为了使JVM的启动速度更快,在Java 8发布后的这段时间里,CDS档案经历了一些变化。从JDK 12开始,在构建过程中创建CDS档案是默认启用的(JEP 341)。JDK 13中的一项改进(JEP 350)允许在每个应用程序运行后更新档案。

类数据共享也在Oracle企业性能包8u345版本中为Java 8实现。然而,目前还不清楚这些变化的意义有多大;描述表明,只增加了JEP 310的范围。然而,我无法确认这一点。

Nicolai Parlog的一篇很好的文章演示了如何使用这个功能来改善你的应用程序的启动时间。

Java Flight Recorder and Java Mission Control

Java Flight Recorder(JEP 328)允许以较低的(目标1%)性能成本对运行中的Java应用程序进行监控和分析。Java Mission Control允许摄取和可视化JFR数据。请看Baeldung的教程,大致了解如何使用它以及可以从中得到什么。

你应该从Java 8迁移到Java 17吗?

简而言之:是的,你应该这样做。如果你有一个大型的、高负荷的企业应用,并且仍在使用Java 8,你肯定会在迁移后看到更好的性能、更快的启动时间、更低的内存占用率。从事该应用的程序员也应该更高兴,因为语言本身有很多改进。

然而,这样做的成本是很难估计的,而且根据所使用的应用服务器、库和应用程序本身的复杂性(或者说是它使用/重新实现的低级功能的数量)而有很大差异。

如果你的应用是微服务,很可能你只需要将基础docker镜像改为17-alpine,将maven中的代码版本改为17,一切就能正常工作了。一些框架或库的更新可能会派上用场(但无论如何你都会定期进行更新,对吧?)

现在,所有流行的服务器和框架都支持Java 9的Jigsaw项目。它是生产级的,经过了大量的测试,并在多年后修复了错误。许多产品提供了迁移指南,或者至少为Java 9兼容的版本提供了广泛的发布说明。请看OSGI的一篇不错的文章或Wildfly 15的一些发布说明,其中提到了模块支持。

如果你使用Spring Boot作为你的框架,有一些文章可以提供迁移技巧,比如spring-boot wiki中的这篇,Baeldung上的这篇,以及DZone上的另一篇。infoq也有一个有趣的案例研究。将Spring Boot 1迁移到Spring Boot 2是一个不同的话题,可能也值得考虑。Spring Boot本身有一个教程,Baeldung上也有一篇文章涉及这个话题。

如果你的应用程序没有自定义类加载器,没有严重依赖Unsafe,没有大量使用sun.misc或sun.security,那么你可能会没事。请参考JDEP关于Java依赖性分析工具的这篇文章,了解你可能需要做的一些改变。

有些东西从第8版开始就从Java中删除了,包括Nashorn JS引擎、Pack200 APIs和工具、Solaris/Sparc端口、AOT和JIT编译器、Java EE和Corba模块。有些东西仍然存在,但已被废弃删除,如Applet API或安全管理器。由于有很好的理由将其删除,你应该重新考虑在你的应用程序中使用它们。

我询问了我们Pretius的项目技术负责人关于他们从Java 8到Java 9+迁移的经验。有几个例子,没有一个是有问题的。在这里,一个库不工作,不得不更新;在那里,需要一些额外的库或配置,但总的来说,这根本不是一个糟糕的经历。

总结

Java 17 LTS将在未来几年内得到支持。另一方面,Java 8的支持已经结束。这当然是考虑转移到新版本的Java的坚实理由。在这篇文章中,我介绍了第8版和第17版之间最重要的语言和JVM变化(包括一些关于Java 8到Java 9+迁移过程的信息),这样就更容易理解它们之间的差异–以及评估迁移的风险和收益。

如果你碰巧是你公司的决策者,要问自己的问题是:会不会有 “好时机 “把Java 8留下来?有些钱总是要花的,有些时间总是要消耗的,有些需要做的额外工作的风险总是存在的。如果永远没有 “好时机”,那么现在很可能是一个好时机,就像永远不会有一样。

Java 17 特性的常见问题

Java 8是什么时候发布的?

Java 8 是在 2014 年 3 月发布的。

Java 17是什么时候发布的?

Java 17于2021年9月15日发布。

Java的最新版本是什么?

Java的最新版本是Java 19,于2022年9月发布。

我有什么版本的Java?

您可以在Java控制面板的 “常规 “选项卡中的 “关于 “部分查看您当前的Java版本。您也可以在您的bash/cmd中输入以下命令:

java -version

什么是Java 17?

它是具有长期支持的Java SE platform 的最新版本。

如何更新到Java 17?

安装软件只需运行可执行文件即可,但让您的系统为这一变化做好准备可能更复杂。

JDK 17 有哪些新功能?

JDK 17 是一个大型的 Java 更新,有大量的改进和新东西。它提供了以下新功能:

  1. 增强的伪随机数生成器
  2. 恢复始终严格的浮点运算语义
  3. macOS/AArch64支持
  4. 新的macOS Rendering pipelines
  5. 强化封装的JDK内部结构
  6. 废弃Applet API,以便于将来删除
  7. Switch 的模式匹配(预览)
  8. 封闭的类(Sealed Classes)
  9. 移除RMI的激活
  10. 撤销安全管理器
  11. 外来函数和内存API(孵化)
  12. 移除实验性AOT和JIT编译器
  13. 特定上下文的反序列化过滤器
  14. 矢量API(第二孵化)

Java 17和Java 18之间有什么区别

Java 17是一个长期支持版本–它至少会被支持8年。另一方面,Java 18只是一个较小的更新,有一些额外的功能,支持期为6个月。

JDK 17中包括JRE吗?

是的,与所有 JDK 版本一样,JDK 17 包括 Java 17 JRE。

什么是 Java 8 的企业性能包?

这是一种付费订阅,您可以通过购买它在 Java 8 中获得一些 Java 17 的功能,如 G1 垃圾收集器。

【译】选择最佳垃圾收集算法,以获得更好的Java性能


原文:Choosing the Best Garbage Collection Algorithm for Better Performance In Java

在这篇文章中,我将解释垃圾收集是如何在幕后工作以释放内存的。在过去的几个 Java版本中,Java内存管理已经出现了很多。了解不同的GC算法将帮助你更好地调整它(如果需要的话),这取决于我们在许多基于Java的应用程序性能测试中看到的不同性能问题。当你的Java应用程序运行时,它会创建占用内存空间的对象。只要该对象被使用(即被应用程序引用的地方),它就会占用内存。当对象不再被使用时(例如,当你干净地关闭一个DB连接时),对象所占用的空间就可以被垃圾收集器回收。

对于任何基于Java的应用程序的性能测试,如何为你的用例选择最好的垃圾收集器?在我们进入这个问题之前,让我们先谈谈下面的一些基本概念。

权衡利弊

当我们谈论垃圾收集时,一般需要考虑三件事。

  1. 内存

这是分配给程序的内存量,这被称为HEAP内存。请不要与footprint(GC算法运行所需的内存量)相混淆。

  1. 吞吐量

我们需要了解的第二件事是吞吐量。吞吐量是指代码运行的时间与你的垃圾收集运行的时间相比有多少。例如,如果你的吞吐量是99%,这意味着99%的时间代码在运行,1%的时间垃圾收集在运行。对于任何高容量的应用程序,我们希望在我们运行的任何负载测试中,尽可能地提高吞吐量。

  1. 延迟性

我们需要了解的第三个方面是延迟(LATENCY)。延迟是指每当垃圾收集运行时,我们的程序为垃圾收集的正常运行停止多少时间。所有这些都是以毫秒为单位的,但它们可以达到几秒钟,这取决于内存的大小和我们为负载测试选择的垃圾收集算法。理想情况下,我们希望LATENCY尽可能的低或者尽可能的可预测。

垃圾收集的代际假说

这个假说说,大多数被创建的对象都是早死的。当一个对象不能再被访问时,它就被标记为符合垃圾收集的条件,这可能发生在对象超出范围时。当一个对象的引用变量被分配一个明确的空值或被重新初始化时,也可能发生。如果一个对象不能被访问,这意味着任何活的线程都不能通过程序中使用的任何引用变量访问它。

垃圾收集算法将你的堆内存大小分成YOUNG一代和OLD一代。当我们第一次创建对象时,它们被保留在年轻一代中,大多数对象在年轻时就死亡了,或者它们很快就有资格进行垃圾收集,这就是为什么我们有大量的垃圾收集运行在年轻一代上,这种收集被称为Minor GC。

如果有一些对象,比如类级变量,也被称为实例级变量,它们的寿命会更长,即使经过多次小规模的GC收集,当这些对象仍然不符合垃圾收集的条件时,它们会被提升到OLD一代。每当有大量的对象在OLD代中,比方说,如果OLD代中占用的内存空间超过了阈值,例如60%或70%,那么就会触发Major GC。

垃圾收集算法的步骤

任何垃圾收集算法都有三个基本步骤。

  1. 标记
    • 这是第一步,GC通过内存中的对象图,从所有对多个对象的引用的根节点开始,将可以到达的对象标记为活的。当标记阶段结束时,每个活的对象都被标记。这个标记阶段的持续时间取决于活着的对象的数量,直接增加堆的内存并不影响标记阶段的持续时间。
  2. 扫除
    • 无论哪个对象都是可触及的,没有被触及的对象都会被删除,并重新获得内存。
  3. 压缩
    • 夯实是将所有东西按顺序排列的过程。这一步是通过压缩内存来消除内存碎片,以消除分配的内存区域之间的空隙。

标记和复制算法

在YOUNG世代中,一般来说,空间被分为EDEN空间和两个SURVIVOR SPACE 1和SURVIVOR SPACE 2的幸存者空间。

所有在内存中创建的新对象首先被分配到EDEN空间。每当Minor GC运行时,只有EDEN空间的活对象被标记并复制到SURVIVOR空间,这包括以下步骤

  1. 它首先将所有的对象标记为活的,这意味着这些对象仍在被使用或被引用,不符合垃圾收集的条件。
  2. 将所有活着的对象复制到S1或S2的SURVIVOR空间。

一旦它复制了所有的活对象,现在这个EDEN空间由已经被复制的对象和符合垃圾收集条件的对象组成,整个EDEN空间就被清除掉了。

MARK SWEEP和COMPACT算法

这通常是在旧的一代运行。假设我们有很多被分配的对象,其中有些是活的,有些是符合垃圾收集条件的。首先,我们将只标记活的对象。其次,我们将清扫并删除所有符合垃圾收集条件的对象,然后它将删除空格并使其成为空白,从技术上讲,我们并没有删除空格,数据结构本身被更新,说空格是空的。第三个方面是压缩,我们将把所有仍在使用的活对象移到左边,并把它们集中在一起。这种方法的缺点是增加了GC暂停的时间,因为我们需要把所有的对象复制到一个新的地方,并更新所有对这些对象的引用。

紧凑的好处是,当我们想分配新的对象时,我们所要做的就是保持一个指针和引用,说明左边的所有东西都被利用了,右边的所有东西都被释放了。

串行垃圾收集器(-XX:+UseSerialGC)

串行收集器是所有收集器中占地面积最小的。这个垃圾收集器运行所需的数据结构量的足迹是非常小的。这个收集器使用一个单线程来进行小收集和大收集。串行收集器使用凹凸指针技术进行压缩,这就是为什么分配的速度更快。这种收集器一般最适合在共享CPU上运行的应用程序,其内存量非常小。

让我们想象一下,我们有一个QUAD CORE CPU,四个应用程序在上面运行。如果你的垃圾收集器不是单线程的,而是多线程的,在某个时间点上,我们的垃圾收集器会在CPU的四个核上启动所有的四个线程,并利用整个CPU进行自己的垃圾收集,这时在CPU上运行的其他应用程序会受到影响。如果有多个应用程序运行在一个CPU上,并且我们必须确保我们的垃圾收集器不影响其他核心或应用程序,那么我们可以使用串行垃圾收集器。

并行/吞吐量收集器 (-XX:+UseParallelGC , -XX:+UseParallelOldGC)

下一个需要了解的收集器叫做并行收集器。我们有Parallel Collector和Parallel Old Collector。我们一般只使用Parallel Old Collector,它在Minor GC和Major GC上都使用多个线程。这个收集器并不与应用程序同时运行。它被命名为Parallel是因为它有多个垃圾收集线程,所有这些线程都是平行运行的,但是当垃圾收集器运行时,所有的线程都是停止的,如果我们的应用程序被部署在多核或多处理器系统上,这个收集器会给我们带来最大的吞吐量。

在最短的时间内,它将能够收集尽可能多的垃圾。它可以停止整个应用程序,并且可以停止一段时间,它是最好的收集器,只用于批处理应用程序。在批处理应用中,我们不关心用户和响应时间,因为在前端没有用户,它的批处理应用在幕后运行。对于批处理应用,并行收集器将是最好的选择。

并发标记和扫描收集器 (-XX:+UseConcMarkSweepGC, -XX:+UseParNewGC)

这被称为并发标记和扫除。这个收集器与应用程序同时运行,以标记所有实时对象。应用程序必须停止的时间较少,所以应用程序的延迟也较少。在实际收集中,它仍然有STW暂停。STW也被称为 “停止世界”(Stop the World)的停顿,这意味着它在非常小的时间内停止应用程序来进行实际的垃圾收集。这种CMS收集器比并行收集器需要更多的空间,它有更多的数据结构需要处理。它的吞吐量比并行收集器小,但优点是它的暂停时间比并行收集器小。这种收集器是所有普通Java应用程序中最常用的收集器。

G1收集器(-XX:+UseG1GC)(垃圾优先)

对CMS收集器的改进被称为G1收集器。这种收集器没有为Heap设置特定的年轻一代和老一代,而是使用整个Heap并将其划分为多个区域。它有更多的占用空间,这种收集器的优点是它有最可预测的延迟,这是该收集器的最好的特点。当我们启动我们的应用程序时,我们可以在这个变量上传递我们的应用程序可以承受的最大暂停时间(maxTargetPauseTime),例如10ms。G1收集器将努力确保垃圾收集只在10ms内完成,即使有一些垃圾残留,它也会在下一个周期内处理。如果我们想获得可预测的延迟和暂停时间,G1收集器将是最好的收集器。这是最常用的收集器,可以满足所有的性能测试需求。

Shenandoah收集器(-XX:+UseShenandoahGC)

还有一个收集器叫做Shenandoah收集器。这个收集器是在G1收集器的基础上改进的,它需要更多的占用空间,所以它在幕后需要更多的数据结构,但它的延迟比G1收集器更低。

Shenandoah是一个超低暂停时间的垃圾收集器,通过与运行中的Java程序同时进行更多的垃圾收集工作来减少GC的暂停时间。CMS和G1都对实时对象进行并发标记。Shenandoah增加了并发压缩功能。

Epsilon Collector(-XX:+UseEpsilonGC) – JDK 的无为收集器

JDK 11中引入的Epsilon垃圾收集器是一个实验性的收集器,只分配内存。它不能释放任何已分配的内存,所以应用程序很可能因为OutOfMemoryError而崩溃。Epsilon收集器中的GC不做任何GC循环,因此不关心对象图、对象标记、对象复制等问题。一旦Java堆被耗尽,就不可能进行分配,不可能进行内存回收,因此测试会失败。

最显著的优点是没有GC开销,JVM不会暂停清除内存,因为它甚至不会尝试释放任何内存。Epsilon GC已经被添加为一个基准,用于测试应用程序的性能、内存使用、延迟和吞吐量的改进。Epsilon收集器帮助我们计算出Java虚拟机(JVM)用尽所有内存并关闭所需的时间。Epsilon GC有助于测试原始应用程序的性能,没有GC的干扰,也没有嵌入代码中的GC障碍。在JDK 11中,Epsilon GC功能默认是禁用的,我们必须启用才能使用这个收集器。

对于超延迟敏感的应用程序,要完全了解内存分配、内存占用,以及了解程序的性能受垃圾收集影响的程度,Epsilon收集器是最好用的。

(ZGC) Z垃圾收集器 (-XX:+UseZGC)

Z垃圾收集器(ZGC)是可扩展的,具有低延迟性。它是一个全新的GC,从头开始编写。它可以标记内存,复制和重新定位内存,所有这些都是并发的,它可以在堆内存中工作,范围从KBs到大型TB内存。作为一个并发的垃圾收集器,ZGC保证不超过应用延迟10毫秒,即使是更大的堆大小。ZGC最初是作为Java 11(Linux)的实验性GC发布的,随着时间的推移,预计JDK 11、13和14会有更多的变化。

在ZGC中,stop-the-world的暂停只限于根扫描。它使用带有彩色指针的负载屏障来执行线程运行时的并发操作,它们被用来跟踪堆的使用。有色指针是ZGC的核心概念之一,它使ZGC能够找到、标记、定位和重新映射对象。与G1相比,ZGC有更好的方法来处理非常大的对象分配,这在回收内存和重新分配内存时有很高的性能,它是一个单代GC。

ZGC将内存划分为若干区域,也称为ZPages。这些ZPages可以动态地创建和销毁,也可以动态地确定大小。与其他GC不同,ZGC的物理堆区域可以映射到一个更大的堆地址空间(可以包括虚拟内存),这可以避免内存碎片问题。

结论

一般来说,串行收集器适用于小型设备,或者当我们想确保GC不影响其他应用程序或CPU时,并行收集器最适合批量应用程序,CMS收集器用于一般应用程序,G1收集器最适合可预测的延迟,Shenandoah收集器是G1的改进,我们将能够在几个版本的Java中作为默认收集器使用(从Java 12)。Epsilon和ZGC收集器是在JDK 11中引入的新的实验性收集器,它们在不同的版本中仍然经历着许多变化。

参考资料有很多,我真诚地感谢所有的Java大师:)

https://blogs.oracle.com/javamagazine/

http://plumbr.io/

https://www.journaldev.com/

谢谢你阅读这篇文章:) 学习愉快:)

全文Translated with DeepL,致谢DeepL。

修复Big Sur JD-GUI启动失败,无法正常使用


升级Big Sur之后,JD-GUI启动会报错,无法正常使用

ERROR launching 'JD-GUI'

No suitable Java version found on your system!
This program requires Java 1.8+
Make sure you install the required Java version.

错误原因分析:

JD-GUI启动的时候,会调用/Applications/JD-GUI.app/Contents/MacOS/universalJavaApplicationStub.sh检查启动环境,其中涉及到Java安装环境的检查,根据错误提示,可以找到具体发生错误的代码:

if /usr/libexec/java_home -F -v ${JVMVersion}; then

其中${JVMVersion}是从/Applications/JD-GUI.app/Contents/Info.plist文件中读取JVMVersion参数,在Info.plist文件中默认配置JVMVersion参数值为1.8+,通过将参数代入/usr/libexec/java_home -F -v命令执行,会发现报如下错误:

xieshaohu@MBP-2018 ~ % /usr/libexec/java_home -F -v 1.8+
The operation couldn’t be completed. Unable to locate a Java Runtime that supports (null).
Please visit http://www.java.com for information on installing Java.

这个命令出错的原因是我本地没有安装1.8的jdk,或者本身这个版本号就不应该设置为1.8+,将参数改成我本地已经安装的openjdk版本11后,命令已经能够正常执行:

xieshaohu@MBP-2018 ~ % /usr/libexec/java_home -F -v 11  
/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home

命令测试通过后,将/Applications/JD-GUI.app/Contents/Info.plist文件中JVMVersion参数修改为11,JD-GUI已经能够正常启动。

延伸知识点:

  • macOS的jdk默认安装在/Library/Java/JavaVirtualMachines下
  • 可以通过/usr/libexec/java_home命令显示当前jdk的java_home位置,而JD- GUI是因为参数判断出错,导致无法正常获取java主目录,所以无法启动。
  • /usr/libexec/java_home -V命令可以列出本机安装的所有jdk版本
  • /usr/libexec/java_home -V 11 命令可以列出指定版本的jdk主目录

XML创建/解析/验证


最近用到XML结合JMS TextMessage传输数据的技术。采用TextMessage+XML格式传输对象是考虑到这个平台不仅仅使用Java语言开发,同时还需要能够支持其他语言接入:比如C, Ruby, Perl, Python, PHP, ActionScript/Flash, Smalltalk,借助于ActiveMQ提供的Stomp,这些非Java语言能够很方便的就接入平台。

总结下来整个传输过程如下(Java to Java):

DTO对象->XML对象->XML文本->JMS TextMessage->XML文本->XML对象->DTO

对象

涉及技术点:

  1. JavaDTO对象如何转变为XML对象
  2. 如何从XML对象中获得XML文本
  3. 如何通过XML文本构造XML对象
  4. 遍历XML对象创建JavaDTO对象

先说第一点,JavaDTO对象如何转变为XML对象

// 创建DocumentBuilderFactory,创建和解析XML都需要
DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder
DocumentBuilder builder = domfac.newDocumentBuilder();
// 新建文档
Document doc = builder.newDocument();
doc.setXmlVersion("1.0");
// 创建文档根节点
Element root = doc.createElement("root");
// 循环创建叶子节点
Element leaf = doc.createElement("leaf");
// 添加叶子节点属性
leaf.setAttribute("name", "从Java对象中获取");
// 增加叶子节点至根节点
root.appendChild(leaf);

至此,XML对象已经创建完成,现在需要从XML对象中获取XML文本。

继续阅读

Log4j中添加自定义的日志级别


Log4j自带了trace,debug,info.warn,error,fatal几个日志级别。

由于项目需要拆分业务和系统日志,而我又想偷懒,所以决定在log4j日志框架上增加自定义的日志级别专门用于业务日志输出。需要增加的日志级别定为audit,在log4j所有自带日志级别之上。

Log4j官方推荐的增加自定义的日志级别的方法是“继承org.apache.log4j.Level类”,具体的可以参考log4j中的示例XLevel。由于系统开发时不直接使用Log4j,而是通过slf4j调用log4j,官方推荐的方法行不通,所以决定修改log4j和slf4j相关源代码。

Log4j源代码修改点如下:

org.apache.log4j.Logger添加audit方法

public void audit(Object message) {
	if (repository.isDisabled(Level.AUDIT_INT)) {
		return;
	}

	if (Level.AUDIT.isGreaterOrEqual(this.getEffectiveLevel())) {
		forcedLog(FQCN, Level.AUDIT, message, null);
	}
}

org.apache.log4j.Priority类添加AUDIT_INT属性

public final static int AUDIT_INT = 60000; //日志级别大于log4j包含的所有日志级别,便于后期通过Threshold属性过滤日志

org.apache.log4j.Level类添加AUDIT相关属性和方法

public static final Level AUDIT = new Level(AUDIT_INT, "AUDIT", 0);public static Level toLevel(int val, Level defaultLevel) {
	switch(val) {
		case ALL_INT: return ALL;
		case AUDIT_INT: return Level.AUDIT;
		case DEBUG_INT: return Level.DEBUG;
		case INFO_INT: return Level.INFO;
		case WARN_INT: return Level.WARN;
		case ERROR_INT: return Level.ERROR;
		case FATAL_INT: return Level.FATAL;
		case OFF_INT: return OFF;
		case TRACE_INT: return Level.TRACE;
		default: return defaultLevel;
	}
}

至此Log4j修改完成,下面修改slf4j相关源代码。slf4j需要修改slf4j-api和slf4j-log4j12两个包 继续阅读

FWK005 parse may not be called while parsing.


最近在使用javax.xml.parsers.DocumentBuilder解析xml文件的时候偶尔会出错:

org.xml.sax.SAXException: FWK005 parse may not be called while parsing.
        at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:263)
        at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:284)
        at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:208)
...

跟踪了一下代码,发现这个异常是在com.sun.org.apache.xerces.internal.parsers.DTDConfiguration.parse(DTDConfiguration.java:546)抛出来的。该段代码结构如下:

if(fParseInProgress) {
    throw new XNIException("FWK005 parse may not be called while parsing.");
}

fParseInProgress = true;

// 解析xml文件

finally {
    fParseInProgress = false;
}

从程序逻辑来看,如果当前DocumentBuilder对象正在转换文档,此时再次请求转换文档,那么直接抛出XNIException(“FWK005 parse may not be called while parsing.”);异常。

这个问题也比较好解决,一种是对转换xml文档的方法,增加synchronized关键字,这样子不会有两个线程同时访问方法。

还有一种方法是创建一个DocumentBuilder类型的ThreadLocal变量,这样子每个线程都拥有自己的DocumentBuilder对象,能够同时转换多个xml文件。代码如下:

private static ThreadLocal docBuildeIns = new ThreadLocal() {
    protected DocumentBuilder initialValue() {
        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            String msg = "DocumentBuilder 对象初始化失败!";
            log.error(msg, e);
            throw new IllegalStateException(msg, e);
        }
    }
};

解析xml文件时的调用方法:

docBuildIns.get().parse(File);

get()方法返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。

跟踪 C3p0 连接池连接泄漏之参数优化与日志分析


上一篇:跟踪 C3p0 连接池连接泄漏 我们有谈到c3p0的debugUnreturnedConnectionStackTraces、unreturnedConnectionTimeout参数的说明,此篇日志进一步介绍应用这两个参数后的一些经验。

首先,为了跟踪问题,debugUnreturnedConnectionStackTraces参数肯定是设置为true的。下面主要说明unreturnedConnectionTimeout参数的设置,unreturnedConnectionTimeout参数是在连接被应用程序checkout后指定时间内未checkin则由连接缓冲池执行kill操作,同时打印堆栈跟踪信息。在我的应用里,maxIdleTime的设置是120秒,所以,我把unreturnedConnectionTimeout设置成150秒,如果达到最大存活时间后,连接还是不能被连接缓冲池正常关闭,那么肯定出现了连接泄漏,此时,再过30秒后,由连接缓冲池主动执行kill。

通过以上设置后,确实收获了一些成果,通过分析日志,找到了连接泄漏问题代码。异常信息如下:

2011-04-06 15:49:42,599 INFO : com.mchange.v2.resourcepool.BasicResourcePool.removeResource(BasicResourcePool.java:1395) – Logging the stack trace by which the overdue resource was checked-out.
java.lang.Exception: DEBUG ONLY: Overdue resource check-out stack trace.
at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:506)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:525)
at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:128)
at org.springframework.orm.hibernate3.LocalDataSourceConnectionProvider.getConnection(LocalDataSourceConnectionProvider.java:82)
at org.hibernate.jdbc.ConnectionManager.openConnection(ConnectionManager.java:423)
at org.hibernate.jdbc.ConnectionManager.getConnection(ConnectionManager.java:144)
at org.hibernate.jdbc.AbstractBatcher.prepareQueryStatement(AbstractBatcher.java:139)
at org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:1547)
at org.hibernate.loader.Loader.doQuery(Loader.java:673)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:236)
at org.hibernate.loader.Loader.doList(Loader.java:2220)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2104)
at org.hibernate.loader.Loader.list(Loader.java:2099)
at org.hibernate.loader.custom.CustomLoader.list(CustomLoader.java:289)
at org.hibernate.impl.SessionImpl.listCustomQuery(SessionImpl.java:1695)
at org.hibernate.impl.AbstractSessionImpl.list(AbstractSessionImpl.java:142)
at org.hibernate.impl.SQLQueryImpl.list(SQLQueryImpl.java:152)
at com.leo.dao.XXXDAO.queryXXX(XXXDAO.java:20)
at jsp_servlet._keyareas._country.__taskcdb_add._jspService(__taskcdb_add.java:195)
at weblogic.servlet.jsp.JspBase.service(JspBase.java:34)
at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:227)
at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:125)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:300)
at weblogic.servlet.internal.ServletStubImpl.onAddToMapException(ServletStubImpl.java:416)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:326)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:183)
at weblogic.servlet.internal.RequestDispatcherImpl.invokeServlet(RequestDispatcherImpl.java:526)
at weblogic.servlet.internal.RequestDispatcherImpl.forward(RequestDispatcherImpl.java:253)

通过堆栈信息,很容易就确认了问题点。这个问题点是由于DAO使用不规范导致的,XXXDAO继承了HibernateDaoSupport,在方法中使用了getHibernateTemplate().getSessionFactory().openSession()获得session,然后通过session.createSQLQuery(),随后却并没有执行session.close()。

关于日志大小:在目前maxIdleTime=120,maxPoolSize=18,minPoolSize=5,idleConnectionTestPeriod=30的情况下,日志PatternLayout %d %-5p: %l – %m%n,一天的日志记录情况大约是12M,在硬盘空间允许的情况下建议按天存储日志。

使用JRockit JVM运行eclipse


来源:Running Eclipse on JRockit JVM

Oracle收购Sun之后,曾有媒体爆出Oracle考虑整合Sun JVM和JRockit JVM,要知道JRockit JVM也不是Oracle自己开发的,来自于之前的BEA公司。其实作为开发人员,真心希望两个JVM整合,以提供更好的性能。最近就在折腾把eclipse以及tomcat的开发环境从Sun JVM切换到JRockit JVM。

虽然JRockit JVM是着眼与服务器端的高性能JVM,但是也能运行客户端程序,像eclipse就能在JRockit JVM上很好的运行。

一、最简单的配置方法

-showsplash
org.eclipse.platform
-vm
/opt/Oracle/jrockit-jdk1.6.0_22-R28.1.1-4.0.1/bin/java

二、指定堆内存大小

-showsplash
org.eclipse.platform
-vm
/opt/Oracle/jrockit-jdk1.6.0_22-R28.1.1-4.0.1/bin/java
-vmargs -Xms384m -Xmx384m

三、配置deterministic GC,提高响应速度

-showsplash
org.eclipse.platform
-vm
/opt/Oracle/jrockit-jdk1.6.0_22-R28.1.1-4.0.1/bin/java
-vmargs
-Xms384m
-Xmx384m
-XgcPrio:deterministic
-XpauseTarget:20
-XXcompactratio:1

安装完成后,从主观感受上来看,eclipse响应速度确实快了不少。客观上,使用384内存出来的效果和Sun JVM服务器模式运行512内存出来的效果一样,也就是说相比调整后Sun JVM(Eclipse JVM 性能调优),内存占用有所降低。

此外,JRockit JVM还提供了Mission Control for eclipse插件,可以在eclipse通过Mission Control perspective视图分析JVM工作情况。这个插件的安装方法也很简单。找到安装目录下的missioncontrol目录,将里面的featrues和plugins文件夹复制到eclipse中即可。

对于tomcat,我们在Server Runtime Environment中选择jrockit jvm即可实现使用jrockit jvm运行tomcat,启动速度那是飕飕的!!!有木有!!!

资源:
Oracle JRockit Online Documentation

解决JasperReport 导出 Html 时图片显示问题


JasperReport导出为Html时,图片显示不正常的问题,从项目一开始就困扰着我,今天终于解决了。太爽了!o(∩∩)o…

首先自己犯了一个错误,Html报表的图片输出没有使用net.sf.jasperreports.j2ee.servlets.ImageServlet,导致新打开的报表总是使用的之前报表的图片,且第一次输出报表时,图片不加载。

JasperReport导出Html报表的正确配置应该是

  1. 在web.xml配置net.sf.jaasperreports.j2ee.servlets.ImageServlet
  2. 设置JRHtmlExporterParameter.IMAGES_DIR_NAME,用于存放图片
  3. 导出报表时设置JRHtmlExporterParameter.IMAGES_URI变量,内容和web.xml中配置的ImageServlet的url-pattern一致
  4. 在Session中设置print对象
  5. 然后就可以执行导出了

下面是代码

String jasperFilePath = “WEB-INF/reports/test.jasper”;
// 获得报表模板文件对象
File reportFile = getApplicationContext().getResource(jasperFilePath).getFile();

JasperPrint print = JasperFillManager.fillReport(reportFile.getAbsolutePath(), map, dataSource.getConnection());

JRHtmlExporter exporter = new JRHtmlExporter();
// 组装图片请求URI,增加time参数,防止同一个报表的图片缓存。
String imageURI = request.getContextPath() + “/service/reportImages?time=” + System.currentTimeMillis() + “&image=” ;

exporter.setParameter(JRHtmlExporterParameter.IMAGES_URI, imageURI);
request.getSession().setAttribute(ImageServlet.DEFAULT_JASPER_PRINT_SESSION_ATTRIBUTE, print);
exporter.setParameter(JRHtmlExporterParameter.IS_USING_IMAGES_TO_ALIGN, Boolean.TRUE);
exporter.setParameter(JRExporterParameter. JASPER_PRINT, print);

exporter.setParameter(JRExporterParameter. OUTPUT_WRITER, response.getWriter());
exporter.setParameter(JRHtmlExporterParameter.FLUSH_OUTPUT, Boolean.TRUE);

exporter.exportReport();

参考:Chart caching issues with JasperReports web output? A solution!

Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”.


今天碰到了一个很头疼的问题,自定义的一个XML文档,在eclipse中做测试的时候能够正常解析,但是一旦部署到服务器上就会出现下面的错误:

[Fatal Error] :1:1476: Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”.
net.sf.json.JSONException: nu.xom.ParsingException: Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”. at line 1, column 1476
at net.sf.json.xml.XMLSerializer.read(XMLSerializer.java:331)
at net.sf.json.xml.XMLSerializer.FromStream(XMLSerializer.java:391)
at net.sf.json.xml.XMLSerializer.FromFile(XMLSerializer.java:355)
……
Caused by: nu.xom.ParsingException: Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”. at line 1, column 1476
at nu.xom.Builder.build(Unknown Source)
at nu.xom.Builder.build(Unknown Source)
at net.sf.json.xml.XMLSerializer.read(XMLSerializer.java:309)
… 62 more
Caused by: org.xml.sax.SAXParsingException: Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”.

从错误提示来看,是有标签没有结束,清空文件内容,一点点的添加,逐步排查,发现所有的标签都正常结束了阿。按照异常提示,一层层阅读源代码,从json-lib.jar到xom.jar,最后恍然大悟,应该直接拿

org.xml.sax.SAXParsingException: Element type “nodeName” must be followed by either attribute specifications, “>” or “/>”.

作为搜索条件,果不其然,使用这个关键字搜索的结果比net.sf.json.JSONException和nu.xom.ParsingException的结果多多了。

最后在关于dom4j解析编码的问题,org.xml.sax.SAXParseException: Invalid byte 1 of 1-byte UTF-8 sequence找到了关联答案,在二楼有人说

UTF-8编码中中文解析有问题
将编码格式改成“GB2312”后就可以正常解析了。<?xml version=”1.0″ encoding=”GB2312″?>

马上联想,这么奇怪的问题是不是也是编码问题导致的呢?最后把encoding从UTF-8修改为GB18030,问题解决。

疑问:为什么在eclipse中测试的时候没有出现问题,部署到tomcat或者weblogic都会出现这样的问题呢?

问题的根本原因找到了: 继续阅读