java基础-JVM日志、参数、内存结构、垃圾回收器

news2024/9/8 23:40:04

一、基础基础

1.1 数据类型

Java的数据类型分为原始数据类型和引用数据类型。

  • 原始数据类型又分为数字型和布尔型。
    • 数字型又有byte、short、int、long、char、float、double。注意,在这里char被定义为整数型,并且在规范中明确定义:byte、short、int、long分别是8位、16位、32位、64位有符号整数,而char为16位无符号整数,表示UTF-16的字符。
    • 布尔型只有两种取值:true和false。
  • 引用数据类型分为3种:类或接口、泛型类型和数组类型

1.1.2 整数在Java虚拟机中的表示

整数有byte、short、int、long四种,分别表示8位、16位、32位、64位有符号整数。

  • 所谓原码,就是符号位加上数字的二进制表示。以int为例,第1位表示符号位(正数或者负数)

10的原码为:00000000 00000000 00000000 00001010
-10的原码为:10000000 00000000 00000000 00001010

  • 对于原码来说,绝对值相同的正数和负数只有符号位不同。反码就是在原码的基础上,符号位不变,其余位取反

以-10为。例,其反码为:11111111111111111111111111110101

负数的补码就是反码加1,整数的补码就是原码本身。

在Java中,可以使用位运算查看整数中每一位的实际值,方法如下
在这里插入图片描述

(2)使用补码可以简化整数的加减法计算,将减法计算视为加法计算,实现减法和加法的完全统一,实现正数和负数加法的统一。现使用8位(byte)整数说明这个问题;
计算-6+5的过程如下。
-6的补码:11111010
5的补码:00000101
直接相加得:11111111
通过计算可知,11111111表示-1。
计算4+6的过程如下。
4的补码:00000100
6的补码:00000110
直接相加得:00001010
通过计算可知,00001010表示10(十进制)。
可以看到,使用补码表示时,只需要将补码简单地相加,即可得
到算术加法的正确结果,而无须区别正数或者负数。

1.2 浮点数在Java虚拟机中的表示

在Java虚拟机中,浮点数有float和double两种,分别是32位和64位浮点数。浮点数在虚拟机中的表示比整数略显复杂。在IEEE754的定义中一个浮点数由3部分组成,分别是符号位、指数位和尾数位。以32位float类型为例,符号位占1位,表示正负数,指数位占8位,尾数位占剩余的23位

在这里插入图片描述

二、Java虚拟机的基本结构

2.1 Java虚拟机的架构

在这里插入图片描述
类加载子系统负责从文件系统或者网络中加载Class信息,

  • 方法区 加载的类信息存放于一块称为方法区的内存空间中。除了类的信息,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

  • Java堆中:Java堆在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的,这是一块与Java应用密切相关的内存区域。

  • Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区域。通常,访问直接内存的速度会优于Java堆。因此,出于性能考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在Java堆外,因此,它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统的最大内存。

  • 垃圾回收系统是Java虚拟机的重要组成部分,垃圾回收器可以对方法区、Java堆和直接内存进行回收。其中,Java堆是垃圾收集器的工作重点。和C/C++不同,Java中所有对象空间释放都是隐式的。也就是说,Java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括Java堆、方法区和直接内存中的全自动化管理。有关垃圾回收系统的更多信息

  • Java栈:Java虚拟机线程都有一个私有的Java栈。一个线程的Java栈在线程创建的时候被创建。Java栈中保存着帧信息(参阅2.4节),Java栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关

  • 本地方法栈:和Java栈非常类似,最大的不同在于Java栈用于Java方法的调用,而本地方法栈则用于本地方法的调用。作为对Java虚拟机的重要扩展,Java虚拟机允许Java直接调用本地方法(通常使用C语言编写)

  • PC 寄存器:PC(Program Counter)寄存器也是每个线程私有的空间,Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined。

  • 执行引擎:是Java虚拟机的最核心组件之一,它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。执行引擎的进一步描述可以参阅第11章。

2.2 学会设置Java虚拟机的参数

Java进程的命令行使用方法如下
在这里插入图片描述
其中,-options表示Java虚拟机的启动参数,class为带有main()函数的Java类,args表示传递给主函数main()的参数。如果需要设定特定的Java虚拟机参数,在options处指定即可。目前,Hotspot虚拟机支持大量的虚拟机参数;

在这里插入图片描述
上述代码打印了传递给main()函数的参数,
在这里插入图片描述
如果读者使用Eclipse等开发工具运行程序,在运行对话框的参数选项卡上,也可以设置这两个参数,如图2.2所示,显示了“程序参数”和“虚拟机参数”两个文本框,将所需的参数填入即可。
在这里插入图片描述

2.3 辨清Java堆

Java堆是和Java应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆中,并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显式地释放。

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构。最为常见的一种构成是将整个Java堆分为新生代和老年代。其中,新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代
有可能分为eden、s0、s1,其中s0和s1也被称为from和to区域,它们是两块大小相等、可以互换角色的内存空间
。详细信息可以参阅第4章。
在这里插入图片描述

在绝大多数情况下,对象首先在eden区分配,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,之后,每经过一次新生代回收,对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代

2.3.1Java堆、方法区和Java栈之间的关系

在这里插入图片描述
上述代码声明了一个SimpleHeap类,并在main()函数中创建了两个SimpleHeap实例,此时,各对象和局部变量的存放如图2.4所示。SimpleHeap实例本身在堆中分配,描述SimpleHeap类的信息存放在方法区,main()函数中s1和s2局部变量存放在Java栈中,并指向堆中的两个实例。
在这里插入图片描述

2.4 函数如何调用:出入Java栈

线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。Java栈与数据结构中的栈有着类似的含义,它是一块先进后出的数据结构,只支持出栈和入栈两种操作。在Java栈中保存的主要内容为栈帧。每一次函数调用,都会有一个对应的栈帧被压入Java栈,每一次函数调用结束,都会有一个栈帧被弹出Java栈;当函数返回时,栈帧从Java栈中被弹出。Java方法有两种返回函
数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几部分。
在这里插入图片描述
由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间。因此,如果栈空间不足,那么函数调用自然无法继续进行。当请求的栈深度大于最大可用栈深度时,系统就会抛出StackOverflowError栈溢出错误。

2.4.1 Java虚拟机提供了参数-Xss优化栈

