JVM学习笔记

                                                    JVM学习笔记

一.什么是JVM?

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。

Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。

Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。其中垃圾收集模块在Java虚拟机规范中并没有要求Java虚拟机垃圾收集,但是在没有发明无限的内存之前,大多数JVM实现都是有垃圾收集的。而运行时数据区都会以某种形式存在于每一个JAVA虚拟机实例中,但是Java虚拟机规范对它的描述却是相当抽象。这些运行时数据结构上的细节,大多数都由具体实现的设计者决定。

二.JVM各模块详解

Java源文件通过javac编译成java字节码文件.class,通过类加载器ClassLoader加载.class文件,将class文件放入内存中各个区域

1).类加载器

1.什么是类加载器?

       负责加载class文件(class文件在文件开头有特定的文件标识)将class文件字节码内容加载到内存中,
并将这些内容转换成方法区(Jdk1.7)中的运行时数据结构并且ClassLoader只负责class文件的加载,
至于它是否可以运行,则由执行引擎(Execution Engine)决定

2.类加载器都有哪些?

类加载器
虚拟机自带加载器
1.启动类加载器BootStrap
2.扩展类加载器Extension
3.应用程序类加载器AppClassLoader
用户自定义加载器
Java.lang.ClassLoader的子类,用户可以定制类的加载方式

3.双亲委派原则?

工作流程:
       如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,
而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,
因此所有的加载请求最终都应该传送到顶层的启动类加载器中,
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

4.沙箱安全?

使用双亲委派模型来组织类加载器之间的关系,好处就是Java类随着他的类加载器一起具备了一种带有优先级的层次关系,例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,保证了不会出现用户自己编写了一个Object类加载器去加载此类,造成应用环境的混乱。

2).运行时数据区

1.程序计数器

