Java中,只有GC Roots以及被它们强引用到的对象不会被GC回收,GC Roots大概分几类:
1. 栈对象:也就是函数中使用或创建的对象,存于栈上的时刻恰好是正在被使用的时刻,自然不能被GC;
2. 静态对象:经常是很多其它对象引用关系的根节点,生命周期至少从第一次使用的那一刻起,到JVM结束;
3. 常量对象:逻辑上需要认为是从JVM起,到JVM死都不变的对象;
4. JNI引用的对象:生命周期由代码逻辑控制,档案关系并不在java的一亩三分地上;
Spring的IOC容器本质上是一个Map,通过key-value形式存储了各个单例类,而该容器则被servlet的成员变量:servetcontext所引用,只要servlet不回收,servletcontext就不会回收,spring自然也不会回收。
1. 基础知识
1.1 什么是jvm

- JVM俗称Java虚拟机,是一个运行在操作系统之上的软件,本身不会与计算机硬件有直接的交互。
- Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。
- Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
- Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。
1.2 jvm体系图

- 灰色方块是处于CPU寄存器中的,因此内存占用特别小,不会被GC垃圾回收期回收。
- 橙色方块处于内存中,占用较大,所以垃圾回收器主要就是为这部分东西工作。
灰色区域是私有的,橙色部分则是共享的,这也就是为什么垃圾回收器要回收这部分区域,因为一个资源被占用,会导致其他成员无法使用该资源。
1.3 类加载器 ClassLoader

类加载器负责将Java字节码文件加载进虚拟机中,以备执行。
- 类加载器通过字节码文件中cafe babe的开头以及后续的jvm规范来识别文件,以防止造假行为,这个验证是非常严格的,如下图。

- 确认了字节码文件后,类加载器会将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎决定。
类加载器的具体结构如下图:
- 加载器也是一个类,是可以编写的。
- 虚拟机自带的加载器 启动类加载器(Bootstrap)C++编写,这里也可以体现出Java语言是类C语言。
- 扩展类加载器(Extension)Java编写,Java语言后续所有的扩展包都会由该加载器加载,比如javax.*就是常见的扩展包。
- 应用程序类加载器(AppClassLoader)Java编写,也叫系统类加载器,加载当前应用的classpath的所有类。
- 用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式,自定义加载器只需继承上述的基础加载器即可。
- Object是所有java类的父类,他是Java中自带的类,在java.lang包下,之所以说Java是面向对象的语言,就是因为这个缘故,同样的,启动类加载器也是jvm中最顶层的加载器,由于他是用c++语言编写的,所以启动类加载器在控制台输出会是null。
- JDK安装目录下的.**\jre\lib\rt.jar**文件,这个jar包就是java语言最基础的类包,这个包中的类都没有父类。
sun.misc.Launcher是一个java虚拟机的入口应用
双亲委派机制
假如一个用户自定义了一个和jdk自带类同名、同路径的类,虚拟机要如何避免源代码被污染?Java使用来双亲委派机制避免该问题:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求最终都应该传送到启动类加载器**(Bootstrap)**中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
- 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。
这个机制可以总结成一句话:Java中的任意类加载都是从顶层加载器开始的,先到先得,后到没有。这样就避免了源码污染,因此Java是能保证类的安全性的。当然,实际编程中也应该避免使用保留类名。
1.4 执行引擎 Execution Engine
执行引擎负责解释Java字节码中的命令,转译成计算机能识别的语言,提交给操作系统执行。
1.5 本地方法栈Native Method Stack
Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,因此出现来本地接口Native Interface,其作用是融合不同的编程语言为 Java 所用。
- 具体做法是在Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
- 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
- 例如,多线程是系统级的方法,Java想支持多线程就必须使用C语言或者C++来直接操作计算机内存和处理器(JVM本身不直接交互硬件),因此,在java.lang.Thread类中可以发现一个带native声明的方法,这个方法实际上就是代表了将任务交由其他语言执行。
PC寄存器
- 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
- 这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 如果执行的是一个Native方法,那这个计数器是空的。
寄存器中的部分用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会中断,更不会发生内存溢出(OutOfMemory=OOM)错误
1.6 方法区 Method Area
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时的常量池(Runtime Constant Pool)、类中定义的字段和方法数据、构造函数和普通函数的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。
但是,实例变量是存在堆内存中的,和方法区无关,实例变量是类在实例化后的成员字段,所以不同的实例化类中的字段属性值不会发生混淆。
1.7 栈Stack
什么是栈
栈也叫栈内存,主管Java程序的运行,在线程创建时创建,它的生命期是跟随线程的生命期的,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就会释放,其生命周期和线程一致,是线程私有的。8种基本类型的变量、对象的引用变量、实例方法都是在栈内存中分配。
- 栈帧中主要保存3 类数据:
| 本地变量(Local Variables) | 输入参数和输出参数以及方法内的变量 |
|---|---|
| 栈操作(Operand Stack) | 记录出栈、入栈的操作 |
| 栈帧数据(Frame Data) | 包括类文件、方法等等 |
- 栈中的数据遵循先进后出,后进先出规则。
- 总结:栈管运行,所以调试程序打断点的时候,实际上是中断栈区
运行原理
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。
当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
- A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈
- B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈
- ……
- 执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。
为什么方法执行要在栈中执行呢?因为栈是先进后出的,比如,在A方法中调用了B方法,A肯定先执行,B再执行,如果不用栈,那么就不能A方法可以执行到最后,如果用了队列这样的先进先出结构,A方法会在B调用完成的时候就退出执行,那么程序必然故障。