**Java虚拟机提供了参数-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。**函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持的嵌套调用次数就越多。

2.4.2 局部变量表

局部变量表是栈帧的重要组成部分之一。它用于保存函数的参数及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,函数栈帧销毁,局部变量表也会随之销毁。由于局部变量表在栈帧之中,因此**,如果函数的参数和局部变量较多,会使局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少**。
使用jclasslib工具可以进一步查看函数的局部变量信息

2.4.3 帧数据区

除了局部变量表和操作数栈,Java栈帧还需要一些数据来支持常量池解析、正常方法返回和异常处理等。大部分Java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池

2.4.4 栈上分配

栈上分配是Java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处
是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。

2.4.4.1 -XX:+DoEscapeAnalysis启用逃逸分析

这里使用参数-server执行程序,因为在Server模式下,才可以启用逃逸分析。参数-XX:+DoEscapeAnalysis启用逃逸分析,-Xmx10m指定了堆空间最大为10MB。显然,如果对象在堆上分配,必然会引起大量的GC。如果GC真的发生了,参数-XX:+PrintGC将打印GC日
志。参数-XX:+EliminateAllocations开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。参数-XX:-
UseTLAB关闭了TLAB。
在这里插入图片描述

2.5 类去哪儿了:识别方法区

和Java堆一样,方法区是一块所有线程共享的内存区域。它用于保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。

2.5.1 JDK 1.6、JDK 1.7中 方法区可以理解为永久区以及参数设置

2.5.1.1 方法区可以理解为永久区 数据存放

在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm)。在JDK 1.8、JDK1.9、JDK1.10中,永久区已经被彻底移除。取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。图2.12显示了JDK 1.8中的元数据区,JDK 1.9、JDK 1.10与此相同,不再赘述。

永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize为64MB。一个大的永久区可以保存更多的类信息。如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,这时就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。

注意: 这里使用CGLIB动态产生类,不仅仅是对象实例。由于类的信息(字段、方法、字节码等)保存在方法区,因此,这个操作会占用方法区的空间

2.5.1.2 方法区可以理解为永久区以及参数设置 -XX:PermSize和-XX:MaxPermSize

永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize为64MB。一个大的永久区可以保存更多的类信息。如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类**,这时就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。
在这里插入图片描述

这里指定了初始永久区为5MB,最大永久区为5MB,即当5MB空间耗尽时,系统将抛出内存溢出异常
在这里插入图片描述

在JDK 1.8、JDK1.9、JDK1.10中,永久区已经被彻底移除。取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。图2.12显示了JDK 1.8中的元数据区,JDK 1.9、JDK 1.10与此相同,不再赘述。**

在这里插入图片描述
在这里插入图片描述
如果元数据区发生溢出,虚拟机一样会抛出异常,如下所示

在这里插入图片描述

三 、常用Java虚拟机参数

3.1 掌握跟踪调试参数

3.1.1 JDK9、JDK10 GC参数 -XX:+PrintGC

最简单的一个GC参数是-XX:+PrintGC(在JDK9、JDK10中建议使用-Xlog:gc),使用这个参数启动Java虚拟机后,只要遇到GC,就会打印日志,如下所示
在这里插入图片描述
该日志显示,一共进行了4次GC,每次GC占用一行,在GC前,堆空间使用量约为4MB,在GC后,堆空间使用量为377KB,当前可用的堆空间总和约为16MB(15936KB)。最后,显示的是本次GC所
花的时间。

3.1.2 JDK9、JDK10 GC日志 -Xlog:gc

JDK9、JDK10默认使用G1作为垃圾回收器,使用参数-Xlog:gc来打印GC日志,如下所示

在这里插入图片描述
该日志显示,一共进行了1次GC,在GC前,堆空间使用量为16MB,在GC后,堆空间使用量为7MB,当前可用的堆空间总和为34MB。最后,显示的是本次GC所花的时间,为23.511ms。\

3.1.3 JDK8 GC 日志详情 -XX:+PrintGCDetails

如果需要更加详细的信息,可以使用-XX:+PrintGCDetails参数。JDK8(JDK9、JDK10建议使用-Xlog:gc*,后面讲述)中的输出可能如下:

在这里插入图片描述
从这个输出中可以看到,系统经历了3次GC,

第1次仅为新生代GC,回收的效果是新生代从回收前的8MB左右降低到1MB。整个堆从22MB左右降低到17MB。
第2次(加粗部分)为Full GC,它同时回收了新生代、老年代和永久区。日志显示,新生代在这次GC中没有释放空间(严格来说,这是GC日志的一个小bug,事实上,在这次FullGC完成后,新生代被清空,由于GC日志输出时机的关系,各个版本JDK的日志多少有些不太精确的地方,读者需要留意),老年代从16MB降低到13MB。整个堆大小从26MB左右降低为13MB左右(这个大小完全与老年代
实际大小相等,因此也可以推断,新生代实际上已被清空)。永久区的大小没有变化。日志的最后显示了GC所花的时间,其中user表示用户态CPU耗时,sys表示系统CPU耗时,real表示GC实际经历的时间。

参数-XX:+PrintGCDetails还会使虚拟机在退出前打印堆的详细信息,详细信息描述了当前堆的各个区间的使用情况。如上输出所示,当前新生代(newgeneration)总大小为9792KB,已使用4586KB。紧跟其后的3个16进制数字表示新生代的下界、当前上界和上界。
在这里插入图片描述

使用上界减去下界就能得到当前堆空间的最大值,使用当前上界减去下界,就是当前虚拟机已经为程序分配的空间。如果当前上界等
于下界,说明当前的堆空间已经没有扩大的可能性。在本例中(0x00000000f98a00000x00000000f8e00000)/1024=10880KB。这块空间正好等于eden+from+to的总和。而可用的新生代9792KB为eden+from(to)的总和,对于两者出现差异的原因,读者可以参考本书第4章。

3.1.4 JDK9、JDK10使用参数-Xlog:gc*来打印更加详细的GC日志,

如下所示:
在这里插入图片描述
从这个输出中可以看到,堆的最大可用大小为32MB,系统经历了1次GC,为新生代GC,回收的效果是整个堆从14MB左右降低到了1MB。在JDK9、JDK10中,除了新生代、老年代,还新增了一个巨
型区域,即上述输出中的Humongous regions。另外,日志中有详细的时间信息,