又称PC寄存器 
       每个线程都有一个程序计数器,是线程私有的,它是一个指针,指向方法区的方法字节码(用来存储指向下一条指令的地址,也即要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
       这块内存区域很小,它是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
       如果执行的是一个Native方法,那这个计数器是空的。
       用于完成分支、循环、跳转、异常处理、线程恢复等基础功能,不会发生内存溢出(OOM)错误。

2.Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用知道执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。(子弹和子弹夹的关系)先进后出。
栈的大小和具体的JVM的实现有关,通常在256K~~756K之间,约等于1Mb左右。在Java虚拟机规范中,对这个区域规定了两种异常情况:
      如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverfloError异常;
      如果虚拟机栈可以动态扩张,扩张时无法申请到足够的内容,就会抛出OutOfMemoryError异常。

下图即表示1个线程在内存中的结构

栈运行原理:
栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,
当一个方法A调用时就产生了一个栈帧F1,并被压入栈中,
A方法有调用了B方法,于是产生栈帧F2也被压入栈,
B方法又调用了C方法,于是产生栈帧F3也被压入栈,
......
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧......
遵循"先进后出"/“后进先出”原则。

3.本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

4.Java堆

①.堆简介

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配对象。
Java堆是垃圾收集器管理的主要区域。
从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代:再细致一点的有Eden空间、From Survivor空间、To Survivor空间、Old空间等。
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。(Java堆的大小可通过-Xms和-Xms控制)

②.对象内存分配

JVM根据对象存活周期的不同,将堆分成几个不同的结构区域,一般来说是分为:新生代(young)、老年代(old)、永久代(permanent)也就是方法区,1.8之后叫元空间。

为什么要进行分代?

  • 堆内存是虚拟机管理的最大的一块内存区域,也是垃圾回收最为频繁的区域。
  • 程序运行时的所有对象实例都在这里保存。内存分代管理就是为对象的内存分配和销毁回收提高效率。
  • 试想一下,如果不进行分代划分,所有的对象都放在一起,那些新生的和一些 生命周期很长的都在一起,
每次进行GC的时候,都必须得扫描遍历全部的对象,这个过程是十分耗费资源的,会严重的影响GC的效率。
  • 有了内存的分代,会大大提升垃圾回收的效率。新生成的对象在新生代中分配内存,经过几次的垃圾回收依然存活下来的就存放到老年中,

永久代中存放静态属性以及类信息等。新生代中的对象,生命周期最短,相对的来说GC的频率就很高。
老年代的生命周期较长,不需要进行频繁的内存回收。永久代基本不需要进行回收操作。
当然了,回收机制是可以根据不同代的特点来选择合适的垃圾回收算法的。

Java堆内存GC的大致过程

发生在新生代的GC称为MinorGC
      1.eden、SurvivorFrom复制到SurvivorTo区,年龄+1
首先,当Eden区满的时候会出发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到SurvivorTo区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
       2.清空Eden、SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是To
       3.SurvivirTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域复制来复制去,如此交换15次(Y由JVM参数MaxTenuringThreshold决定,默认值为15),最终如果还是存活,就存入老年代。

对象分配规则

  1. 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  2. 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。通过参数-XX:PretenureSizeThreshold=3145728控制。
  3. 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,对象每熬过了1次Minor GC对象的年龄加1,达到阀值对象进入老年区。通过参数-XX:MaxTenuringThreshold=15(默认)控制。
  4. 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold要求的年龄数。
  5. 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。 (-XX:-HandlePromotionFailure)

5.永久代(元空间)

永久代与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译编译后的代码等数据,且其空间大小受限于虚拟机大内存大小。
1.8之后将方法区的实现永久代改为了元空间

运行时常量池是方法区的一部分
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,
用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行市场量池中存放。

逻辑上在堆中,实际上它的内存大小不受堆内存容量的控制

6.直接内存

三.GC

1.什么是GC

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,在使用JAVA的时候,一般不需要专门编写内存回收和垃圾清理代 码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。
        电脑的内存大小的不变的,当我们使用对象的时候,如使用New关键字的时候,就会在内存中生产一个对象,但是我们在使用JAVA开发的时候,当一个对象使用完毕之后我们并没有手动的释放那个对象所占用的内存,就这样在使用程序的过程中,对象越来越多,当内存存放不了这么多对象的时候,电脑就会崩溃了,JAVA为了解决这个问题就推出了这个自动清除无用对象的功能,或者叫机制,这就是GC,有个好听是名字叫垃圾回收,其实就在用来帮你擦屁股的,好让你安心写代码,不用管内存释放,对象清理的事情了。

Java GC主要采用分代收集算法:次数上频繁收集Young区,次数上较少收集Old区,基本不动元空间

Java GC通过可达性分析(Reachability Analysis)来判定对象是否存活着。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任务引用链相连(用图论的话来说,就是从GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 虽然互相有关联,但是他们到GC Roots是不可达的,所以他们将会判断为可回收对象

2.GC算法的种类及含义

       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倍以上

GC四大算法

①.计数引用法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能再被使用的。

②.复制算法

年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

原理:
       年轻代中的GC,主要是复制算法(Copying)
HotSpot JVM把年轻代分为了三部分1个Eden区和2个Survivor区(分别叫from和to)默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。 

复制算法劣势:
复制算法它的缺点也是相当明显的。 
  1、它浪费了一半的内存,这太要命了。 
  2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

③.标记清除算法

       用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。
       主要进行两项工作,第一项则是标记,第二项则是清除。 
标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出要回收的对象。
清除:遍历整个堆,把标记的对象清除。 
缺点:此算法需要暂停整个应用,会产生内存碎片 

标记清除算法劣势:

1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序(Stop The World现象),这会导致用户体验非常差劲
2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

④.标记整理(压缩)算法

       在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。 
  标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价

标记压缩算法劣势:

标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
从效率上来说,标记/整理算法要低于复制算法。

标记清除压缩(Mark-Sweep-Compact)

 

该算法结合标记清除算法和标记压缩算法的优势和劣势组合使用

3.总结

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。 
内存整齐度:复制算法=标记整理算法>标记清除算法。 
内存利用率:标记整理算法=标记清除算法>复制算法。 
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
  难道就没有一种最优算法吗? 猜猜看,下面还有=
回答:无,没有最好的算法,只有最合适的算法。==========>分代收集算法。
年轻代(Young Gen) 
年轻代特点是区域相对老年代较小,对像存活率低。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen)
老年代的特点是区域较大,对像存活率高。
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。
Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。
Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。
基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

4.验证

①.栈溢出

设置JVM栈内存大小为128K(-Xss128k),方便模拟栈溢出异常。

package com.jvm;

/**
 * 模拟栈溢出
 * 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
 * 最常见的就是递归造成栈溢出
 */
public class StackOverflowErrorTest {

	private static int number = 0;//记录栈的深度
	
	private static void testStackOverflow(){
		number++;
		testStackOverflow();
	}
	public static void main(String[] args) {
		try {
			testStackOverflow();
		} catch (Throwable e) {
			System.out.println("stack height:"+number);
			e.printStackTrace();
		}
	}
}





stack height:1098
java.lang.StackOverflowError
	at com.jvm.HeadTest.testStackOverflow(HeadTest.java:12)
	at com.jvm.HeadTest.testStackOverflow(HeadTest.java:13)
	at com.jvm.HeadTest.testStackOverflow(HeadTest.java:13)
    ......

②.堆溢出

VM Args:-Xms10m //堆内存最小值

                  -Xmx10m //堆内存最大值

注:一般-Xms和-Xmx值是一样的,避免堆的收缩和扩容,提高性能

Code:

package com.jvm;

import java.util.ArrayList;
import java.util.List;
/**
 * 模拟堆内存溢出
 * VM Args:-Xms10m -Xmx10m
 * @author Hai
 *
 */
public class OutOfMemoryTest {

	public static void main(String[] args) {
		List<Object> objList = new ArrayList<>();//使用集合保存变量,防止对象被GC掉
		while(true){
			Object obj = new Object();
			objList.add(obj);
		}
	}
}

Execute Result:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.jvm.OutOfMemoryTest.main(OutOfMemoryTest.java:17)

③.元数据空间内存溢出

VM Args:-XX:MaxMetaspaceSize=1M //设置元数据空间大小最大为1M

                 -XX:+PrintGCDetails //打印GC日志

Code:

package com.jvm;

import java.util.ArrayList;
import java.util.List;

/**
 * 模拟元空间区内存溢出
 * VM Args:-XX:MaxMetaspaceSize=1M
 * 1M不足main方法启动所需要的元空间内存大小(main方法启动需要的元数据空间内存2700k左右:
 * 	 Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K)
 * @author Hai
 *
 */
public class MethodAreaError {

	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		int i = 0;
		while(true){
			list.add(String.valueOf(i++));
		}
	}
}

