JAVA虚拟机最强详解

230 阅读49分钟

内容来源,感谢!!

www.cnblogs.com/ysocean/p/9…

www.cnblogs.com/ysocean/p/9…

www.cnblogs.com/ysocean/p/9…

www.cnblogs.com/ysocean/p/1…

www.cnblogs.com/ysocean/p/1…

www.cnblogs.com/ysocean/p/1…

[TOC]

Java虚拟机详解(一)------简介

目录

 


  本系列博客我们将以当前默认的主流虚拟机HotSpot 为例,详细介绍 Java虚拟机。以 JDK1.7 为主,同时介绍与 JDK1.8 的不同之处,通过Oracle官网以及各种文献进行整理,并加以验证,力求保证这块知识的正确性,完整性。

  以下是本系列博客参考的相关文档:

  ①、JDK1.7虚拟机规范:docs.oracle.com/javase/spec…

    JDK1.8虚拟机规范:docs.oracle.com/javase/spec…

  ②、Oracle Java SE 8 产品组件:docs.oracle.com/javase/8/do…

  ③、周志明老师:《深入理解Java虚拟机:JVM高级特性与最佳实践》

  ④、陈涛老师:《HotSpot实战》

回到顶部

1、Oracle Java SE 8 产品组件

  通过上面给定的地址,我们可以看到如下这张图:

  

  通常来说 Java平台标准版(Java SE)包括 Java SE开发工具包(JDK)和Java SE运行时环境(JRE)。

  JRE提供了运行以Java编程语言编写的applet和应用程序所必需的库,Java虚拟机和其他组件;JDK包括JRE以及编译器和调试器等命令行开发工具,可以用来开发Java应用程序 。

  PS:JDK包含JRE,我们通常安装JDK的同时也会安装JRE。

回到顶部

2、虚拟机

  上图的最下一行Java虚拟机是被 JRE 所包含,我们在介绍Java虚拟机时,先了解虚拟机的概念。

  所谓虚拟机,其实就是一台虚拟的机器,可以用来执行一系列虚拟的命令。大体上虚拟机可以分为两种:系统虚拟机和程序虚拟机。

  ①、系统虚拟机:是完全对物理计算机的仿真,可以说和一台真实的PC操作系统没什么区别。比如常用的 Vmare 以及 Visual Box 软件,通过这些软件能够模拟出具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。

  ②、程序虚拟机:专门为执行单个计算程序而产生,最典型的就是Java虚拟机,在Java虚拟机中执行字节码文件命令。

回到顶部

3、Java虚拟机

  了解了什么是虚拟机,我们再看什么是 Java虚拟机。

  Java虚拟机可以看做是一台抽象的计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域,它与Java语言没有必然的联系,只与特定的二进制文件——class 文件格式关联(字节码文件),可以通过Java语言或者其他语言编写的程序编译成class文件,然后在Java虚拟机上运行。Java虚拟机有以下二个特点:

  ①、语言无关

  Java虚拟机只和class文件关联,所以只要你编写程序的语言能够编译成class文件,那么都能够在Java虚拟机上运行。

  

 

  ②、平台无关

  Java从诞生之初就宣传的一个口号:一次编写,到处运行。

  也就是说Java是一个跨平台的语言,那么Java是如何实现跨平台的呢?

  其实Java之所以跨平台是因为Java虚拟机的适配,不同的系统实现不同的Java虚拟机。Java虚拟机就相当于操作系统和应用程序之间的中介,每种平台安装适应该平台的Java虚拟机,那么我们编写的程序当然能够在任意平台运行。

  

回到顶部

4、Java虚拟机种类

  商用虚拟机:

   ①、Sun HotSpot

  该虚拟机性能优越,是 sun JDK1.3 及以后所有 sun JDK 版本默认的虚拟机,使用最为广泛,本系列博客就是以这个虚拟机为平台进行介绍。

  

  ②、BEA  JRockit

  JRockit 虚拟机是 BEA 公司于 2002 年从 Appeal Virtual Machines 收购获得的虚拟机。它是一款面向服务器硬件和服务端使用场景高度优化过得虚拟机,曾经号称是“世界上速度最快的虚拟机”。由于专注于服务端应用,它的内部不包含解析器的实现,全部代码都靠即时编译器编译后执行。

  ③、IBM J9

  J9 虚拟机是 IBM 公司单独开发的高性能虚拟机,它并不独立出售,而是作为 IBM 公司各种产品的执行平台,IBM 把它定义为一个可以适应从嵌入式设备到大型企业级应用的、高可移植性的Java运行平台。

  ④、Sun Classic

  这个虚拟机很原始,是 JDK1.0 时代使用的Java虚拟机,是各种虚拟机的鼻祖,它的内部不存在即时编译器,只能使用纯解释的方式运行。

  ⑤、Sun Exact

  这是 Sun 公司在 HotSpot 之外的另一个虚拟机,在 JDK1.2 时代曾短暂的投入过商用,它和 HotSpot 同时开发,但最终被 HotSpot 取代。

  ⑥、Apache Harmony

  Harmony 是 Apache 软件基金会主导的、开源的、独立的、实际兼容与 JDK1.5 和 JDK1.6的虚拟机实现,它间接催生了 Google Android 平台的 Dalvik 虚拟机,Android 的影响力现在有多大不用多说,目前已经是最成功的的数码设备通用平台。但是由于它的 TCK 授权问题,直接导致 Apache 与 Oracle 的决裂,从而退出了 JCP 组成,这是近代 Java 阵营遇到的最严重的分裂危机。

  嵌入式虚拟机

  ①、Dalvik

  Dalvik 虚拟机是 Google 等厂商合作开发的 Android 移动设备平台的核心组成部分之一,它执行 dex(Dalvik Executable) 文件而不是 class 文件,使用寄存器架构而不是栈架构,但是它的开发体系与Java有着千丝万缕的关系,可以直接使用大部分的 Java API、dex 文件可以直接从class文件转化而来。并且在 Android 2.2 中提供了即时编译器的实现,性能大大的提高。

  ②、KVM

  在 Android、IOS 等智能手机操作系统出现之前,曾广泛应用于手机平台的一种虚拟机。

  ③、CDC/CLDC HotSpot

  CDC和 CLDC HotSpot 分别是 Sun 针对高端嵌入式设备和中低端嵌入式设备的虚拟机,用来代替 KVM。