第一列显示Java程序运行的时间,Pause Young (G1 Evacuation Pause) 14M->1M(32M) 7.028ms表示新生代垃圾回收花了7.028ms。Pre Evacuate Collection Set、Evacuate Collection Set、PostEvacuate Collection Set、Other代表G1垃圾回收标记—清除算法不同阶段所花费的时间。最后一行的时间信息跟JDK8相同,不再赘述。如果需要更全面的堆信息,还可以使用参数-XX:+PrintHeapAtGC(考虑到兼容性,从JDK9开始已经删除此参数,查看堆信息可以使用VisualVM,第6章将会讲述)。它会在每次GC前、后分别打印堆的信息,就如同-XX:+PrintGCDetails的最后输出一样。下面就是-XX:+PrintHeapAtGC的输出样式,限于篇幅,只给出部分输出

在这里插入图片描述
可以看到,在使用-XX:+PrintHeapAtGC后,在GC日志输出前、后都有详细的堆信息输出,分别表示GC回收前和GC回收后的堆信息,使用这个参数,可以很好地观察GC对堆空间的影响。
如果需要分析GC发生的时间,还可以使用-XX:+PrintGCTimeStamps(JDK9、JDK10中使用-Xlog:gc已经默认打印出时间,前文关于-Xlog:gc已经有讲述,这里不再赘述)参数,该参数会在每次GC时,额外输出GC发生的时间,该输出时间为虚拟机启动后的时间偏移量。如下代码表示在系统启动后0.08s、0.088s、0.094s发生了GC。

在这里插入图片描述
由于GC会引起应用程序停顿,因此还需要特别关注应用程序的执行时间和停顿时间。使用参数-XX:
+PrintGCApplicationConcurrentTime可以打印应用程序的执行时间,使用参数-XX:+PrintGCApplicationStoppedTime可以打印应用程序由于GC而产生的停顿时间,如下所示:如果想跟踪系统内的软引用、弱引用、虚引用和Finallize队列,可以打开-XX:+PrintReferenceGC(考虑到兼容性,从JDK9开始已经删除此参数,查看堆信息可以使用VisualVM,第6章将会讲述)开关,结果如下:在这里插入图片描述
默认情况下,GC的日志会在控制台中输出,这不便于后续分析和定位问题。所以,虚拟机允许将GC日志以文件的形式输出,可以使用参数-Xloggc指定。比如使用参数-Xloggc:log/gc.log(在JDK9、
JDK10中建议使用-Xlog:gc:log/gc.log)启动虚拟机,可以在当前目录的log文件夹下的gc.log文件中记录所有的GC日志。图3.1显示了由-Xloggc生成的gc.log文件,JDK9、JDK10生成的文件与JDK8相同,不再赘述
在这里插入图片描述

3.1.5 类加载/卸载的跟踪

Java程序的运行离不开类的加载,为了更好地理解程序如何执行,有时候需要知道系统加载了哪些类。一般情况下,系统加载的类存在于文件系统中,以jar的形式打包或者以class文件的形式存在,
可以直接通过文件系统查看。但是随着动态代理、AOP等技术的普遍使用,系统也极有可能在运行时动态生成某些类,这些类相对比较隐蔽,无法通过文件系统找到,此时虚拟机提供的类加载/卸载跟踪参数就显得格外有意义

3.1.5.1 参数-verbose:class跟踪类的加载/卸载

可以使用参数-verbose:class跟踪类的加载/卸载,也可以单独使用参数-XX:+TraceClassLoading(在JDK9、JDK10中建议使用-Xlog:class+load=info,跟JDK8中的参数-XX:+TraceClassLoading效果相同)跟踪类的加载,使用参数-XX:+TraceClassUnloading(在JDK9、JDK10中建议使用-Xlog:class+unload=info,跟JDK8中的参数-XX:+TraceClassLoading效果相同)跟踪类的卸载。这两类参数是等价的。

下面这段代码使用ASM动态生成名为Example的类,并将其反复加载到系统中(ASM的使用可以参阅9.3节)。
在这里插入图片描述
上述代码的第318行使用ASM生成名为Example的类,并将其保存在code数组中。第2027行使用ClassLoader将新生成的类反复加载到系统中,每次循环使用新的ClassLoader实例,并在循环结束前进行FullGC,释放上一次循环加载的类,因此,这一过程会涉及类的加载和卸载。使用参数-XX:+TraceClassUnloading和参数-XX:+TraceClassLoading执行上述代码,跟踪类的加载和卸载过程,部分输出如下在这里插入图片描述
从这份日志中可以看到,系统先加载了java.lang.Object类,作为所有类的父类。日志的后半部分显示,系统对Example类先后进行了10次加载和9次卸载(最后一次加载的类没有机会被卸载)。
注意:动态类的加载是非常隐蔽的,它们由代码逻辑控制,不出现在文件系统中,跟踪这些类需要使用-XX:+TraceClassLoading等参数。
Java虚拟机还允许研发人员在运行时打印、查看系统中类的分布情况,只要在系统启动时加上-XX:+PrintClassHistogram参数,然后在Java的控制台中按下Ctrl+Break组合键,控制台上就会显示当前的类信息柱状图,如下所示:

在这里插入图片描述
通过这个柱状图信息,可以看到当前系统中占用空间最大的对象,以及它的实例数量和所占用空间大小

3.1.6 查看系统参数

3.1.6.1参数-XX:+PrintVMOptions可以在程序运行时打印虚拟机接收到

的命令行显式参数。其输出如下:
在这里插入图片描述
这说明该虚拟机启动时,命令行明确指定了UseSerialGC、DisableExplicitGC两个参数。

3.1.6.2 参数-XX:+PrintCommandLineFlags可以打印传递给虚拟机的显式和隐式参数

参数-XX:+PrintCommandLineFlags可以打印传递给虚拟机的显式和隐式参数,隐式参数未必是通过命令行直接给出的,它可能是在虚拟机启动时自行设置的,使用-XX:+PrintCommandLineFlags后,
可能的输出如下:
在这里插入图片描述
在本例中,-XX:InitialHeapSize、-XX:MaxHeapSize和-XX:-UseLargePagesIndividualAllocation并未在命令行显式指定,是由虚拟机自行设置的。此外,另一个有用的参数是-XX:+PrintFlagsFinal,它会打印所有的系统参数的值。开启这个参数后,虚拟机可能会产生500多行输出,每一行为一个配置参数及其当前取值,读者如果对虚拟机的各参数感兴趣,可以打印出这些参数,逐个学习。