Execute Result:

Error occurred during initialization of VM
OutOfMemoryError: Metaspace

④.大对象直接进入老年代验证

VM Args: -Xms20m //堆内存最小值

                -Xmx20m //堆内存最大值

                -Xmn10m //堆中老年代区域的内存大小

                -XX:+PrintGCDetails //打印gc日志

Code:

package com.jvm;

/**
 * 大对象直接进入老年代验证
 * VM Args:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
 * @author Hai
 *
 */
public class BigObjectTest {
	public static void main(String[] args) {
		int a = 8*1024*1024;
		byte[] bigObject = new byte[a];
		System.out.println(a/1024+"k");
	}
}

Execute Result:

8192k
Heap
 PSYoungGen      total 9216K, used 1148K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff71f1a0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
 Metaspace       used 2671K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K
我们可以看到Eden空间几乎没有被使用,而老年代的10M空间被用了8M,也就是8MB的bigObject对象直接就分配到老年代中。由此证明当jvm判断Eden空间不足以存放某个大对象时,会将该大对象直接放入老年区。

 

5.面试题

1.JVM内存模型以及分区,需要详细到每个区放什么

2.堆里面的分区:Eden,survival from to,老年代,各自的特点。

3.GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方

4.Minor GC与Full GC分别在什么时候发生

6.垃圾收集器分类及含义

垃圾回收器种类

在垃圾收集器中并行和并发含义

并行:垃圾收集的多线程的同时进行。

并发:垃圾收集的多线程和应用的多线程同时进行。

垃圾回收器各自含义

Serial/Serial Old

最古老的,单线程,独占式,成熟,适合单CPU  服务器

-XX:+UseSerialGC 新生代和老年代都用串行收集器

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old

ParNew

和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

Parallel Scavenge(ParallerGC)/Parallel Old

关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

-XX:+UseParallerOldGC:新生代使用ParallerGC,老年代使用Parallel Old

-XX:MaxGCPauseMills  :参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

-XX:GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

-XX:+UseAdaptiveSizePolicy 当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。

如果对于收集器运作原来不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  1. 初始标记-短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记-和用户的应用程序同时进行,进行GC RootsTracing的过程
  3. 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  4. 并发清除

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

同时用户的线程还在运行,需要给用户线程留下运行的内存空间。

-XX:CMSInitialOccupyFraction  ,因为以上两点,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 早期版本的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

-XX:+UseCMSCompactAtFullCollection为了解决这个问题,CMS收集器提供了一个这个开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入FullGC时都进行碎片整理)。

 

G1

-XX:+UseG1GC

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

内存布局:在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

  1. 新生代GC

回收Eden区和survivor区,回收后,所有eden区被清空,存在一个survivor区保存了部分数据。老年代区域会增多,因为部分新生代的对象会晋升到老年代。

     2.并发标记周期

初始标记:短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,产生一个全局停顿,都伴随有一次新生代的GC。

根区域扫描:扫描survivor区可以直接到达的老年代区域。

并发标记阶段:扫描和查找整个堆的存活对象,并标记。

重新标记:会产生全局停顿,对并发标记阶段的结果进行修正。

独占清理:会产生全局停顿,对GC回收比例进行排序,供混合收集阶段使用

并发清理:识别并清理完全空闲的区域,并发进行

      3.混合收集

对含有垃圾比例较高的Region进行回收。

G1当出现内存不足的的情况,也可能进行的FullGC回收。

G1中重要的参数:

-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。

-XX:ParallerGCThreads:设置GC的工作线程数量

未来的垃圾回收

ZGC通过技术手段把stw的情况控制在仅有一次,就是第一次的初始标记才会发生,这样也就不难理解为什么GC停顿时间不随着堆增大而上升了,再大我也是通过并发的时间去回收了

关键技术

  1. 有色指针(Colored Pointers
  2. 加载屏障(Load Barrier
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值