【整理】Java 8新特性总结

2021-06-18 14:19

阅读:490

参数名称

为了在运行时获得Java程序中方法的参数名称,老一辈的Java程序员必须使用不同方法,例如Paranamer liberary。Java 8终于将这个特性规范化,在语言层面(使用反射API和Parameter.getName()方法)和字节码层面(使用新的javac编译器以及-parameters参数)提供支持。

package com.javacodegeeks.java8.parameter.names;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}

在Java 8中这个特性是默认关闭的,因此如果不带-parameters参数编译上述代码并运行,则会输出如下结果:

Parameter: arg0

如果带-parameters参数,则会输出如下结果(正确的结果):

Parameter: args

如果你使用Maven进行项目管理,则可以在maven-compiler-plugin编译器的配置项中配置-parameters参数:

org.apache.maven.plugins
    maven-compiler-plugin
    3.1-parameters1.81.8

 

Java 类库的新特性

Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。

Optional类

到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。

  • Optional 类的引入很好的解决空指针异常。
  • Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
  • Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

更多详情请参考官方文档。

我们下面用两个小例子来演示如何使用Optional类:一个允许为空值,一个不允许为空值。

Optional fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );       
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );

-----
输出结果:
  Full Name is set? false
  Full Name: [none]
  Hey Stranger!

如果Optional类的实例为非空值的话,isPresent()返回true,否从返回false。为了防止Optional为空值,orElseGet()方法通过回调函数来产生一个默认值。map()函数对当前Optional的值进行转化,然后返回一个新的Optional实例。orElse()方法和orElseGet()方法类似,但是orElse接受一个默认值而不是一个回调函数。

让我们来看看另一个例子:Optional firstName = Optional.of( "Tom" );

 

System.out.println( "First Name is set? " + firstName.isPresent() );       
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();

------
输出结果:
 First Name is set? true
 First Name: Tom
  Hey Tom!

更多详情请参考官方文档

Stream类

最新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理,这点后面我们会看到)。让我们以一个简单的Task类为例进行介绍:

public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };
     
    private static final class Task {
        private final Status status;
        private final Integer points;
 
        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }
         
        public Integer getPoints() {
            return points;
        }
         
        public Status getStatus() {
            return status;
        }
         
        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}

Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子:

final Collection tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 )
);

我们下面要讨论的第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
         
System.out.println( "Total points: " + totalPointsOfOpenTasks );

-------
输出结果:
  Total points: 18
 

这里有几个注意事项。第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。

在继续讲解下面的例子之前,关于stream有一些需要注意的地方(详情在这里).stream操作被分成了中间操作与最终操作这两种。

中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream
中符合给定谓词的所有元素。

像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。

stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
     
System.out.println( "Total points (all tasks): " + totalPoints );

---------
输出结果:
  Total points (all tasks): 26.0

这个例子和第一个例子很相似,但这个例子的不同之处在于这个程序是并行运行的,其次使用reduce方法来算最终的结果。

经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子:

// Group tasks by their status
final Map > map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

--------
输出结果:
  {CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
 

让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。

// Calculate the weight of each tasks (as percent of total points)
final Collection result = tasks
    .stream()                                        // Stream
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream
    .collect( Collectors.toList() );                 // List
         
System.out.println( result );

--------
输出结果;
  [19%, 50%, 30%]
 

最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。

final Path path = new File( filename ).toPath();
try( Stream lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。

Stream API、Lambda表达式与方法引用在接口默认方法与静态方法的配合下是Java 8对现代软件开发范式的回应。更多详情请参考官方文档。

Date/Time API (JSR 310)

Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是Java程序员最痛苦的地方之一,标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂),

这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。在设计新版API时,十分注重与旧版API的兼容性:不允许有任何的改变(从java.util.Calendar中得到的深刻教训)。如果需要修改,会返回这个类的一个新实例。

让我们用例子来看一下新版API主要类的使用方法。第一个是Clock类,它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()。

// Get the system clock as UTC offset
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );

-------
输出结果:
  2018-09-25T10:06:53.963Z
  1537870014196

 

我们需要关注的其他类是LocaleDate与LocalTime。LocaleDate只持有ISO-8601格式且无时区信息的日期部分。相应的,LocaleTime只持有ISO-8601格式且无时区信息的时间部分。LocaleDate与LocalTime都可以从Clock中得到。