3.1.6.3另一个有用的参数是-XX:+PrintFlagsFinal,它会打印所有的系统参数的值

开启这个参数后,虚拟机可能会产生500多行输出,每一行为一个配置参数及其当前取值,读者如果对虚拟机的各参数感兴趣,可以打印出这些参数,逐个学习。

3.2 学习堆的配置参数

本节将主要介绍与堆有关的参数设置,这些参数可以说是Java虚拟机中最重要的,也是对程序性能有着重要影响的

图3.2显示了几个重要的堆分配参数的含义

在这里插入图片描述

3.2.1 最大堆和初始堆的设置 参数 -Xms -Xmx

当Java进程启动时,虚拟机就会分配一块初始堆空间,可以使用参数-Xms指定这块空间的大小。一般来说,虚拟机会尽可能维持在初始堆空间的范围内运行。但是如果初始堆空间耗尽,虚拟机将会对堆空间进行扩展,其扩展上限为最大堆空间,最大堆空间可以使用参数-Xmx指定

在这里插入图片描述

在这里插入图片描述
程序的输出如下:

在这里插入图片描述
在这里插入图片描述
可以看到,当前最大内存由-XX:MaxHeapSize=20971520指定,它正好是20×1024×1024=20971520字节。而打印的最大可用内存仅为20316160字节,比设定值略少。这是因为分配给堆的内存
空间和实际可用内存空间并非一个概念。由于垃圾回收的需要,虚拟机会对堆空间进行分区管理,不同的区采用不同的回收算法,一些算法会使用空间换时间的策略,因此会存在可用内存的损失,详细算法
可以参见4.2.3节,这里不展开讨论。最终的结果就是实际可用内存会浪费大小等于from/to的空间。因此,实际最大可用内存为-Xmx的值减去from的值。从堆的详细信息可以看到,from的大小为
0x33420000-0x33400000=0x20000=131072字节。但很不幸,读者应该会发现20971520-131072=20840448字节,与实际值20316160依然存在偏差。出现这个偏差是因为虚拟机内部并没有直接使用新生代from/to的大小,而是进一步对它们做了对齐操作。对于串行GC的情况,虚拟机使用以下方法估算from/to的大小,并进行对齐

在这里插入图片描述
提示: 在实际工作中,也可以直接将初始堆-Xms与最大堆-Xmx设置为相等。这样的好处是,可以减少程序运行时进行垃圾回收的次数,从而提高程序的性能。

3.2.2 新生代的配置 参数-Xmn 参数-XX:SurvivorRatio用来设置新生代中eden区和from/to区的比例

注意:
-XX:SurvivorRatio可以设置eden区与survivor的比例。-
XX:NewRatio可以设置老年代与新生代的比例。

参数-Xmn可以用于设置新生代的大小。设置一个较大的新生代会减小老年代的大小,这个参数对系统性能及GC行为有很大的影响。新生代的大小一般设置为整个堆空间的1/3到1/4。

参数-XX:SurvivorRatio用来设置新生代中eden区和from/to区的比例,它的含义如下
在这里插入图片描述
【示例3-3】考察以下这段简单的Java程序,它连续向系统请求10MB空间(每次申请1MB)。

在这里插入图片描述
使用不同的堆分配参数执行这段程序,因为虚拟机的表现受到堆空间分配的影响,所以运行过程不尽相同。下面分别使用不同的参数执行,读者可以一起来实战演习。

(1)使用-Xmx20m-Xms20m-Xmn1m-XX:SurvivorRatio=2-
XX:+PrintGCDetails运行上述Java程序,输出如下

在这里插入图片描述
这里eden区与from区的比值为2∶1,故eden区为512KB。总可用新生代大小为512KB+256KB=768KB,新生代总大小为512KB+256KB+256KB=1024KB=1MB。

由于eden区无法容纳任何一个程序中分配的1MB数组,故触发了一次新生代GC,对eden区进行了部分回收。同时,这个偏小的新生代无法为1MB数组预留空间,故所有的数组都分配在老年代,老年代
最终占用10354KB空间。

(2)使用参数-Xmx20m-Xms20m-Xmn7m-
XX:SurvivorRatio=2-XX:+PrintGCDetails运行上述程序,将新生代
扩大为7MB,则输出如下

在这里插入图片描述
在这个参数下,由于eden区有足够的空间,因此所有的数组都分配在eden区。但eden区并不足以预留10MB的空间,故在程序运行期间出现了3次新生代GC。由于每申请一次空间,同时也废弃了上一次申请的空间(上一次申请的内存失去了引用),故在新生代GC中,有效回收了这些失效的内存。最终结果是:所有的内存分配都在新生代进行,通过GC保证了新生代有足够的空间,而老年代没有为这些数组预留任何空间,只是在GC过程中,部分新生代对象晋升到老年代。

(3)使用参数-Xmx20m-Xms20m-Xmn15m-
XX:SurvivorRatio=8-XX:+PrintGCDetails运行上述程序,输出如下:
在这里插入图片描述
在这次执行中,由于新生代使用15MB空间,其中eden区占用了12288KB,完全满足10MB数组的分配,因此所有的分配行为都在eden区直接进行,且没有触发任何GC行为。因此from/to和老年代
tenured
的使用率都为0。

由此可见,不同的堆分布情况对系统会产生一定影响。在实际工作中,应该根据系统的特点做合理的设置,基本策略是:尽可能将对象预留在新生代,减少老年代GC的次数(本例中的第一种情况,对
象都分配在老年代,显然为后续的老年代GC埋下了伏笔)。除了可以使用参数-Xmn指定新生代的绝对大小,还可以使用参数-XX:NewRatio来设置新生代和老年代的比例
,如下所示:

在这里插入图片描述
(4)使用参数-Xmx20M-Xms20M-XX:NewRatio=2-XX:
+PrintGCDetails运行上述代码,输出如下:

在这里插入图片描述
此时,因为堆大小为20MB,新生代和老年代的比为1∶2,故新生代大小为20MB×1/3≈7MB,老年代为13MB。由于在新生代GC时,from/to空间不足以容纳任何一个1MB数组,影响了新生代的正
常回收,故在新生代回收时需要老年代进行空间担保。这导致两个1MB数组进入老年代(在新生代GC时,尚有1MB数组幸存,理应进入from/to,而from/to只有640KB,不足以容纳)。

注意:
-XX:SurvivorRatio可以设置eden区与survivor的比例。-
XX:NewRatio可以设置老年代与新生代的比例。

3.2.3 堆溢出处理 参数-XX: +HeapDumpOnOutOfMemoryError

在Java程序的运行过程中,如果堆空间不足,则有可能抛出内存溢出错误,简称OOM(OutOfMemory)。如下文字显示了典型的堆内存溢出

在这里插入图片描述
Java虚拟机提供了参数-XX:+HeapDumpOnOutOfMemoryError,可以在内存溢出时导出整个堆
的信息。和它配合使用的还有-XX:HeapDumpPath,可以指定导出堆的存放路径。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
可以看到,虚拟机将当前的堆信息导出,并保存到D:/a.dump文件。使用MAT等工具打开该文件进行分析,如图3.3所示,可以很容易地找到这些byte数组和保存它们的Vector对象实例。有关MAT等工
具的使用方法,可以参阅第7章。除了在发生OOM时可以导出堆信息,虚拟机还允许在发生错误
时执行一个脚本文件。该文件可以用于崩溃的程序的自救、报警或者通知,也可以帮助开发人员获得更多的系统信息,如完整的线程转存(即Thread Dump或者Core Dump)文件。

在这里插入图片描述
这里给出一个在发生OOM时导出线程转存的例子。准备printstack.bat脚本如下:

在这里插入图片描述
以上脚本将会导出给定Java虚拟机进程的线程信息,并保存在D:/a.txt文件中。

使用如下参数执行上述代码

在这里插入图片描述
在程序异常退出时,系统D盘会生成新文件a.txt,里面保存着线
程转存信息。本例中,文件路径“D:/tools/jdk1.7_40”为笔者的JDK安
装目录,读者可以替换成自己的JAVA_HOME目录进行尝试。

3.3 了解非堆内存的参数配置

除了堆内存,虚拟机还有一些内存用于方法区、线程栈和直接内存。它们与堆内存是相对独立的。与堆内存相比,虽然这些内存空间和应用程序本身的关系可能不那么密切,但是从系统层面看,有效、
合理地配置这些内存参数,对系统的性能和稳定性也有着重要作用

3.3.1 方法区配置 -XX:MaxMetaspaceSize指定永久区的最大可用值

在JDK1.6和JDK1.7等版本中,可以使用-XX:PermSize和-XX:MaxPermSize配置永久区大小。其中,-XX:PermSize表示初始的永久区大小,-XX:MaxPermSize表示最大永久区大小。

从JDK 1.8开始,永久区被彻底移除,使用了新的元数据区存放类的元数据。在默认情况下,元数据区只受系统可用内存的限制,但依然可以使用参数-XX:MaxMetaspaceSize指定永久区的最大可用值。方法区的详细使用和配置可以参考2.5节。

3.3.2 栈配置 -Xss参数指定线程的栈大小

栈是每个线程私有的内存空间。在Java虚拟机中可以使用-Xss参数指定线程的栈大小。由于在2.4节中已经详细介绍了栈的配置和使用,这里不再展开。

3.3.3 直接内存配置 参数 -XX:MaxDirectMemorySize

直接内存也是Java程序中非常重要的组成部分,特别是在NIO被广泛使用后,直接内存的使用也变得非常普遍。直接内存跳过了Java堆,使Java程序可以直接访问原生堆空间。因此,从一定程度上加快
了内存空间的访问速度。但是,武断地认为使用直接内存一定可以提高内存访问速度也是不正确的。

最大可用直接内存可以使用参数-XX:MaxDirectMemorySize设置,如果不设置,默认值为最大堆空间,即-Xmx的值当直接内存使用量达到-XX:MaxDirectMemorySize时,就会触发垃圾回收,如果垃圾回收不能有效释放足够的空间,直接内存溢出依然会引起系统的OOM。

由此可以得出结论:直接内存适合申请次数较少、访问较频繁的场合。如果需要频繁申请内存空间,则并不适合使用直接内存。

3.4 Client和Server二选一:虚拟机的工作模式

目前Java虚拟机支持Client和Server两种运行模式。使用参数-client可以指定使用Client模式,使用参数-server可以指定使用Server模式。在默认情况下,虚拟机会根据当前计算机系统环境自动选择运
行模式。使用-version参数可以查看当前模式

在这里插入图片描述
使用-server参数后,就可以得到如下输出:

在这里插入图片描述
与Client模式相比,Server模式的启动比较慢,因为Server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。因此,当系统完全启动并进入运行稳定期后,Server模式的执行速度会远远快于Client模式。所以,对于后台长期运行的系统来说,使用-server参数启动对系统的整体性能可以有不小的帮助。但对于用户界面程序而言,运行时间不长,又追求启动速度,Client模式

四、 垃圾回收的概念与算法

4.1 常用的垃圾回收算法

主要内容是理解Java垃圾回收机制的理论基础,包括引用计数法、标记清除法、复制算法、标记压缩法、分代算法和分区算法

在这里插入图片描述

注意:
由于单纯的引用计数法隐含着循环引用及性能问题,Java虚拟机并未选择此算法作为垃圾回收算法。
【名词解释】

  • 可达对象: 指通过根对象进行引用搜索,最终可以达到的对
    象。
  • 不可达对象 :指通过根对象进行引用搜索,最终没有被引用到
    的对象。

4.1.1 引用计数法(Reference Counting)

引用计数法的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使
用。

引用计数器的实现也非常简单,只需要为每个对象配备一个整型的计数器即可。但是,引用计数器有两个非常严重的问题:
(1)无法处理循环引用。因此,在Java的垃圾回收器中没有使用这种算法。
(2)引用计算器要求在每次引用产生和消除的时候,伴随一个加法操作和一个减法操作,对系统性能会有一定的影响。

4.1.2 标记清除法(Mark-Sweep)