Java虚拟机详解(二)------运行时内存结构

目录

 


  首先通过一张图了解 Java程序的执行流程:

  

  我们编写好的Java源代码程序,通过Java编译器javac编译成Java虚拟机识别的class文件(字节码文件),然后由 JVM 中的类加载器加载编译生成的字节码文件,加载完毕之后再由 JVM 执行引擎去执行。在加载完毕到执行过程中,JVM会将程序执行时用到的数据和相关信息存储在运行时数据区(Runtime Data Area),这块区域也就是我们常说的JVM内存结构,垃圾回收也是作用在该区域。

  关于这幅图涉及到的:

  ①、class文件

  ②、类加载器

  ③、运行时数据区

  ④、执行引擎

  ⑤、垃圾回收器

  这都是接下来将要介绍的重点。

  本篇博客我们将首先介绍什么是运行时数据区。

  PS:下面介绍的是根据 Java虚拟机规范 定义的运行时数据区,上一篇博客我们讲过根据虚拟机规范实现的虚拟机有很多个,而不同的虚拟机其运行时数据区定义也会有所不同。比如默认的 HotSpot 在实现 JDK1.7 虚拟机规范时,其常量池的定义不在方法区中,而是移到了堆中;到了 HotSpot JDK1.8 中,则彻底移除了持久代(方法区)而使用Metaspace(元数据区)来进行替代等等,关于这些区别本篇博客也会在文章末尾进行相应的说明。

回到顶部

1、运行时数据区结构图

  ①、Java虚拟机规范定义的运行时数据区

   

  ②、HotSpot JDK1.8定义的运行时数据区

  

  注意: HotSpot实现的运行时数据区和Java虚拟机规范定义的还是有所不同的,

  ①、将Java虚拟机栈和本地方法栈合二为一;

  ②、元数据区取代了方法区,并且元数据区不在Java虚拟机中,而是在本地内存中。

  ③、运行时常量池由方法区中移到了堆中

回到顶部

2、程序计数器

  程序计数器(Program Conputer Register)这是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  ①、线程私有

  Java虚拟机支持多线程,是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任一确定的时刻,一个处理器只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。因此线程启动时,JVM 会为每个线程分配一个PC寄存器(Program Conter,也称程序计数器)。

②、记录当前字节码指令执行地址

  如果当前线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,则这个计数器值为空(Undefined)。

  ③、不抛 OutOfMemoryError 异常

  程序计数器的空间大小不会随着程序执行而改变,始终只是保存一个 returnAdress 类型的数据或者一个与平台相关的本地指针的值。所以该区域是Java运行时内存区域中唯一一个Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

回到顶部

3、虚拟机栈

  Java虚拟机栈(Java Virtual Machine stack),这块区域也是线程私有的,与线程同时创建,用于存储栈帧。Java 每个方法执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  

  ①、线程私有

  随线程创建而创建,声明周期和线程保持一致。

  ②、由栈帧组成

  线程每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  ③、抛出 StackOverflowError 和 OutOfMemoryError 异常

  如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError  异常。

回到顶部

4、本地方法栈

  本地方法栈(Native Method Stacks)作用和虚拟机栈类型,虚拟机栈执行的是Java方法,本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常。

  注意:由于虚拟机规范并没有对本地方法栈中的方法使用语言、使用方式和数据结构强制规定,因此具体的虚拟机可以自由实现它。上图我们也给出在 HotSpot 虚拟机中,本地方法栈和虚拟机栈合为一体了。

回到顶部

5、Java堆

  Java堆是Java虚拟机所管理内存最大、被所有线程共享的一块区域,目的是用来存放对象,基本上所有的对象实例和数组都在堆上分配(不是绝对)。Java堆也是垃圾回收器管理的主要区域。

  ①、线程共享

  堆存放的对象,某个线程修改了对象属性,另外一个线程从堆中获取的该对象是修改后的对象,为什么堆要设计成线程共享呢?

  我们可以假设堆是线程私有的,很显然一个系统创建的对象会有很多,而且有些对象会比较大,如果设计成线程私有的,那么如果有很多线程同时工作,那么都必须给他们分配相应的私有内存,我相信内存很快就撑爆了,很显然将堆设计为线程共享是最好不过了,不过凡事都具有两面性,线程共享的设计这也带来了多线程并发资源冲突问题,关于这个问题由于不是本系列博客的主旨,这里就不做详细介绍了。

  ②、存放对象

  基本上所有的对象实例和数组都要在堆上进行分配,但是随着 JIT 编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换等优化技术会导致对象不一定在堆上进行分配。

  ③、垃圾收集

  Java堆也被称为“GC堆”,是垃圾回收器的主要操作内存区域。当前垃圾回收器都是使用的分代收集算法,所以Java堆还可以分为:新生代和老年代,而新生代又可以分为 Eden 空间、From Survivor 空间、To Survivor空间。这是为了更好的回收内存,关于垃圾回收算法在后续博客会详细介绍。

  

  ④、抛出 OutOfMemoryError 异常

  根据Java虚拟机规范,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,实现时既可以实现成固定大小,也可以是扩展的。如果在堆中没有完成实例分配,并且堆也无法扩展,将抛出OutOfMemoryError 异常。

