【问题标题】:Least GC consuming way to convert double to String without E and 10 digit precision在没有 E 和 10 位精度的情况下将双精度转换为字符串的最少 GC 消耗方式
【发布时间】:2026-01-23 16:40:06
【问题描述】:

我的 spark 工作是创建很多小对象并遇到垃圾收集问题。我尝试了多种方法将 double 转换为具有 10 位精度的字符串,而无需科学记数法。

第一道

String.format("%.10f", denomValue)

这个案例的错误:

java.lang.OutOfMemoryError: GC overhead limit exceeded
at sun.misc.FormattedFloatingDecimal.create(FormattedFloatingDecimal.java:254)
at sun.misc.FormattedFloatingDecimal.fillDecimal(FormattedFloatingDecimal.java:267)
at sun.misc.FormattedFloatingDecimal.<init>(FormattedFloatingDecimal.java:76)
at sun.misc.FormattedFloatingDecimal.valueOf(FormattedFloatingDecimal.java:38)
at java.util.Formatter$FormatSpecifier.print(Formatter.java:3298)
at java.util.Formatter$FormatSpecifier.print(Formatter.java:3238)
at java.util.Formatter$FormatSpecifier.printFloat(Formatter.java:2802)
at java.util.Formatter$FormatSpecifier.print(Formatter.java:2753)
at java.util.Formatter.format(Formatter.java:2520)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2925)

第二种方式

new BigDecimal(d).setScale(10,BigDecimal.ROUND_UP).toPlainString()

这种情况下的错误:-

java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.math.MutableBigInteger.divideMagnitude(MutableBigInteger.java:1489)
at java.math.MutableBigInteger.divideKnuth(MutableBigInteger.java:1227)
at java.math.MutableBigInteger.divide(MutableBigInteger.java:1153)
at java.math.MutableBigInteger.divide(MutableBigInteger.java:1147)
at java.math.BigDecimal.divideAndRound(BigDecimal.java:4326)
at java.math.BigDecimal.setScale(BigDecimal.java:2470)

【问题讨论】:

  • 我不会想象转换本身会耗尽内存 - 更有可能是保留一堆源值和转换后的值

标签: java apache-spark garbage-collection


【解决方案1】:

引用“Understand the OutOfMemoryError Exception”:

线程 thread_name 中的异常:java.lang.OutOfMemoryError:超出 GC 开销限制

原因: 详细信息“GC overhead limit exceeded”表示垃圾收集器一直在运行,Java 程序进展缓慢。垃圾回收后,如果 Java 进程花费超过大约 98% 的时间进行垃圾回收,并且如果它回收的堆少于 2%,并且到目前为止一直在做最后 5 个(编译时间常数)连续垃圾集合,然后抛出 java.lang.OutOfMemoryError。通常会抛出此异常,因为实时数据量几乎无法放入 Java 堆中,几乎没有可供新分配使用的可用空间。

操作:增加堆大小。超过 GC Overhead limit 的 java.lang.OutOfMemoryError 异常可以通过命令行标志 -XX:-UseGCOverheadLimit 关闭。

如果您的代码确实将 98% 的时间用于垃圾收集,那么您绝对应该增加堆大小,因为这意味着您的内存的 98% 用于(半)永久数据,而小于 2 % 可免费用于临时分配。

您可能有严重的内存泄漏问题。

但正如它所说,如果需要,您可以禁用该特定错误。

至于您的代码,您应该坚持使用选项 1。选项 2 会产生比选项 1 更多的垃圾。