标记清除法是现代垃圾回收算法的思想基础。标记清除法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现如下。

  • 在标记阶段,首先通过根节点标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。
  • 在清除阶段,清除所有未被标记的对象。标记清除法的最大问题是可能产生空间碎片。
    在这里插入图片描述
    回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续空间。因此,这也是该算法的最大缺点。

注意:
标记清除法先通过根节点标记所有的可达对象,然后清除所有的不可达对象,完成垃圾回收。

4.1.3 复制算法(Copying)

复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。。又由于对象是在垃圾回收过程中统一被复制到新的内存空间中的,可确保回收后的内存空间是没有碎片的。虽然有以上两大优点,但是复制算法的代价却是将系统内存折半,因此,单纯的复制算法也很难让人接受。
如图4.3所示,A、B两块相同的内存空间,A在进行垃圾回收时,将存活对象复制到B中,B在复制后保持连续。复制完成后,清空A,并将空间B设置为当前使用空间。

在这里插入图片描述
在Java的新生代串行垃圾回收器中,使用了复制算法的思想。新生代分为eden区、from区和to区3个部分。其中from区和to区可以视为用于复制的两块大小相同、地位相等且可进行角色互换的空间。
from区和to区也称为survivor区,即幸存者空间,用于存放未被回收的对象。

【名词解释】

  • 新生代: 存放年轻对象的堆空间。年轻对象指刚刚创建的或者经历垃圾回收次数不多的对象。
  • 老年代: 存放老年对象的堆空间。老年对象指经历多次垃圾回收后依然存活的对象。

在进行垃圾回收时,eden区的存活对象会被复制到未使用的survivor区(假设是to区),正在使用的survivor区(假设是from)的年轻对象也会被复制到to区(大对象或者老年对象会直接进入老年
代,如果to区已满,则对象也会直接进入老年代)。此时,eden区和from区的剩余对象就是垃圾对象,可以直接清空,to区则存放此次回收后的存活对象。这种改进的复制算法,既保证了空间的连续性,又避免了大量的内存空间浪费
,如图4.4所示,显示了复制算法的实际回收过程。当所有存活对象都复制到survivor区(图中为to)后,简单地清空eden区和备用的survivor区(图中为from)即可。

注意:
复制算法比较适合新生代,因为在新生代垃圾对象通常会多于存活对象,复制算法的效果会比较好。

4.1.4 标记压缩法(Mark-Compact)

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本将很高。因此,基于老年代垃圾回收的特性,需要使用其他算法。

标记压缩法是一种老代的回收算法。它在标记清除法的基础上做了一些优化。和标记清除法一样,标记压缩法首先也需要从根节点开始,对所有可达对象做一次标记。但之后,它并不只是简单地清理
未标记的对象,而是将所有的存活对象压缩到内存的一端。然后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,性价比较高。

如图4.5所示,在通过根节点标记出所有的可达对象后,沿虚线进行对象移动,将所有的可达对象都移动到一端,并保持它们之间的引用关系,最后清理边界外的空间,即可完成垃圾回收工作。

在这里插入图片描述
图4.5 标记压缩算法工作示意图
标记压缩法的最终效果等同于标记清除法执行完成后再进行一次内存碎片整理,因此,也可以把它称为标记清除压缩法。

4.1.5 分代算法(Generational Collecting)

在前面介绍的算法中,没有一种算法可以完全替代其他算法,它们都有自己的优势和特点。根据垃圾回收对象的特性,使用合适的算法,才是明智的选择。分代算法就基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点使用不同的回收算法,以提高垃圾回收的效率

一般来说,Java虚拟机会将所有的新建对象都放入称为新生代的内存区域,新生代的特点是对象朝生夕灭,大约90%的新建对象会被很快回收,因此新生代比较适合使用复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收依然得以存活的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。在极端情况下,老年代对象的存活率可以达到100%。如果依然使用复制算法回收老年代,将需要复制大量对象。再加上老年代的回收性价比也低于新生代,因此这种做法是不可取的。根据分代的思想,可以对老年代的回收使用与新生代不同的标记压缩法或标记清除法,以提高垃圾回收效率。如图4.6所示,显示了这种分代回收的思想。

注意:
分代回收的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

在这里插入图片描述
对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的耗时很短,而老年代回收的频率比较低,但是会消耗更多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表
(Card Table)的数据结构。卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。这样在新生代GC时,可以不用花大量时间扫描所有的老年代对象来确定每一个对象的引用关系,可以先扫描卡表,只有当卡表的标记位为1时,才需要扫描给定区域的老年代对象,而卡表位为0的老年代对象,一定不含有新生代对象的引用。如图4.7所示,卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此,在新生代GC时只需要扫描卡表位为1的老年代空间。使用这种方式,可以大大加快新生代的回收速度

在这里插入图片描述
图4.7 根据卡表新生代GC只需扫描部分老年代

4.1.6 分区算法(Region)

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。如图4.8所示。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收小区
间的数量

一般来说,在相同条件下,堆空间越大,一次GC所需要的时间就越长,从而产生的停顿也越长(GC产生的停顿请参见4.4节)。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个
小块,根据目标停顿时间,每次合理地回收若干个小区间,而不是回收整个堆空间,从而减少一次GC所产生的停顿

在这里插入图片描述

4.2 谁才是真正的垃圾:判断可触及性

4.2.1 对象的复活

垃圾回收的基本思想是考查每一个对象的可触及性,即从根节点开始是否可以访问这个对象,如果可以,则说明当前对象正在被使用,如果从所有的根节点开始都无法访问到某个对象,说明该对象已
经不再使用了,一般来说,该对象需要被回收。但事实上,一个无法触及的对象有可能在某个条件下使自己“复活”,如果是这样的情况,那么对它的回收就是不合理的,为此,需要给出一个对象可触及性状态的定义,并规定在什么状态下才可以安全地回收对象

简单来说,可触及性包含以下3种状态。

  • 可触及的: 从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活。
  • 不可触及的: 对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()函数只会被调用一次。

以上3种状态中,只有在对象不可触及时才可以被回收。

4.2.2 引用和可触及性的强度

finalize()函数是一个非常糟糕的模式,不推荐读者使用finalize()函数释放资源。
第一,因为finalize()函数有可能发生引用外泄,在无意中复活对象;
第二,由于finalize()函数是被系统调用的,调用时间是不明确的,因此不是一个好的资源释放方案,推荐在try-catch-finally语句中进行资源的释放。