回到顶部

6、方法区

juejin.cn/post/708667… 详解    方法区(Method Area)用来存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  方法区也称为“永久代”,这是因为垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。经常会导致对此内存未完全回收而导致内存泄露,最后当方法区无法满足内存分配时,将抛出 OutOfMemoryError 异常。

  PS:在Java虚拟机规范中把方法区描述为堆的一个逻辑部分(docs.oracle.com/javase/spec…),在很多虚拟机中(JRockit、IBM J9等虚拟机不存在永久代的概念)。

     在JDK1.8 的 HotSpot 虚拟机中,已经去掉了方法区的概念,用 Metaspace 代替,并且将其移到了本地内存来规划了。

回到顶部

7、运行时常量池

   在Java虚拟机规范中,运行时常量池(Runtime Constant Pool)用于存放编译期生成的各种字面量和符号引用,是方法区的一部分。但是Java虚拟机规范对其没有做任何细节的要求,所以不同虚拟机实现商可以按照自己的需求来实现该区域,比如在 HotSpot 虚拟机实现中,就将运行时常量池移到了堆中。

  ①、存放字面量、符号引用、直接引用

  通常来说,该区域除了保存Class文件中描述的引用外,还会把翻译出来的直接引用也存储在运行时常量池,并且Java语言并不要求常量一定只能在编译器产生,运行期间也可能将常量放入池中,比如String类的intern()方法,当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并``返回此对象的引用。 关于该方法的介绍可以看我这篇博客

  ②、抛出 OutOfMemoryError 异常

  运行时常量池是方法区的一部分,会受到方法区内存的限制,当常量池无法申请到内存时,会抛出该异常。

回到顶部

8、直接内存

  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,它也不是Java虚拟机规范定义的内存区域。我们可以看到在 HotSpot 中,就将方法区移除了,用元数据区来代替,并且将元数据区从虚拟机运行时数据区移除了,转到了本地内存中,也就是说这块区域是受本机物理内存的限制,当申请的内存超过了本机物理内存,才会抛出 OutOfMemoryError 异常。

  直接内存也是受本机物理内存的限制,在JDK1.4中新加入的 NIO(new input/output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用操作,这样避免了在Java堆和Native堆中来回复制数据,显著提高性能。


Java虚拟机详解(三)------垃圾回收 

目录

 


  如果对C++这门语言熟悉的人,再来看Java,就会发现这两者对垃圾(内存)回收的策略有很大的不同。

  C++:垃圾回收很重要,我们必须要自己来回收!!!

  Java:垃圾回收很重要,我们必须交给系统来帮我们完成!!!

  我想这也能看出这两门语言设计者的心态吧,总之,Java和C++之间有一堵由内存动态分布和垃圾回收技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。

  本篇博客我们就来详细介绍Java的垃圾回收策略。

回到顶部

1、为什么要进行垃圾回收

  我们知道Java是一门面向对象的语言,在一个系统运行中,会伴随着很多对象的创建,而这些对象一旦创建了就占据了一定的内存,在上一篇博客Java运行时内存结构中,我们介绍过创建的对象是保存在堆中的,当对象使用完毕之后,不对其进行清理,那么会一直占据内存空间,很明显内存空间是有限的,如果不回收这些无用的对象占据的内存,那么新创建的对象申请不了内存空间,系统就会抛出异常而无法运行,所以必须要经常进行内存的回收,也就是垃圾收集。

回到顶部

2、为什么要了解垃圾回收

  文章开头,我们就说Java的垃圾回收是系统自动进行的,不需要我们程序员手动处理,那么我们为什么还要了解垃圾回收呢,?

  其实这也是一个程序员进阶的过程,生产项目在运行过程中,很可能会存在内存溢出、内存泄露等问题,出现了这些问题,我们应该怎么排查?以及在生产服务器有限的资源上如何更好的分配Java运行时内存区域,提高系统运行效率等,我们必须知其然知其所以然。

  PS:本篇博客只是介绍Java垃圾回收机制,关于排查内存泄漏、溢出,运行时内存区域参数调优等会在后面进行介绍。

回到顶部

3、回收哪部分区域内存

  还是结合上一篇博客Java运行时内存结构,我们介绍了Java运行时的内存结构,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。

  那么现在就剩下Java堆方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域。

回到顶部

4、如何判断对象为垃圾对象

  通过上面介绍了,我们了解了为什么要进行垃圾回收以及回收哪部分的垃圾,那么接下来我们怎么去区分哪些对象为垃圾呢?

  换句话来说,我们如何判断哪些对象还“活着”,哪些对象已经“死了”,那些“死了”的对象占据的内存就是我们要进行回收的。

①、引用计数算法

  这种算法是这样的:给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了。

  这种算法实现简单,而且效率也很高。但是Java没有采用该算法来进行垃圾回收,因为这种算法无法解决对象之间的循环引用问题。

  下面我们就来构造一个循环引用的例子:

  首先,有一个 Person 类,这个类有两个自引用属性,分别表示其父亲,儿子。

 View Code

  接着,我们通过Person类构造两个对象,分别是父亲,儿子,如下:

复制代码

 1 public static void main(String[] args) {
 2 
 3     Person father = new Person();
 4     Person son = new Person();
 5     father.setSon(son);
 6     son.setFather(father);
 7 
 8     father = null;
 9     son = null;
10     
11     /**
12      * 调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,
13      * 而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
14      */
15     System.gc();
16 }

复制代码

  首先,从第3-6行代码,其运行时内存结构图如下:

   

  father对象和son对象,其引用计数第一个是栈内存指向,第二个就是其属性互相引用对方,所有引用计数器都是2。

  接着我们看第8,9行代码,分别将这两个对象置为null,也就是去掉了栈内存指向。

  

  这时候其实这两个对象只是自己互相引用了,没有别的地方在引用它们,引用计数器为1,那么这两个对象按照引用计数算法实现的虚拟机就不会回收,可想而知,这是我们不能接受的。

  所以Java虚拟机都没有使用该算法来判断对象是否存活, 我们可以通过增加打印虚拟机参数来验证。

  我们将上面的man函数,增加如下Java虚拟机参数,用来打印gc信息。

-verbose:gc

  在IDEA编辑器中,添加方式如下:

  

   运行结果如下:

  

  我们看到12201K->1088K(125952K)的输出,表示垃圾收集GC前有12201K,回收后剩下1088K,堆的总量为125952K,回收的内存为12201K-1088K = 11113K。

  换句话说,上面的例子Java虚拟机是有进行垃圾回收的,所以,这也间接佐证了Java虚拟机并不是采用的引用计数法来判断对象是否是垃圾。

  PS:这些参数信息详解也会在后面博客进行详细介绍。

②、根搜索算法

  我们这里直接给出结论:在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)来判定对象是否存活。 

  该算法思路:通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了。

  

  上图Object1,Object2,Object3,Object4到GC Roots是可达的,所以不会被作为垃圾回收。

  

  上图Object1,Object2,Object3这三个对象互相引用,但是到 GC Roots不可达,所以都会被垃圾回收掉。

  那么有哪些对象可以作为 GC Roots 呢?

  在Java语言中,有如下4中对象可以作为 GC Roots:

  

 

   PS:红色的对象是要被当做垃圾回收的!