// Get the local date and local time
final Clock clock = Clock.systemUTC();

final LocalDate date = LocalDate.now();final LocalDate dateFromClock = LocalDate.now( clock );
         
System.out.println( date );
System.out.println( dateFromClock );
         
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
         
System.out.println( time );
System.out.println( timeFromClock );

---------
输出结果:
   2018-09-25
    2018-09-25
    18:08:32.543
    10:08:32.543

LocaleDateTime把LocaleDate与LocaleTime的功能合并起来,它持有的是ISO-8601格式无时区信息的日期与时间。下面是一个快速入门的例子。

// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
         
System.out.println( datetime );
System.out.println( datetimeFromClock );

--------
输出结果:
    2018-09-25T18:12:09.071
    2018-09-25T10:12:09.071
 

如果你需要特定时区的日期/时间,那么ZonedDateTime是你的选择。它持有ISO-8601格式具具有时区信息的日期与时间。下面是一些不同时区的例子:

 
// Get the zoned date/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
         
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );

--------
输出结果:
  
    2018-09-25T18:14:15.541+08:00[Asia/Shanghai]
    2018-09-25T10:14:15.541Z
    2018-09-25T03:14:15.545-07:00[America/Los_Angeles]

最后,让我们看一下Duration类:在秒与纳秒级别上的一段时间。Duration使计算两个日期间的不同变的十分简单。下面让我们看一个这方面的例子。

final LocalDateTime from = LocalDateTime.of( 2017, Month.September, 25, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2018, Month.September, 25, 23, 59, 59 );

final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );

-----------
输出结果:
    Duration in days: 365
    Duration in hours: 8783 

上面的例子计算了两个日期2017年9月25号与2018年9月25号之间的过程。

 

对Java 8在日期/时间API的改进整体印象是非常非常好的。一部分原因是因为它建立在“久战杀场”的Joda-Time基础上,另一方面是因为用来大量的时间来设计它,并且这次程序员的声音得到了认可。更多详情请参考官方文档。

JavaScript引擎Nashorn

Nashorn,一个新的JavaScript引擎随着Java 8一起公诸于世,它允许在JVM上开发运行某些JavaScript应用。Nashorn就是javax.script.ScriptEngine的另一种实现,并且它们俩遵循相同的规则,允许Java与JavaScript相互调用。下面看一个例子:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
         
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );

-------
输出结果:
  jdk.nashorn.api.scripting.NashornScriptEngine
  Result: 2

Base64

在Java 8中,Base64编码已经成为Java类库的标准。它的使用十分简单,下面让我们看一个例子:

public class Base64s {
    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";

        final String encoded = Base64
                .getEncoder()
                .encodeToString(text.getBytes(StandardCharsets.UTF_8));
        System.out.println(encoded);

        final String decoded = new String(
                Base64.getDecoder().decode(encoded),
                StandardCharsets.UTF_8);
        System.out.println(decoded);
    }
}

-------------
输出结果:
    QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
   Base64 finally in Java 8!
    
 

Base64类同时还提供了对URL、MIME友好的编码器与解码器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

并行(parallel)数组

Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。

package com.javacodegeeks.java8.parallel.arrays;
 
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
 