4.2.3 引用和可触及性的强度

在Java中提供了4个级别的引用:强引用、软引用、弱引用和虚引用。除强引用外,其他3种引用均可以在java.lang.ref包中找到。如图4.9所示,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。其中FinalReference为“最终”引用,它用以实现对象的finalize()函数

在这里插入图片描述

4.2.3.1 强引用
  • 强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下都是可以被回收的
    在这里插入图片描述
    假设以上代码是在函数体内运行的,那么局部变量str将被分配在栈上,而对象StringBuffer实例将被分配在堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是
    StringBuffer实例的强引用,如图4.10所示。
    在这里插入图片描述
    此时,如果再运行一个赋值语句:
    在这里插入图片描述
    那么,str所指向的对象也将被str1所指向,同时在局部变量表上会分配空间存放str1变量,如图4.11所示。此时,该StringBuffer实例就有两个引用。引用的“==”操作用于表示两操作数所指向的堆空间
    地址是否相同,不表示两操作数所指向的对象是否相等。

本例中的两个引用都是强引用,强引用具备以下特点:

  • 强引用可以直接访问目标对象。
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向的对象。
  • 强引用可能导致内存泄漏。
4.2.3.2 软引用

软引用是比强引用弱一点的引用类型。如果一个对象只持有软引用,那么当堆空间不足时,就会被回收。软引用使用java.lang.ref.SoftReference类实现。

下面的示例演示了软引用会在系统堆内存不足时被回收。
在这里插入图片描述
在这里插入图片描述
使用参数-Xmx10m运行上述代码,得到:在这里插入图片描述
因此,从该示例中可以得到结论:GC未必会回收软引用的对象,但是当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

4.2.3.4 弱引用—发现即回收

弱引用是一种比软引用弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,并不一定能很快地发现
持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。一旦一个弱引用对象被垃圾回收器回收,便会加入一个注册的引用队列(这一点和软引用很像)
弱引用使用java.lang.ref.WeakReference类实现。

下面的例子显示了弱引用的特点

在这里插入图片描述
在这里插入图片描述
可以看到,在GC之后弱引用对象立即被清除。弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。读者可以参考4.3.3节中的介绍自行实现,在此不再赘述。

注意:
软引用、弱引用都非常适合保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当
长的时间,从而起到让系统加速的作用。

4.2.3.5 虚引用—对象回收跟踪

虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3垃圾回收时的停顿现象:Stop-The-World案例实战

垃圾回收器的任务是识别和回收垃圾对象,以进行内存清理。为了让垃圾回收器可以正常且高效地执行,在大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程的执行,只
有这样系统中才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于垃圾回收器更好地标记垃圾对象。因此,在垃圾回收时,都会产生应用程序的停顿。停顿产生时,整个应用程序会被卡死,没有任何响应,因此这个停顿也叫作“Stop-The-World”(STW)

使用以下参数运行上述代码
在这里插入图片描述
此参数设置了1GB的堆空间,并基本废弃了新生代(只保留512KB)。运行程序得到的部分输出如下:
在这里插入图片描述
注意加粗部分,原本应该每0.1秒进行的输出在这几处有明显的时间延长。对应的GC日志如下:
在这里插入图片描述

注意加粗部分的GC时间戳和实际花费的时间,不难发现,在程序打印的25.12025.956秒处,发生了大约0.8秒左右的停顿,对应于GC日志中25.286秒的FullGC。在程序打印的25.95626.56秒处,发生了大约0.6秒的停顿,对应于GC日志26.248秒处的0.6秒停顿。在26.75527.363秒处发生了大约0.6秒的停顿,对应于GC日志26.854秒处的0.61秒的停顿。最后,在27.36328.179秒处发生了大约0.8秒的停顿,对应于GC日志27.460秒处的0.82秒的停顿。由此可见,每一次应用程序的意外停顿都可以在GC日志中找到对应的线索给予解释。这也间接证明了GC对于应用程序的影响。笔者使用VisualGC观察上述程序的运行过程,如图4.12所示,可以看到老年代GC共进行了5次,合计耗时2.9秒,平均一次约0.6秒,而新生代GC合计进行了3895次,合计耗时5.1秒多,GC总耗时约8秒。
在这里插入图片描述
图4.12 使用Visual GC观察GC过程1
从这个例子可以看到,新生代GC比较频繁,但每一次GC耗时较短,老年代GC发生次数较少,但每一次所消耗的时间较长。这种现象和虚拟机参数设置有关。下面通过修改虚拟机参数改变这种现象。
使用下面的虚拟机参数执行上述代码:
在这里插入图片描述
从这个例子可以看到,新生代GC比较频繁,但每一次GC耗时较短,老年代GC发生次数较少,但每一次所消耗的时间较长。这种现象和虚拟机参数设置有关。下面通过修改虚拟机参数改变这种现象。
使用下面的虚拟机参数执行上述代码:
在这里插入图片描述
此参数设置了一个较大的新生代(900MB),并将from、to和eden区设置为各300MB。同时,修改上述代码第8行为(读者考虑一下为何需要做这个修改):
在这里插入图片描述
使用这种超大新生代的设置,会导致复制算法复制大量对象,也
会在很大程度上延长GC时间。程序的部分输出如下:
在这里插入图片描述
在这里插入图片描述
可以看到,在10.351~11.409处产生了大约1秒的停顿。翻阅GC
日志,不难发现:
在这里插入图片描述
使用Visual GC观察这次行为,如图4.13所示。
在这里插入图片描述
可以看到,在增大新生代空间后,新生代GC次数明显减少,但是每次耗时增加,这里显示的6次新生代GC合计耗时775毫秒。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1694233.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

primeflex样式库笔记 Display相关的案例

回顾 宽度设置的基本总结 w-full:表示widtdh:100%;占满父容器的宽度。 w-screen:表示占满整个屏幕的宽度。 w-1到w-12,是按百分比划分宽度,数字越大,占据的比例就越大。 w-1rem到w-30rem&…

推特热帖:大语言模型自荐能够替代的20种人类工作!快来看你是否需要转行!

最近推特上有一个例子引起了广泛的讨论,事情的起因是这样的:网友让 GPT-4o 预测一下自己未来将会替代人类哪些工作? 这听起来很有趣!GPT-4o会给出什么样的预测呢? 3.5研究测试:hujiaoai.cn 4研究测试&…