1 1、虚拟机栈(栈帧中的本地变量表)中引用的对象
2 2、方法区中的静态变量属性引用的对象
3 3、方法区中常量引用的对象
4 4、本地方法栈中(JNI)(即一般说的Native方法)的引用的对象

回到顶部

5、如何进行垃圾回收

  垃圾回收涉及到大量的程序细节,而且各个平台的虚拟机操作内存的方式也不一样,但是他们进行垃圾回收的算法是通用的,所以这里我们也只介绍几种通用算法。

①、标记-清除算法

  算法实现:分为标记-清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后在统一回收掉所有被标记的对象。

  缺点

  1、效率低:标记和清除这两个过程的效率都不高。

  2、容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryExecption。

  

②、复制算法

  为了解决标记-清除算法的两个缺点,复制算法诞生了。

  算法实现:将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块区域上,然后再把已使用过的内存空间一次性清理掉。

  优点:每次都是只对其中一块内存进行回收,不用考虑内存碎片的问题,而且分配内存时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。

  缺点:将内存分为两块,但是每次只能使用一块,也就是说,机器的一半内存是闲置的,这资源浪费有点严重。并且如果对象存活率较高,每次都需要复制大量的对象,效率也会变得很低。

  

③、标记-整理算法

  上面我们说过复制算法会浪费一半的内存,并且对象存活率较高时,会有过多的复制操作,效率低下。

  如果对象存活率很高,基本上不会进行垃圾回收时,标记-整理算法诞生了。

  算法实现:首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理到端边界以外的内存。

  局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。

  

④、分代收集算法

   当前商业虚拟机都是采用此算法,但是其实这不是什么新的算法,而是上面几种算法的合集。

  算法实现:根据对象的存活周期不同将内存分为几块,然后不同的区域采用不同的回收算法。

    1、对于存活周期较短,每次都有大批对象死亡,只有少量存活的区域,采用复制算法,因为只需要付出少量存活对象的复制成本即可完成收集;

    2、对于存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或者标记-清除算法。

  比如,对于 HotSpot 虚拟机,它将堆空间分为如下两块区域:

  

  堆有新生代和老年代两块区域组成,而新生代区域又分为三个部分,分别是 Eden,From Surivor,To Survivor ,比例是8:1:1。

  新生代采用复制算法,每次使用一块Eden区和一块Survivor区,当进行垃圾回收时,将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,然后清理到刚存放对象的区域,依次循环。

  老年代采用标记-清除或者标记-整理算法,根据使用的垃圾回收器来进行判断。

  至于为什么要这样,这是由于内存分配的机制导致的,新生代存的基本上都是朝生夕死的对象,而老年代存放的都是存活率很高的对象。关于内存分配下篇博客我们会详细进行介绍。

回到顶部

6、何时进行垃圾回收

  理清了什么是垃圾,怎么回收垃圾,最后一点就是Java虚拟机何时进行垃圾回收呢?

  程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。

  其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。

  这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。

  如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue进行第二次标记,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。


Java虚拟机详解(四)------垃圾收集器

目录

 


  上一篇博客我们介绍了Java虚拟机垃圾回收,介绍了几种常用的垃圾回收算法,包括标记-清除,标记整理,复制等,这些算法我们可以看做是内存回收的理论方法,那么在Java虚拟机中,由谁来具体实现这些方法呢?

  没错,就是本篇博客介绍的内容——垃圾收集器。

回到顶部