public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 5000 ];       
         
        Arrays.parallelSetAll( arrayOfLong,
            index -> ThreadLocalRandom.current().nextInt( 500000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();
         
        Arrays.parallelSort( arrayOfLong );    
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}

-----------------
输出结果:
    43246 429105 479150 498970 478123 404198 273038 114468 488657 438767 
   111 347 552 581 731 852 1118 1252 1545 1735 

上面的代码片段使用了parallelSetAll()方法来对一个有5000个元素的数组进行随机赋值。然后,调用parallelSort方法。这个程序首先打印出前10个元素的值,之后对整个数组排序。这个程序在控制台上的输出如下(请注意数组元素是随机生产的):

并发(Concurrency)

在新增Stream机制与lambda的基础之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法来支持聚集操作。同时也在java.util.concurrent.ForkJoinPool类中加入了一些新方法来支持共有资源池(common pool)。

新增的java.util.concurrent.locks.StampedLock类提供一直基于容量的锁,这种锁有三个模型来控制读写操作(它被认为是不太有名的java.util.concurrent.locks.ReadWriteLock类的替代者)。

在java.util.concurrent.atomic包中还增加了下面这些类:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

更多详情请参考官方文档。

 

新的Java工具

Java 8也带来了一些新的命令行工具。在这节里我们将会介绍它们中最有趣的部分。

Nashorn引擎: jjs

jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。例如,我们创建一个具有如下内容的func.js文件:

function f() {
    return 0 ;
};
 
print( f() + 1 );

------
命令行中执行:jjs func.js
------
输出结果:1

jjs补充

Java 中调用 JavaScript

使用 ScriptEngineManager, JavaScript 代码可以在 Java 中执行,实例如下:

import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
 
public class Java8Tester {
   public static void main(String args[]){
   
      ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
      ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
        
      String name = "JackpotHan";
      Integer result = null;
      
      try {
         nashorn.eval("print(‘" + name + "‘)");
         result = (Integer) nashorn.eval("10 + 2");
         
      }catch(ScriptException e){
         System.out.println("执行脚本错误: "+ e.getMessage());
      }
      
      System.out.println(result.toString());
   }
}

-----------
输出结果:
    $ javac Java8Tester.java 
    $ java Java8Tester
    JackpotHan
    12


JavaScript 中调用 Java

以下实例演示了如何在 JavaScript 中引用 Java 类,创建test.js文件:

var BigDecimal = Java.type(‘java.math.BigDecimal‘);

function calculate(amount, percentage) {

   var result = new BigDecimal(amount).multiply(
   new BigDecimal(percentage)).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN);
   
   return result.toPlainString();
}

var result = calculate(1537933632887,13.14);
print(result);

---------
控制台执行:
$ jjs test.js
输出结果:202084479361.35

更多详情请参考官方文档

类依赖分析器jdeps

jdeps是一个很有用的命令行工具。它可以显示Java类的包级别或类级别的依赖。它接受一个.class文件,一个目录,或者一个jar文件作为输入。jdeps默认把结果输出到系统输出(控制台)上。

下面我们查看现阶段较流行的Spring框架类库的依赖报告,为了简化这个例子,我们只分析一个jar文件:org.springframework.core-3.0.5.RELEASE.jar

jdeps org.springframework.core-3.0.5.RELEASE.jar

这个命令输出的内容很多,所以这里我们只选取一小部分。依赖信息按照包名进行分组。如果依赖不在classpath中,那么就会显示not found

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io                                           
      -> java.lang                                         
      -> java.lang.annotation                              
      -> java.lang.ref                                     
      -> java.lang.reflect                                 
      -> java.util                                         
      -> java.util.concurrent                              
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang                                         
      -> java.lang.annotation                              
      -> java.lang.reflect                                 
      -> java.util

更多详情请参考官方文档

 

Java虚拟机(JVM)的新特性

元空间(MetaSpace)

PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。JVM选项-XX:PermSize-XX:MaxPermSize分别被-XX:MetaSpaceSize-XX:MaxMetaspaceSize所代替。

JDK8 HotSpot JVM 将移除永久区,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace),这与Oracle JRockit 和IBM JVM’s很相似,如下图所示

技术分享图片

这意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用……但请等等,这么说还为时过早。在默认情况下,这些改变是透明的,接下来我们的展示将使你知道仍然要关注类元数据内存的占用。请一定要牢记,这个新特性也不能神奇地消除类和类加载器导致的内存泄漏。

java8中metaspace总结如下:

  PermGen 空间的状况

    这部分内存空间将全部移除。

    JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。

  Metaspace 内存分配模型

    大部分类元数据都在本地内存中分配。

    用于描述类元数据的“klasses”已经被移除。

  Metaspace 容量

    默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。

    新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。

  Metaspace 垃圾回收

    对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。

    适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

  Java 堆内存的影响

    一些杂项数据已经移到Java堆空间中。升级到JDK8之后,会发现Java堆 空间有所增长。

  Metaspace 监控

    元空间的使用情况可以从HotSpot1.8的详细GC日志输出中得到。

    Jstat 和 JVisualVM两个工具,在使用b75版本进行测试时,已经更新了,但是还是能看到老的PermGen空间的出现。

前面已经从理论上充分说明,下面让我们通过“泄漏”程序进行新内存空间的观察……

PermGen vs. Metaspace 运行时比较

为了更好地理解Metaspace内存空间的运行时行为,