【讨论】:

    【解决方案2】:

    我对格式化十进制数的意见很少进行基准测试

    • java.util.Formatter
    • java.lang.String.format()
    • java.text.DecimalFormat
    • org.apache.xmlgraphics.util.DoubleFormatUtil.formatDouble()

    在所有情况下,我都测量了格式化一个十进制数所浪费的内存。 以下是 32 JVM 的结果。

    java.util.Formatter
    Format mem usage: 1120
    Format + string mem usage: 1184
    
    java.text.DecimalFormat
    Format + string mem usage: 288
    
    org.apache.xmlgraphics.util.DoubleFormatUtil.formatDouble()
    Format mem usage: 0
    Format + string mem usage: 64
    
    java.lang.String.format()
    Format + string mem usage: 1448
    

    代码如下

    @Test
    public void testDecimalFormat() {
        double pi = Math.PI;
        System.out.println("\njava.text.DecimalFormat");
    
        DecimalFormat fmt = new DecimalFormat(".##########");
        fmt.setRoundingMode(RoundingMode.UP);
    
        // warm up
        fmt.format(pi);
    
        long mme = -(getCurrentThreadAllocatedBytes() - getCurrentThreadAllocatedBytes());
        long memu1 = getCurrentThreadAllocatedBytes();
    
        String val = fmt.format(pi);
    
        long memu2 = getCurrentThreadAllocatedBytes();
    
        System.out.println(val);
        System.out.println("Format + string mem usage: " + (memu2 - memu1 - mme));
    
    }
    
    @Test
    public void testFormatter() {
        System.out.println("\njava.util.Formatter");
        double pi = Math.PI;
    
        StringBuilder sb = new StringBuilder(64);
        Formatter ftm = new Formatter(sb);
    
        // warm up
        ftm.format("%.10f", pi);
        sb.setLength(0);
    
        // memory measurment error
        long mme = -(getCurrentThreadAllocatedBytes() - getCurrentThreadAllocatedBytes());
    
        long memu0 = getCurrentThreadAllocatedBytes();
    
        ftm.format("%.10f", pi);
    
        long memu1 = getCurrentThreadAllocatedBytes();
    
        String val = sb.toString();
    
        long memu2 = getCurrentThreadAllocatedBytes();
    
        System.out.println(val);
        System.out.println("Format mem usage: " + (memu1 - memu0 - mme));
        System.out.println("Format + string mem usage: " + (memu2 - memu0 - 2 * mme));
    
    }
    
    @Test
    public void testStringFormat() {
        System.out.println("\njava.lang.String.format()");
    
        double pi = Math.PI;
    
        // warmup
        String.format("%.10f", pi);
    
        // memory measurment error
        long mme = -(getCurrentThreadAllocatedBytes() - getCurrentThreadAllocatedBytes());
    
        long memu1 = getCurrentThreadAllocatedBytes();
    
        String val = String.format("%.10f", pi);
    
        long memu2 = getCurrentThreadAllocatedBytes();
    
        System.out.println(val);
        System.out.println("Format + string mem usage: " + (memu2 - memu1 - mme));
    
    }
    
    @Test
    public void testDoubleFormatUtil() {
        System.out.println("\norg.apache.xmlgraphics.util.DoubleFormatUtil.formatDouble()");
    
        double pi = Math.PI;
    
        StringBuffer sb = new StringBuffer(64);
    
        // warmup
        DoubleFormatUtil.formatDouble(pi, 10, 10, sb);
        sb.setLength(0);
    
        // memory measurment error
        long mme = -(getCurrentThreadAllocatedBytes() - getCurrentThreadAllocatedBytes());
    
        long memu0 = getCurrentThreadAllocatedBytes();
    
        DoubleFormatUtil.formatDouble(pi, 10, 10, sb); 
    
        long memu1 = getCurrentThreadAllocatedBytes();
    
        String val = sb.toString();
    
        long memu2 = getCurrentThreadAllocatedBytes();
    
        System.out.println(val);
        System.out.println("Format mem usage: " + (memu1 - memu0 - mme));
        System.out.println("Format + string mem usage: " + (memu2 - memu0 - 2 * mme));
    
    }
    
    
    private long getCurrentThreadAllocatedBytes() {
        return ((com.sun.management.ThreadMXBean)ManagementFactory.getThreadMXBean()).getThreadAllocatedBytes(Thread.currentThread().getId());
    }
    

    【讨论】:

      最近更新 更多