1、垃圾收集器种类

  事实上Java虚拟机规范对垃圾收集器应该如何实现,并没有任何的规定,所以不同的厂商、不同版本的虚拟机所提供的垃圾收集器都会有所不同,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

  下图是基于 Sun HotSpot 虚拟机1.6版 Update 22的虚拟机种类:

  

  由上图我们可以总结出几个结论:

  ①、新生代垃圾收集器:Serial、ParNew、Parallel Scavenge;

    老年代垃圾收集器:Serial Old(MSC)、Parallel Old、CMS;

    整堆垃圾收集器:G1

  ②、垃圾收集器之间的连线表示可以搭配使用,有如下几种组合:

    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

  ③、串行收集器Serial:Serial、Serial Old

    并行收集器 Parallel:Parallel Scavenge、Parallel Old

    并发收集器:CMS、G1

  ps:对于文章中有一些名词不理解的,可以先看本篇博客最后一个小节

回到顶部

2、Serial收集器

  这是一个最基本,历史最悠久的垃圾收集器,是JDK1.3之前新生代唯一的垃圾收集器。

  该收集器有如下特点:

  ①、作用于新生代

  由上图也可看出,这是一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。

  ②、单线程

  工作时只会使用一个CPU或者一条收集线程去完成工作。

  ③、进行垃圾收集时,必须暂停所有工作线程

  也就是说使用Serial收集器进行垃圾回收时,别的工作线程都暂停,系统这时候会有卡顿现象产生。

  ④、适用场景

  Serial 收集器由于没有线程交互的开销,对于限定单个CPU的环境,可以获得最高的单线程收集效率。

  一般在用户的桌面场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或一两百兆的新生代,定顿时间可以控制在几十毫秒,只要不是频繁发生的,这点停顿是可以接受的。

  所以 Serial 收集器对于运行在 Client 模式下的虚拟机是一种很好的选择。

回到顶部

3、ParNew收集器

  这个收集器其实就是Serial收集器的多线程版本。

  也就是说其特点除了多线程,其余和Serial收集器一样,事实上,这两个收集器实现上也共用了很多代码。

①、作用于新生代

  一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。

  ②、多线程

****弥补了Serial收集器单线程的缺陷。

  ③、适用场景

  由于其多线程的特性,是大多数运行在 Server 模式下的虚拟机首选新生代垃圾收集器。

  另外需要说明的是,能够与下面将要介绍的划时代垃圾收集器CMS(Concurrent Mark Sweep)配合使用,也是一个重要原因。

回到顶部

4、Parallel Scavenge收集器

  前面介绍的垃圾收集器关注点是尽可能缩小垃圾收集时的用户线程停顿时间。而 Parallel Scanvenge 收集器是为了达到一个可控制的吞吐量。

  吞吐量 = 运行用户代码的时间 / (运行用户代码的时间+垃圾收集时间)

  可以用下面两个参数进行精确控制:

  -XX:MaxGCPauseMills  设置最大垃圾收集停顿时间

  -XX:GCTimeRatio 设置吞吐量大小

①、作用于新生代

  一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。

  ②、多线程

****并行的多线程垃圾收集器。

  ③、吞吐量

  这个收集器可以精确控制吞吐量。

  ④、适用场景

****设置垃圾收集停顿时间短适合需要与用户快速交互的程序;

  而设置高吞吐量可以最高效的利用CPU效率,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

回到顶部

5、Serial Old收集器

  Serial Old 收集器是 Serial 收集器的老年代版本,特点如下:

  ①、作用于老年代

  ②、单线程

  ③、使用标记-整理算法

  ④、进行垃圾收集时,必须暂停所有工作线程

回到顶部

6、Parallel Old收集器

****Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

  ①、作用于老年代

  ②、多线程

  ③、使用标记-整理算法

****除了具有以上几个特点,比较关键的是能和新生代收集器 Parallel Scavenge 配置使用,获得吞吐量最大化的效果。

回到顶部

7、CMS收集器

  CMS,全称为 Concurrent Mark Sweep ,顾名思义并发的,采用标记-清除算法。另外也将这个收集器称为并发低延迟收集器(Concurrent Low Pause Collector)

  这是一款跨时代的垃圾收集器,真正做到了垃圾收集线程与用户线程(基本上)同时工作。和 Serial 收集器的 Stop The World(妈妈打扫房间的时候,你不能再将垃圾丢到地上) 相比,真正做到了妈妈一边打扫房间,你一边丢垃圾。

  ①、作用于老年代

  ②、多线程

  ③、使用标记-清除算法

  整个算法过程分为如下 4 步:

  一、初始标记(CMS initial mark):只是仅仅标记GC Root 能够直接关联的对象,速度很快,但是需要“Stop The World”  

  二、并发标记(CMS concurrent mark):进行GC Root Tracing的过程,简单来说就是遍历Initial Marking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。

  三、重新标记(CMS Remark):修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。这个时间一般比初始标记长,但是远比并发标记时间短。

  四、并发清除(CMS concurrent sweep):对上一步标记的对象进行清除操作。

  由于整个过程最耗时的操作是第二(并发标记)、四步(并发清除),而这两步垃圾收集器线程是可以和用户线程一起工作的。所以整体来说,CMS垃圾收集和用户线程是一起并发的执行的。

  缺点:

①、对CPU资源敏感

****因为在并发阶段,会占用一部分CPU资源,从而导致应用程序变慢,总吞吐量会降低。

  ②、产生浮动垃圾

  由于CMS并发清理阶段用户线程还在工作,这个时候产生的垃圾,CMS无法在本次收集中处理掉它们,只能留在下一次GC时再将其处理掉,这部分垃圾称为“浮动垃圾”。

  ③、产生内存垃圾碎片

  因为采用的算法是标记-清除,很明显,会有空间碎片产生。

回到顶部