将进行以下几种场景的测试:

  1. 使用JDK1.7运行Java程序,监控并耗尽默认设定的85MB大小的PermGen内存空间。
  2. 使用JDK1.8运行Java程序,监控新Metaspace内存空间的动态增长和垃圾回收过程。
  3. 使用JDK1.8运行Java程序,模拟耗尽通过“MaxMetaspaceSize”参数设定的128MB大小的Metaspace内存空间。

首先建立了一个模拟PermGen OOM的代码

public class ClassA {
     public void method(String name) {
      // do nothing
     }
}

上面是一个简单的ClassA,把他编译成class字节码放到D:/classes下面,测试代码中用URLClassLoader来加载此类型上面类编译成class

/**
 * 模拟PermGen OOM
 * @author benhail
 */
public class OOMTest {
    public static void main(String[] args) {
        try {
            //准备url
            URL url = new File("D:/classes").toURI().toURL();
            URL[] urls = {url};
            //获取有关类型加载的JMX接口
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
            //用于缓存类加载器
            List classLoaders = new ArrayList();
            while (true) {
                //加载类型并缓存类加载器实例
                ClassLoader classLoader = new URLClassLoader(urls);
                classLoaders.add(classLoader);
                classLoader.loadClass("ClassA");
                //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
                System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
                System.out.println("active: " + loadingBean.getLoadedClassCount());
                System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虚拟机器参数设置如下:-verbose -verbose:gc

设置-verbose参数是为了获取类型加载和卸载的信息

设置-verbose:gc是为了获取垃圾收集的相关信息

JDK 1.7 @64-bit – PermGen 耗尽测试

Java1.7的PermGen默认空间为85 MB(或者可以通过-XX:MaxPermSize=XXXm指定)

技术分享图片

可以从上面的JVisualVM的截图看出:当加载超过6万个类之后,PermGen被耗尽。我们也能通过程序和GC的输出观察耗尽的过程。

程序输出(摘取了部分)

......
[Loaded ClassA from file:/D:/classes/]
total: 64887
active: 64887
unloaded: 0
[GC 245041K->213978K(536768K), 0.0597188 secs]
[Full GC 213978K->211425K(644992K), 0.6456638 secs]
[GC 211425K->211425K(656448K), 0.0086696 secs]
[Full GC 211425K->211411K(731008K), 0.6924754 secs]
[GC 211411K->211411K(726528K), 0.0088992 secs]
...............
java.lang.OutOfMemoryError: PermGen space

JDK 1.8 @64-bit – Metaspace大小动态调整测试

Java的Metaspace空间:不受限制 (默认)

技术分享图片

从上面的截图可以看到,JVM Metaspace进行了动态扩展,本地内存的使用由20MB增长到646MB,以满足程序中不断增长的类数据内存占用需求。我们也能观察到JVM的垃圾回收事件—试图销毁僵死的类或类加载器对象。但是,由于我们程序的泄漏,JVM别无选择只能动态扩展Metaspace内存空间。程序加载超过10万个类,而没有出现OOM事件。

JDK 1.8 @64-bit – Metaspace 受限测试

Java的Metaspace空间:128MB(-XX:MaxMetaspaceSize=128m)

技术分享图片

可以从上面的JVisualVM的截图看出:当加载超过2万个类之后,Metaspace被耗尽;与JDK1.7运行时非常相似。我们也能通过程序和GC的输出观察耗尽的过程。另一个有趣的现象是,保留的原生内存占用量是设定的最大大小两倍之多。这可能表明,如果可能的话,可微调元空间容量大小策略,来避免本地内存的浪费。

从Java程序的输出中看到如下异常。

[Loaded ClassA from file:/D:/classes/]
total: 21393
active: 21393
unloaded: 0
[GC (Metadata GC Threshold) 64306K->57010K(111616K), 0.0145502 secs]
[Full GC (Metadata GC Threshold) 57010K->56810K(122368K), 0.1068084 secs]
java.lang.OutOfMemoryError: Metaspace

在设置了MaxMetaspaceSize的情况下,该空间的内存仍然会耗尽,进而引发“java.lang.OutOfMemoryError: Metadata space”错误。因为类加载器的泄漏仍然存在,而通常Java又不希望无限制地消耗本机内存,因此设置一个类似于MaxPermSize的限制看起来也是合理的。

小结:

  • 之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
  • 提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;
  • 隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。

参考资料

  https://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html


评论


亲,登录后才可以留言!