栈溢出
任何硬件的存储空间都是有限的,栈也不例外,如果在栈中不断的塞入数据,但又不释放,就会造成栈溢出的错误。在Java中,如果程序导致了栈溢出,就会抛错:Exception in thread “main” java.lang.StackOverflowError
假如我们写了一个永不休止的递归方法,那么这个错误就会抛出。
堆、栈和方法区的关系
- 上边提到过,实例变量是存在堆内存中的,和方法区无关
**
// 虽然a和b都实例化了一个Thread类,但他们的属性值会生成两份不同副本,存放在堆中val a = Thread()val b = Thread()
- 如图,虽然a、b都是Thread类的实例化对象,但他们的字段属性是存在堆中的,是两份独立的副本

- 元数据可理解为类的模版,标识这这个类的成员属性在Java中被记作的数据类型。
- Java封装了c语言中难以理解的指针,当你新建一个类的实例化对象时,实际上是在堆中新建了一份该类的副本,你给对象起的名字就是这个副本的指针。
方法区又存储来该对象所属类的信息,从中可以分辨这个对象是从哪个包下的哪个类中实例化的。
1.8 闭包和函数式编程
一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号“{}”来表示闭合,并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包。
- 函数存在作用域,不同函数之间的同名变量是互相隔离、互不干扰的。
- 闭包也是一个函数,并且是能够读取其他函数内部变量的函数。
- 上边介绍过栈内存里存放着方法,但由于方法区中的数据有独立性,Java在版本8之前都无法解决闭包问题,即函数内部的变量不能传递给调用该函数的父级,例如AB方法都有一个变量x,A的内部调用了B,二者同时持有x变量,但由于AB方法执行时是在栈里的不同区域,而栈内部的数据又不是共享的,因此A永远也不能在内部得到B中的x的值。
- 在jdk8之前,我们只能通过全局变量的形式去实现不同方法间变量共享,因为全局变量是存在堆中的(类的属性副本),jdk8引入Lambada表达式后,Java也就支持了闭包。
一个语言只有实现了闭包才能实现函数式编程,Lambada表达式就是典型的闭包。
1.9 堆Heap
什么是堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
Young generation space — 新生区:Young/New
- Tenure generation space — 养老区:Old/ Tenure
- Permanent Space —
永久区:Perm→ Java8更换为元空间
堆内存逻辑上分为三部分:新生区+养老区+元空间**:

- 新生区是类的诞生、成长、消亡的区域,一个类在这里被实例化、调用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被实例化的。幸存区有两个: 0区(Survivor 0 space 别名 from区)和1区(Survivor 1 space 别名 to区)。
- 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC — 轻GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC — 重GC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM堆内存溢出异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:Java虚拟机的堆内存设置不够,可以通过参数-Xms初始堆内存、-Xmx最大堆内存来调整;其次就是代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
基本数据类型和引用数据类型
Java是区分数据类型的,int、double、string等就是基础数据类型,我们的class类就是引用数据类型。
- 为什么这么区分呢?因为基础数据是存在栈内存的,在传递时,不会传递原数据,而是新建一份原数据的副本,而实例化的对象则是存在堆内存的,我们在传递对象属性时,会实打实的把堆内存中的数据副本传递过去。
- 所以,在java中会出现以下结果:

- 有一个特例,String,Java中的字符串是放在常量池的,即便我们以String xx = new String(“xx”)的形式新建了一个字符串,他的指针也会间接的指向常量池,比较反人类
- Java中用包装类来解决基础数据传值问题,kotlin则根本上去除了基础数据类型, 全部原生为引用类型
2. GC垃圾回收
2.1 回收机制


垃圾回收机制:**复制→清空→互换
- eden、SurvivorFrom 复制到 SurvivorTo,年龄+1。首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1。
- 清空 eden、SurvivorFrom。然后,清空Eden和SurvivorFrom中的对象,同时重新根据剩余空间大小来交换from和to的角色,也就是谁空谁变to。
- SurvivorTo和 SurvivorFrom 互换。最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。**
Sun HotSpot™内存管理


- 实际上,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
- 对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
- 永久代(java7之前有) 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
Minor GC和Full GC的区别普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
- 全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上
2.2 回收算法
2.2.1 垃圾收集算法
标记-清除算法(Mark-Sweep)
“标记-清除”算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它主要由两个缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(Copying,针对新生代)

为了解决标记清除算法的效率问题,出现了复制算法,它将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。优点是每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点是将内存缩小为原来的一半,代价太高了一点。
现在的商业虚拟机都采用复制收集算法来回收新生代,有研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当然,并不能保证每次回收都只有10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。即如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。
标记-整理算法(Mark-Compact,针对老年代)