8、G1收集器

  这是当前收集器技术发展的最前沿的成果。可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,首发于JDK8中,是JDK9默认的垃圾回收器。

  这是因为它并不像前面介绍的所有垃圾收集器是区分新生代,老年代的,它作用于全区域。将整个Java堆划分为多个大小固定的独立区域(Regin),并且跟踪这些区域的垃圾堆积面积,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收垃圾最多的区域,这样保证了G1收集器在有限的时间内可以获得最高的收集效率。

  它与前面讲的 CMS 垃圾收集器相比,有两个显著的改进:

  ①、采用 标记-整理 的回收算法

  这样不会产生空间碎片

  ②、可以精确的控制停顿时间

  能让使用者明确指定一个长度为M毫秒的时间片内,消耗在垃圾回收上的时间不超过 N 毫秒。

  ③、作用于整个Java堆

  G1收集器不区分年轻代和老年代,是整堆垃圾收集器。

回到顶部

9、ZGC 收集器

  这是JDK11发布的一款垃圾收集器,是一个可扩展的低延迟垃圾收集器,有如下特性:

  ①、暂停时间不超过10毫秒

  ②、暂停时间不会随堆或实时设置大小而增加

  ③、处理堆范围从几百M到几TB。

  看暂停时间,这是一款很逆天的垃圾收集器了,实际效果咋样,因为版本太高,博主也还没用到过。

回到顶部

10、如何选择垃圾收集器  

  详细文档可以查看官方介绍,如下

  docs.oracle.com/javase/8/do…

  这里我们翻译一下结论:

  除非应用程序有相当严格的暂停时间要求,否则就让JVM自己选择垃圾收集器。并且可以适当优先调整堆的大小来提高性能。如果还不满足要求,则以下面四点作为指导:

  1. 如果应用程序内存小于100M,那么使用选项选择串行收集器-XX:+UseSerialGC

  2. 如果应用程序将在单核处理器上运行,并且没有停顿时间的要求,选择串行-XX:+UseSerialGC或者 JVM 自己选

  3. 如果允许停顿时间超过1秒,选择并行或 JVM 自己选

  4. 如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保持短于大约1秒,则使用-XX:+UseConcMarkSweepGC或选择并发收集器-XX:+UseG1GC

回到顶部

11、几个名词解释

  ①、并行

  指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  适合科学计算、后台处理等弱交互场景。

  ②、并发

  指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户线程继续执行,而垃圾收集线程运行在另一块CPU上。

  适合对响应快速的场景,比如Web。

  ③、停顿时间

  垃圾收集器做垃圾回收中断应用执行的时间。

  ④、吞吐量

   吞吐量 = 运行用户代码的时间 / (运行用户代码的时间+垃圾收集时间)


Java虚拟机详解(五)------JVM参数(持续更新) 

目录

 


  JVM参数有很多,其实我们直接使用默认的JVM参数,不去修改都可以满足大多数情况。但是如果你想在有限的硬件资源下,部署的系统达到最大的运行效率,那么进行相关的JVM参数设置是必不可少的。下面我们就来对这些JVM参数进行详细的介绍。

  JVM参数主要分为以下三种(可以根据书写形式来区分):

回到顶部

1、标准参数

  标准参数,顾名思义,标准参数中包括功能以及输出的结果都是很稳定的,基本上不会随着JVM版本的变化而变化

  我们可以通过 -help 命令来检索出所有标准参数。

  

  关于这些命令的详细解释,可以参考官网:docs.oracle.com/javase/7/do…

  -help 也是一个标准参数,再比如使用比较多的 -version也是。

  ① 、-version

  显示Java的版本信息。

  

回到顶部

2、X 参数

  对应前面讲的标准化参数,这是非标准化参数。表示在将来的JVM版本中可能会发生改变,但是这类以 -X开始的参数变化的比较小。

  我们可以通过 Java -X 命令来检索所有-X 参数。

  

  关于这些参数的介绍,其实上图的中文解释很清楚了,这里我们不作过多的介绍。

回到顶部

3、XX参数

  这是我们日常开发中接触到最多的参数类型。这也是非标准化参数,相对来说不稳定,随着JVM版本的变化可能会发生变化,主要用于JVM调优和debug。

  注意:这种参数是我们后续介绍JVM调优讲解最多的参数。

  该参数的书写形式又分为两大类:

①、Boolean类型

  格式:-XX:[+-] 表示启用或者禁用name属性。

  例子:-XX:+UseG1GC(表示启用G1垃圾收集器)

②、Key-Value类型

  格式:-XX:= 表示name的属性值为value。

  例子:-XX:MaxGCPauseMillis=500(表示设置GC的最大停顿时间是500ms)

回到顶部

4、参数详解(持续更新)

  本节我们会持续更新罗列一些JVM参数。

1、打印已经被用户或者当前虚拟机设置过的参数

-XX:+PrintCommandLineFlags

  比如:

  

2、最大堆和最小堆内存设置

-Xms512M:设置堆内存初始值为512M

  -Xmx1024M:设置堆内存最大值为1024M

****这里的ms是memory start的简称,mx是memory max的简称,分别代表最小堆容量和最大堆容量。但是别看这里是-X参数,其实这是-XX参数,等价于:

-XX:InitialHeapSize

-XX:MaxHeapSize

****在通常情况下,服务器项目在运行过程中,堆空间会不断的收缩与扩张,势必会造成不必要的系统压力。所以在生产环境中,JVM的Xms和Xmx要设置成一样的,能够避免GC在调整堆大小带来的不必要的压力。  

3、Dump异常快照以及以文件形式导出

  -XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath

  堆内存出现OOM的概率是所有内存耗尽异常中最高的,出错时的堆内信息对解决问题非常有帮助,所以给JVM设置这个参数(-XX:+HeapDumpOnOutOfMemoryError),让JVM遇到OOM异常时能输出堆内信息,并通过(-XX:+HeapDumpPath)参数设置堆内存溢出快照输出的文件地址,这对于特别是对相隔数月才出现的OOM异常尤为重要。

  这两个参数通常配套使用:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./