动物合并消除休闲游戏源码 Animal Merge 益智游戏

一款动物合并消除休闲游戏源码,Animal Merge是一款引人入胜的益智游戏,玩家的任务是合并方块,创造出可爱的动物,这些动物的体型会逐渐变大。游戏玩法包括将方块放到网格上,并战略性地将它们合并以形成更大的动物形状。…

数据库--数据库基础(一)

目录 第一章 绪论 一.数据库的基本概念 1. 数据库的4个基本概念 2、数据库系统的特点 二.数据库和文件 三.数据模型 1.概念模型 2.逻辑模型(物理模型) 2.1关系模型 四.数据库系统的三级模式结构: 五数据库的二级映像功能与数据独立性 第二章 关系数据库…

ffpmeg windows WSl 编译so

1.NDK 环境变量配置 2.git clone ffpmeg 3.创建脚本(需先下载gcc编译器) 64位脚本如下 #!/bin/bashexport NDK/home/test/ndk20 #这里配置先你的 NDK 路径 TOOLCHAIN$NDK/toolchains/llvm/prebuilt/linux-x86_64function build_android {./configure \ …

javas-core VS java-object-diff

对照工具选择 javas-core 和 java-object-diff ,对比demo https://github.com/kofgame/objectdiff-vs-javers,都为同源对比,都支持嵌套对象。 使用JMH测试方法进行性能测试,使用题库的QuestionResponseVO对象来进行对照对比,进行…

若依nodejs版本过高问题解决方案

由于nodejs版本过高,可能会导致vue-cli项目运行报错。 目录 方法1:每次启动项目前,输入配置命令 方法2:修改package.js

aws msk加密方式和问控制连接方式

msk加密方式 msk提供了两种加密方式 静态加密传输中加密 创建集群时可以指定加密方式,参数如下 aws kafka create-cluster --cluster-name "ExampleClusterName" --broker-node-group-info file://brokernodegroupinfo.json --encryption-info file:/…

结构体(位段)内存分配

结构体由多个数据类型的成员组成。那编译器分配的内存是不是所有成员的字节数总和呢? 首先,stu的内存大小并不为29个字节,即证明结构体内存不是所有成员的字节数和。   其次,stu成员中sex的内存位置不在21,即可推测…

【AI大模型】这可能是最简单的本地大模型工具,无须部署,一键使用

目录 前言 LM-Studio​编辑 那么问题来了,为什么我要在本地部署大模型? 隐私性: 定制性: 成本和体验的优化: 工具功能特点和使用方式介绍: 首页提供搜索功能和一些模型的推荐 模型下载管理&#x…

监控员工电脑屏幕的五大软件(电脑监控软件大盘点)

监控员工电脑屏幕是企业为了提升工作效率、确保信息安全和合规性而采取的一种常见做法。以下是五款在2024年备受推荐的员工电脑屏幕监控软件,每款软件都具有其独特的功能和优势: 1. 域智盾 域智盾是一款全面的终端管理系统,集成了实时屏幕监…

开源博客项目Blog .NET Core源码学习(26:App.Hosting项目结构分析-14)

后台管理页面的系统管理下主要包括用户管理、角色管理、按钮管理和菜单管理,其中创建用户时要指定角色,创建角色时需指定菜单权限,按钮管理也是基于各菜单项进行设置,只有菜单管理相对独立,因此本文学习并分析App.Host…

JAVA云HIS医院系统源码 HIS源码:云HIS系统与SaaS的关系

云HIS系统与SaaS的关系 云HIS系统是一种基于云计算技术的医院信息系统,它采用B/S架构,通过云端SaaS服务的方式提供。用户可以通过浏览器访问云HIS系统,无需关注系统的部署、维护、升级等问题。云HIS系统通常具有模板化、配置化、智能化等特点…

SpirngMVC框架学习笔记(一):SpringMVC基本介绍

1 SpringMVC 特点&概述 SpringMVC 从易用性,效率上 比曾经流行的 Struts2 更好 SpringMVC 是 WEB 层框架,接管了 Web 层组件, 比如控制器, 视图, 视图解析, 返回给用户的数据格式, 同时支持 MVC 的开发模式/开发架构SpringMVC 通过注解,…

sky walking日志采集以及注意事项

文章目录 1,sky walking日志采集功能概述2,采集log4j2日志3,采集logback日志4,效果展示5,注意事项 1,sky walking日志采集功能概述 在介绍Sky walking日志采集功能之前,最好在系统学习一遍日志…

java在类的定义中创建自己的对象?

当在main方法中新建自身所在类的对象,并调用main方法时,会不断循环调用main方法,直到栈溢出 package com.keywordStudy;public class mainTest {static int value 33;public static void main(String[] args) throws Exception{String[] sn…

【C语言刷题系列】求一个数组中两个元素a和b的和最接近整数m

💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:C语言刷题系列 目录 一、问题描述 二、解题思路 解题思路: 解题步骤: 三、C语言代码实现及测试 一、问题描述 给定一…

03_前端三大件CSS

文章目录 CSS用于页面元素美化1.CSS引入1.1style方式1.2写入head中,通过写style然后进行标签选择器加载样式1.3外部样式表 2.CSS样式选择器2.1 元素选择器2.2 id选择器2.3 class选择器 3.CSS布局相关3.1 CSS浮动背景:先设计一些盒子因此,引出…

中国企业出海,哪些业务需要负载均衡?

国内企业出海的进程正在加速。中国的出海企业剑指跨境电商、社交、游戏、短剧等市场,其中尤其以跨境电商的数据最为突出。据官方数据,2023年我国跨境电商进出口总额达到2.38万亿元,比2016年增长近50倍,占货物贸易总规模的5.7%。 …

6.小程序页面布局 - 账单明细

文章目录 1. 6.小程序页面布局 - 账单明细1.1. 竞品1.2. 布局分析1.3. 布局demo1.4. 页面实现-头部1.5. 账单明细1.5.1. 账单明细-竞品分析1.5.2. 账单明细-实现1.5.2.1. 账单明细-实现-mock数据1.5.2.2. 每日收支数据的聚合整理1.5.2.3. 页面scroll-view 1.6. TODO 1. 6.小程序…