复制收集算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制收集算法。<br /> 根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是**让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存**。
分代收集算法(Generational Collection)
当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无新的方法,只是根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
2.2.2 七种垃圾收集器
Serial收集器(新生代)
单线程,在进行垃圾收集时必须暂停其他所有的工作线程(”Stop the World”)。虚拟机运行在Client模式下的默认新生代收集器。简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程效率。
ParNew收集器(新生代)
ParNew收集器其实是Serial收集器的多线程版本,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器(“吞吐量优先”收集器,新生代)
使用复制算法,并行多线程,这些特点与ParNew一样,它的独特之处是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,吞吐量就是99%。<br /> 停顿时间越短对于需要与用户交互的程序来说越好,良好的响应速度能提升用户的体验;高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不太需要太多交互的任务。**
Serial Old收集器(老年代)
它是Serial收集器的老年代版本,单线程,使用“标记-整理”算法。主要意义是被Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;作为CMS 收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。运行过程同Serial收集器。
Parallel Old收集器(老年代)
它是Parallel Scavenge收集器的老年代版本,多线程,使用“标记-整理”算法。在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器。
CMS收集器(Concurrent Mark Sweep)
它是一种以获取最短回收停顿时间为目标的收集器。优点:并发收集,低停顿。基于“标记-清除”算法。目前很大一部分Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。运作过程较复杂,分为4个步骤:
- 初始标记(CMS initial mark):需要“Stop The World”,标记GC Roots能直接关联到的对象,速度快。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing 过程
- 重新标记(CMS remark):需要“Stop The World”,修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。停顿时间:初始标记<重新标记<<并发标记
- 并发清除(CMS concurrent sweep):时间较长。
缺点:
- 对CPU资源非常敏感,面向并发设计的程序都会对CPU资源较敏感。CMS默认的回收线程数: (CPU数量+3)/4
- 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。并发清理阶段用户程序运行产生的垃圾过了标记阶段所以无法在本次收集中清理掉,称为浮动垃圾。CMS收集器默认在老年代使用了68%的空间后被激活。若老年代增长的不是很快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction 提高触发百分比,但调得太高会容易导致“Concurrent Mode Failure”失败。
基于“标记-清除”算法会产生大量空间碎片。提供开关参数-XX:+UseCMSCompactAtFullCollection 用于在“ 享受”完Full GC服务之后进行碎片整理过程,内存整理的过程是无法并发的。但是停顿时间会变长。
G1收集器(Garbage First)
它是当前收集器技术发展的最前沿成果,也是目前的jdk所使用的收集器。与CMS相比有两个显著改进:
基于“标记-整理”算法实现收集器
- 非常精确地控制停顿
G1收集器可以在几乎不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。区域划分、有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
3. JVM调优
3.1 Java8的元空间
**
- 在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
- 元空间与永久代之间最大的区别在于:永久代使用JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
- 因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory元空间, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
- JVM**实际使用的内存 =堆内存 + 元空间
为什么要用元空间替代方法区
- JDK1.8以前的HotSpot JVM有方法区,也叫永久代(permanent generation)。
- 方法区用于存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码。
- 方法区是一片连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。
- 永久代的GC是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
- JDK1.7开始了方法区的部分移除:符号引用(Symbols)移至native heap,字面量(interned strings)和静态变量(class statics)移至java heap。
- 随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
3.2 Spring IOC如何保证对象不会被垃圾回收?
JVM通过可达性分析来判定对象是否存活。这个算法的基本思路就是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
而在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
根据Spring源码,IOC本质上是一个final修饰的map,属于情况3:方法区中常量引用的对象,因此容器中的类存储于方法区,而不是堆内存,不会被GC垃圾回收。
// SimpleJndiBeanFactory.java:public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFactory {private final Set<String> shareableResources = new HashSet();//使用new创建的map,是栈中引用的对象,可作为GC Roots 对象private final Map<String, Object> singletonObjects = new HashMap();private final Map<String, Class<?>> resourceTypes = new HashMap();}
3.3 JVM发展史
提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。 但不一定所有人都知道的是,这个目前看起来“血统纯正”的虚拟机在最初并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的; 甚至这个虚拟机最初并非是为Java语言而开发的,它来源于Strongtalk VM, 而这款虚拟机中相当多的技术又是来源于一款支持Self语言实现“达到C语言50%以上的执行效率”的目标而设计的虚拟机, Sun公司注意到了这款虚拟机在JIT编译上有许多优秀的理念和实际效果,在1997年收购了Longview Technologies公司,从而获得了HotSpot VM。
HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势, 如它名称中的HotSpot指的就是它的热点代码探测技术(其实两个VM基本上是同时期的独立产品,HotSpot还稍早一些,HotSpot一开始就是准确式GC, 而Exact VM之中也有与HotSpot几乎一样的热点探测。 为了Exact VM和HotSpot VM哪个成为Sun主要支持的VM产品,在Sun公司内部还有过争论,HotSpot打败Exact并不能算技术上的胜利), HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码, 并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。 Oracle公司宣布在不久的将来(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工作,使之优势互补。 整合的方式大致上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务, 使用HotSpot的JIT编译器与混合的运行时系统。
3.4 JVM优化基础
Java提供了Runtime类来实现jvm虚拟机的查询功能,我们可以使用如下代码获取jvm的内存配置:
fun contextLoads() {// 查询虚拟机最大使用内存,字节转MBprintln(Runtime.getRuntime().maxMemory().div(1024 * 1024))// 查询虚拟机初始使用的总内存,字节转MBprintln(Runtime.getRuntime().totalMemory().div(1024 * 1024))}
运行效果:
- 可以看到,最大使用内存是3611MB,初始使用243MB。初始内存默认会比最大内存小很多,但是实际使用时候,建议将初始内存和最大内存调整成一样,这样可以防止程序运行时内存忽高忽低,GC垃圾回收器和堆栈互抢内存空间,出现一些诡异的异常。
- 通过程序发现默认的情况下分配的内存是总内存的1 / 4、而初始化的内存为总内存的1 / 64,我的电脑内存是标称的16G,但实际应该会比这个少,因此最大内存算的3.5G左右
通过IDEA配置jvm
IDEA提供了非常方便的界面化配置工具,我们可以直接打开运行配置,在options选项中输入以下命令来配置内存大小,由于上一步我们的最大内存是3611m,这次也就把初始内存设为3611m,注意,同一命令内不能出现空格:

分析GC垃圾回收
现在,运行一段能很快导致堆溢出的代码:
fun contextLoads() {var str = "aaaaaaaaa"while (true) {str += str + Random.nextInt(88888888) + Random.nextInt(999999999)}}
运行结果如下:
可以看出,一开始jvm通过轻GC回收垃圾,然后开始重GC Full来回收,直到full gc都无法回收后报出堆内存溢出错误。
现在,我们可根据日志内容来分析GC垃圾回收:
[GC (Allocation Failure):GC回收失败的标识
[PSYoungGen: **885302K->54616K(1078784K)] 885302K->263508K(3544576K**), 0.1404797 secs]:
- PSYoungGen代表GC发生错误的位置,这里是新生代,885302K是发回收前的新生代大小,54616K是回收后的新生代大小,1078784K是新生代总大小,下一个885302K是回收前JVM堆内存占用总大小。
- 885302K是回收前JVM堆内存大小,263508K是回收后JVM堆内存大小,3544576K是JVM堆内存总大小,这个大小和我们刚才分配的大小3411m是一致的。
[Times: user=0.84 sys=0.09, real=0.14 secs]
- user是用户程序执行耗时
- sys是系统耗时
- 0.1404797 secs是GC回收的总耗时

方括号 [ ] 包裹的部分是堆内存中的各个区域大小统计,无方括号的则是堆内存的统计。通过这些数据我们可以精准确定堆内存是在什么区域发生溢出的
JVM命令集合
-Xmx3550m:设置JVM 最大堆内存为3550M。
- -Xms3550m:设置JVM 初始堆内存为3550M此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
- -Xss128k:。设置每个线程的栈大小.JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K应当根据应用的线程所需内存大小进行调整在相同物理内存下,。减小这个值能生成更多的线程但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右需要注意的是:当这个值被设置的较大(例如> 2MB)时将会在很大程度上降低系统的性能。
- -Xmn2g:设置年轻代。大小为2G在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。
- -XX:NewSize = 1024m:设置年轻代初始值为1024M。
- -XX:MaxNewSize = 1024m:设置年轻代最大值为1024M。
- -XX:PermSize = 256m:设置持久代初始值为256M。
- -XX:MaxPermSize参数参数= 256米:设置持久代最大值为256M。
- -XX:NewRatio = 4:设置年轻代(包括1个伊甸和2个幸存者区)与年老代的比值表示年轻代比年老代为1:4。
- -XX:SurvivorRatio = 4:设置年轻代中伊甸区与幸存者区的比值。表示2个幸存者区(JVM堆内存年轻代中默认有2个大小相等的幸存者区)与1个伊甸区的比值为2:4,即1个幸存者区占整个年轻代大小的1/6。
- -XX:MaxTenuringThreshold = 7:表示一个对象如果在幸存者区(救助空间)移动了7次还没有被垃圾回收就进入年老代如果设置为0的话,则年轻代对象不经过幸存者区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在幸存者区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。
1、-Xmn,-XX:NewSize / -XX:MaxNewSize,-XX:NewRatio 3组参数都可以影响年轻代的大小,混合使用的情况下,优先级是什么?
- 高优先级:-XX:NewSize/ -XX:MaxNewSize
- 中优先级:-Xmn(默认等效-Xmn = -XX:NewSize = -XX:MaxNewSize =?)
- 低优先级:-XX:NewRatio
- 推荐使用-Xmn参数,原因是这个参数简洁,相当于一次设定NewSize / MaxNewSIze,而且两者相等,适用于生产环境。-Xmn配合-Xms / -Xmx,即可将堆内存布局完成。
- -Xmn参数是在JDK 1.4开始支持。
3.5 如何选择垃圾回收器?
jdk5以后的版本会根据系统环境自动选择合适的GC,所以绝大部分情况都无需额外配置。
并行收集器(吞吐量优先)
- -XX:+ UseParallelGC:。设置为并行收集器此配置仅对年轻代有效即年轻代使用并行收集,而年老代仍使用串行收集。
-XX:ParallelGCThreads = 20:配置并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收此值建议配置与CPU数目相等。
- -XX:+ UseParallelOldGC:配置年老代垃圾收集方式为并行收集.JDK6.0开始支持对年老代并行收集。
- -XX:MaxGCPauseMillis = 100:设置每次年轻代代垃圾回收的最长时间(单位毫秒)如果无法满足此时间,JVM会自动调整年轻代大小,以满足此时间。
-XX:+ UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动调整年轻代伊甸区大小和幸存者区大小的比例,以达成目标系统规定的最低响应时间或者收集频率等指标此参数建议在使用并行收集器时,一直打开。
并发收集器(响应时间优先)
-XX:+ UseConcMarkSweepGC:即CMS收集,设置年老代为并发收集的.cms收集是JDK1.4后期版本开始引入的新GC算法它的主要适合场景是对响应时间的重要性需求大于对吞吐量的需求,能够承受垃圾回收线程和应用线程共享CPU资源,并且应用中存在比较多的长生命周期对象的的的.cms收集的目标是尽量减少应用的暂停时间,减少全GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存。
- -XX:+ UseParNewGC:设置年轻代为并发收集可与CMS收集同时使用.JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此参数。
- -XX:CMSFullGCsBeforeCompaction = 0:由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间并行收集以后会产生内存碎片,内存使用效率降低。此参数设置运行0次Full GC后对内存空间进行压缩和整理,即每次Full GC后立刻开始压缩和整理内存。
- -XX:+ UseCMSCompactAtFullCollection:打开内存空间的压缩和整理,在Full GC后执行。可能会影响性能,但可以消除内存碎片。
- -XX:+ CMSIncrementalMode:设置为增量收集模式一般适用于单CPU情况。
-XX:CMSInitiatingOccupancyFraction = 70:表示年老代内存空间使用到70%时就开始执行CMS收集,以确保年老代有足够的空间接纳来自年代代的对象,避免Full GC的发生。
其它垃圾回收参数
-XX:+ ScavengeBeforeFullGC:年轻代GC优于全GC执行。
- -XX:-DisableExplicitGC:不响应System.gc()的的的代码。
- -XX:+ UseThreadPriorities:启用本地线程优先级API即使生效,不启用
java.lang.Thread.setPriority()则无效。 - -XX:SoftRefLRUPolicyMSPerMB = 0:软引用对象在最后一次被访问后能存活0毫秒(JVM默认为1000毫秒)。
-XX:TargetSurvivorRatio = 90:允许90%的幸存者区被占用(JVM默认为50%)提高对于幸存者区的使用率。
辅助信息参数设置
-XX:-CITime:打印消耗在JIT编译的时间。
- -XX:错误文件= / hs_err_pid.log:保存错误日志或数据到指定文件中。
- -XX:HeapDumpPath = / java_pid.hprof:指定转储堆内存时的路径。
- -XX:-HeapDumpOnOutOfMemoryError:当首次遭遇内存溢出时卸出此时的堆内存。
- -XX:的OnError =“;”:出现致命错误后运行自定义命令。
- -XX:OnOutOfMemoryError =“;”:当首次遭遇内存溢出时执行自定义命令。
- -XX:-PrintClassHistogram:按下Ctrl + Break后打印堆内存中类实例的柱状信息,同JDK的jmap -histo命令。
- -XX:-PrintConcurrentLocks:按下Ctrl + Break后打印线程栈中并发锁的相关信息,同JDK的jstack -l命令。
- -XX:-PrintCompilation:当一个方法被编译时打印相关信息。
- -XX:-PrintGC:每次GC时打印相关信息。
- -XX:-PrintGCDetails:每次GC时打印详细信息。
- -XX:-PrintGCTimeStamps:打印每次GC的时间戳。
- -XX:-TraceClassLoading:跟踪类的加载信息。
- -XX:-TraceClassLoadingPreorder:跟踪被引用到的所有类的加载信息。
- -XX:-TraceClassResolution:跟踪常量池。
- -XX:-TraceClassUnloading:跟踪类的卸载信息。
关于参数名称等
- 标准参数( - ),所有JVM都必须支持这些参数的功能,而且向后兼容;例如:
- -client - 设置JVM使用客户端模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。
- -server - 设置JVM使服务器模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。
- 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;
非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用;
4. 调优示例方案
4.1 大型网站服务器案例
承受海量访问的动态的网络应用
服务器配置:8 CPU,8G MEM,JDK 1.6.X
参数方案:
-server -Xmx3550m -Xms3550m -Xmn1256m -Xss128k -XX:SurvivorRatio = 6 -XX:MaxPermSize = 256m -XX:ParallelGCThreads = 8 -XX:MaxTenuringThreshold = 0 -XX:+ UseConcMarkSweepGC
调优说明:-Xmx与-Xms相同以避免JVM反复重新申请内存。-XMX的大小约等于系统内存大小的一半,即充分利用系统资源,又给予系统安全运行的空间。
- -Xmn1256m,设置年轻代大小为1256MB。此值对系统性能影响较大,sun官方推荐配置年轻代大小为整个堆的3/8。
- -Xss128k,设置较小的线程栈以支持创建更多的线程,支持海量访问,并提升系统性能。
- -XX:SurvivorRatio = 6,设置年轻代中Eden区与Survivor区的比值。系统默认是8,根据经验设置为6,则2个幸存者区与1个Eden区的比值为2:6,一个幸存者区占整个年轻代的1/8。
- -XX:ParallelGCThreads = 8,配置并行收集器的线程数,即同时8个线程一起进行垃圾回收。此值一般配置为与CPU数目相等。
- -XX:MaxTenuringThreshold = 0,设置垃圾最大年龄(在年轻代的存活次数)。如果设置为0的话,则年轻代对象不经过Survivor区直接进入年老代。对于年老代比较多的应用,可以提高效率;如果将此值设置为一个较大值,则年轻代对象会在幸存者区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率根据被海量访问的动态网络应用之特点,其内存要么被缓存起来以减少直接访问数据库,要么被快速回收以支持高并发海量请求,因此其内存对象在年轻代存活多次意义不大,可以直接进入年老代,根据实际应用效果,在这里设置此值为0。
-XX:+UseConcMarkSweepGC,设置年老代为并发收集。CMS(ConcMarkSweepGC)收集的目标是尽量减少应用的暂停时间,减少完全GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存,适用于应用中存在比较多的长生命周期对象的情况。
4.2 内部集成构建服务器案例
高性能数据处理的工具应用
服务器配置:1个CPU,4G MEM,JDK 1.6.X
参数方案:
-server -XX:PermSize = 196m -XX:MaxPermSize = 196m -Xmn320m -Xms768m -Xmx1024m
调优说明:-XX:PermSize = 196m -XX:MaxPermSize = 196m,根据集成构建的特点,大规模的系统编译可能需要加载大量的Java类到内存中,所以预先分配好大量的持久代内存是高效和必要的。
- -Xmn320m,遵循年轻代大小为整个堆的3/8原则。
- -Xms768m -Xmx1024m,根据系统大致能够承受的堆内存大小设置即可。
在64位服务器上运行应用程序,构建执行时,用jmap -heap 11540命令观察JVM堆内存状况如下:附加到进程ID 11540,请稍候...调试程序已成功附加。服务器编译检测到``使用线程局部对象分配的JVM版本是20.12-b01 。具有4个线程的并行GC
堆配置
Heap Configuration:MinHeapFreeRatio = 40MaxHeapFreeRatio = 70MaxHeapSize = 1073741824(1024.0MB)NewSize = 335544320(320.0MB)MaxNewSize = 335544320(320.0MB)OldSize = 5439488(5.1875MB)NewRatio = 2SurvivorRatio = 8PermSize = 205520896(196.0MB)MaxPermSize = 205520896(196.0MB)
堆使用情况
Heap Usage:PS Young GenerationEden Space:capacity = 255852544 (244.0MB)used = 1433824101395504 (96.69828796386719MB)free = 154457040 (147.3017120361328MB)39.63044588683081% usedFrom Space:capacity = 34144256 (32.5625MB)used = 33993968 (32.41917419433594MB)free = 150288 (0.1433258056640625MB)99.55984397492803% usedTo Space:capacity = 39845888 (38.0MB)used = 0 (0.0MB)free = 39845888 (38.0MB)0.0% usedPS Old Generationcapacity = 469762048 (448.0MB)used = 44347696 (42.29325866699219MB)free = 425414352 (405.7067413330078MB)9.440459523882184% usedPS Perm Generationcapacity = 205520896 (196.0MB)used = 85169496 (81.22396087646484MB)free = 120351400 (114.77603912353516MB)41.440796365543285% used
从控制台信息可以发现,优化后的JVM是比较健康的。