4、发送OOM后,执行一个脚本

-XX:OnOutOfMemoryError

比如这样设置:

-XX:OnOutOfMemoryError="C:\Program Files\Java\jdk1.8.0_152\bin\jconsole.exe"

  表示发生OOM后,运行jconsole.exe程序。这里可以不用加“”,因为jconsole.exe路径Program Files含有空格。

  利用这个参数,我们可以在系统OOM后,自定义一个脚本,可以用来发送邮件告警信息,可以用来重启系统等等。

5、打印gc信息

①、打印GC简单信息

-verbose:gc

-XX:+PrintGC

****一个是标准参数,一个是-XX参数,都是打印详细的gc信息。通常会打印如下信息:

  

  比如第一行,表示GC回收之前有12195K的内存,回收之后剩余1088K,总共内存为125951K

②、打印详细GC信息

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

6、指定GC日志以文件输出

  -Xloggc:./gc.log

  这个在参数用于将gc日志以文件的形式输出,更方便我们去查看日志,定位问题。

7、设置永久代大小

  -XX:MaxPermSize=1280m

  在JDK1.7以及以前的版本中,只有Hotspot 才有Perm区,称为永久代,它在启动时固定大小,很难进行调优。

  在某些情况下,如果动态加载类过多,容易产生Perm区的 OOM。比如某个实际 Web 工程中,因为功能点较多,在运行过程中,要不断动态加载很多类,就会出现类似错误:

  "Exception in thread 'dubbo client x.x.connect' java.lang.OutOfMemoryError:PermGenspace"

  为了解决这个问题,就需要在项目启动时,设定运行参数-XX:MaxPermSize。

注意:在JDK1.8以后面的版本,使用元空间来代替永久代。在 JDK1.8以及后面的版本中,如果设定参数-XX:MaxPermSize,启动JVM不会报错,但是会提示:

Java Hotspot 64Bit Server VM warning:ignoring option MaxPermSize=1280m:support was removed in 8.0

 

8、垃圾收集器常用参数

  

 

 

参考文档:www.oracle.com/technetwork…

docs.oracle.com/javase/7/do…


Java虚拟机详解(六)------内存分配

目录

 


  我们说Java是自动进行内存管理的,所谓自动化就是,不需要程序员操心,Java会自动进行内存分配内存回收这两方面。

  前面我们介绍过如何通过垃圾回收器来回收内存,那么本篇博客我们来聊聊如何进行分配内存。

  对象的内存分配,往大方向上讲,就是堆上进行分配(但也有可能经过JIT编译后被拆散为标量类型并间接的在栈上分配),对象主要分配在新生代 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代上(下面会详细介绍),分配的规则并不是百分之百固定的,其细节取决于当前使用哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。

  本篇博客会介绍几条最普遍的内存分配规则。通过增加 -XX:+UseParallelGC 参数,表示使用的垃圾收集器是 Parallel Scavenge + Serial Old ,通过这两个垃圾收集器组合进行校验。

回到顶部

0、Minor GC 、Major GC 和 Full GC

  下面会出现这几个概念,所以这里首先介绍一下。

  ①、Minor GC

  也叫Young GC,指的是新生代 GC,发生在新生代(Eden区和Survivor区)的垃圾回收。因为Java对象大多是朝生夕死的,所以 Minor GC 通常很频繁,一般回收速度也很快。

  ②、Major GC

  也叫Old GC,指的是老年代的 GC,发生在老年代的垃圾回收,该区域的对象存活时间比较长,通常来讲,发生 Major GC时,会伴随着一次 Minor GC,而 Major GC 的速度一般会比 Minor GC 慢10倍。

  ③ 、Full GC

  指的是全区域(整个堆)的垃圾回收,通常来说和 Major GC 是等价的。  

回到顶部

1、对象优先在 Eden 上分配

  大多数情况下,对象优先在 Eden 上分配。当 Eden 区没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC(新生代GC)。

复制代码

package com.ys.algorithmproject.leetcode.demo.JVM;

/**
 * Create by YSOcean
 * 对象优先在Eden区上分配
 */
public class EdenTest {
    private static final int _1MB = 1024*1024;

    /**
     * 虚拟机参数设置:-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        byte[] a = new byte[2*_1MB];
        byte[] b = new byte[2*_1MB];
        byte[] c = new byte[2*_1MB];
        byte[] d = new byte[3*_1MB];
    }
}

复制代码

  运行时的虚拟机参数设置为:

-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

  ①、 -XX:+UseParallelGC 参数,表示使用的垃圾收集器是 Parallel Scavenge + Serial Old ;

  ②、-XX:+PrintGCDetails 参数,表示打印详细的GC日志,便于我们查看GC情况

  ③、-Xms20M -Xmx20M 这两个参数分别表示设置最大堆,最小堆内存都是20M

  ④、-Xmn 参数表示设置新生代大小为 10M

  ⑤、-XX:SurvivorRatio=8 新生代中的 Eden 区和 Survivor 区的比值为8:1,注意 Survivor是有两个的。

  运行打印的GC日志为:

  我们首先分析设置的JVM参数,表示堆中内存为20M,新生代和老年代分别各占一半为10M,并且新生代的Eden区为8M,剩下两个 Survivor 各为 1M。

  在看代码,首先分配了三个大小都为2M的对象 a,b,c。这时候新生代对象的 Eden区已经被占用了6M,这时候来了一个对象d,大小为3M,发现新生代Eden区已经不足以分配对象d了,于是发起一次Minor GC。GC期间虚拟机又发现现在已有3个 2MB对象无法全部放入Survivor空间(Survivor空间只有1MB),所以只好通过分配担保机制提前转移到老年代中,然后将这个对象d分配到新生代Eden区中。

  我们查看日志,在eden区中,总共8192K的空间,被使用了38%,约等于3113K,大概就是对象d(3MB)的大小。其次在老年代中,总共10240K(10MB),被使用了6865K,大概也就是a,b,c这三个对象的大小(6MB)。

回到顶部

2、大对象直接进行老年代

  通常大对象是指需要大量连续内存空间的Java对象,比较典型的就是那种很长的字符串以及数组。

  系统中出现大量大对象是很影响性能的,这样会导致还有不少空间时就提前触发垃圾回收来放置这些对象。

复制代码

package com.ys.algorithmproject.leetcode.demo.JVM;

/**
 * Create by YSOcean
 * 大对象直接在老年代上分配
 */
public class OldTest {
    private static final int _1MB = 1024*1024;

    /**
     * 虚拟机参数设置:-XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        byte[] a = new byte[8*_1MB];

    }
}

复制代码

  运行时虚拟机参数还和上面一样,运行的GC日志如下:

  

  可以看到老年代 ParOldGen直接被使用了 8192K,而新生代只被占用了1820K。

  PS:可以通过设置-XX:PretenureSizeThreshold 参数,大于这个参数设置值的对象直接在老年代中分配,但是这个参数只对 Serial 和 ParNew 这两款垃圾收集器有效,Parallel Scavenge 收集器不认识这个参数。

回到顶部

3、长期存活的对象将进入老年代

   我们知道Java虚拟机是通过分代收集的思想来管理内存,新创建的对象通常放在新生代,除此之外,还有一些对象放在老年代。为了识别哪些对象放在新生代,哪些对象放在老年代,虚拟机给每个对象定义了一个年龄计数器(Age),如果对象在新生代Eden创建,并经历一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,虚拟机会将该对象移动到 Survivor 区域,并将对象的年龄Age+1。

  新生代对象每熬过一次 Minor GC,年龄就增加1,当它的年龄增加到一定阈值时(默认是15岁),就会被晋升到老年代中。

  这个年龄阈值可以通过如下参数来设置(N表示晋升到老年代的阈值):

-XX:MaxTenuringThreshold=N

  验证代码如下:

复制代码

package com.ys.algorithmproject.leetcode.demo.JVM;

/**
 * Create by YSOcean
 * 新生代对象经过N次Minor GC后,晋升到老年代
 */
public class OldAgeTest {
    private static final int _1MB = 1024*1024;

    /**
     * 虚拟机参数设置:-XX:MaxTenuringThreshold=1 -XX:+UseParallelGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        byte[] a = new byte[_1MB];
        System.gc();

    }

}

复制代码

  注意:这里我们设置 -XX:MaxTenuringThreshold=1,也就是经历一次gc,新生代对象就直接进入老年代了,然后手动调用了 System.gc() 方法,表示让虚拟机进行垃圾回收。打印的日志如下:

  

  注意看,代码中我们只创建了一个 1MB大小的对象,但是老年代占用了1999K的内存,而新生代确只有246K。

  接下来可以将 -XX:MaxTenuringThreshold 参数设置的更大一点,来对比打印的日志,这里读者可以自己进行验证。

回到顶部

4、新生代Survivor 区相同年龄所有对象之和大于 Survivor 所有对象之和的一半,大于等于该年龄的对象进入老年代

  Java虚拟机并不会死板的根据上面第3点说的,设置-XX:MaxTenuringThreshold 的阈值,只有对象经历该阈值次GC后,才会进入到老年代。而是会根据新生代对象的年龄来动态的决定哪些对象可以进入到老年代。

  也就是说,新生代经历一次 Minor GC 后,Survivor 区域存活对象的所有相同年龄之和大于整个 Survivor 区域的所有对象之和,那么该区域大于等于这个年龄的对象就会进入老年代,而无需等到 -XX:MaxTenuringThreshold 设置的阈值。

回到顶部

5、空间分配担保原则

  在前面介绍 垃圾回收 时,我们介绍过现在Java虚拟机采用的是分代回收算法,新生代采用复制收集算法,而老年代采用标记整理,或者标记清除算法。

  

  新生代内存分为一块 Eden区,和两块 Survivor 区域,当发生一次 Minor GC时,虚拟机会将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,通常情况下,Java对象朝生夕死,一块 Survivor 区域是能够存放GC后剩余的对象的,但是极端情况下,GC后仍然有大量存活的对象,那么一块 Survivor 区域就会存放不下这么多的对象,那么这时候就需要老年代进行分配担保,让无法放入 Survivor 区域的对象直接进入到老年代,当然前提是老年代还有空间能够存放这些对象。但是实际情况是在完成GC之前,是不知道还有多少对象能够存活下来的,所以老年代也无法确认是否能够存放GC后新生代转移过来的对象,那么这该怎么办呢?

  前面我们介绍的都是Minor GC,那么何时会发生 Full GC?

  在发生 Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则改为 Full GC。如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则也要进行一次 Full GC。

-XX:-HandlePromotionFailure

  回到第一个问题,老年代也无法确认是否能够存放GC后新生代转移过来的对象,那么这该怎么办呢?

  也就是取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,然后与老年代剩余空间进行比较,来决定是否进行 Full GC,从而让老年代腾出更多的空间。

  通常情况下,我们会将 HandlePromotionFaile 设置为允许担保失败,这样能够避免频繁的发生 Full GC。