JVM深度解析
1、StackOverFlowError
Thrown when a stack overflow occurs because an application recurses too deeply.
调用栈深度超过限制
递归运算时会遇到
2、OOM
Out of memory
当JVM分配内存时 不够才会抛出异常
主要分为
java.lang.OutOfMemoryError:Java heap space
新建对象时 JVM内存不足 new
java.lang.OutOfMemoryError:GC overhead limit exceedec
GC回收时间过长时会抛出该异常
过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下才会抛出。
避免恶性循环 浪费CPU性能
java.lang.OutOfMemoryError:Direct buffer memory
写NIO程序时经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(channel)与缓冲区(buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
ByteBuffer.allocate(caoability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocateDirect(caoability) 第2种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OOM
java.lang.OutOfMemoryError:unable to create new native thread
导致原因:
1.你的应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
2、你的服务器不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个
java.lang.OutOfMemoryError:Metaspce
Java 8 之后的版本使用metaspace 来替代永久代
metaspace 是方法区在HotSpot中的实现,它与永久代最大的区别在于:metaspace并不在虚拟机内存中而是使用本地内存
存储信息如下:
虚拟机加载的类信息
常量池
静态变量
即时编译后的代码
————————————————
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
1.JVM的生命周期:
*启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
*运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
*消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
当在电脑上运行一个程序时,就会运行一个java虚拟机,java虚拟机总是开始于main方法,main方法是程序的起点。
java的线程一般分为两种:守护线程和普通线程。守护线程是java虚拟机自己使用的线程,比如GC线程就是一个守护线程,当然你可以把自己的线程设置为守护线程,注意:main方法启动的初始线程不是守护线程。
只要java虚拟机中还有普通线程在执行,java虚拟机就不会停止,如果有足够的权限,你可以调用exit()方法终止线程。
2.JVM的体系结构:
1) 类装载器(ClassLoader)(用来装载.class文件)
2) 执行引擎(执行字节码,或者执行本地方法)
3) 运行时数据区(方法区、堆、虚拟机栈、程序计数器、本地方法栈)
首先我们对运行时数据区中的5个区域进行分析:
3. 运行时数据区:
3.1 堆:
所有线程共享的内存区域,在虚拟机启动时创建。
用来存储对象实例,如:String a = new String()中new String()创建了一个对象,该对象存放在堆内存中,而a 是存放在栈中的,堆中new String() 存放了栈中 a 的内存地址。
可以通过-Xmx和-Xms控制堆的大小
当在堆中没有内存完成实例分配,且堆也无法再扩展时,会报OutOfMemoryError异常。
java堆是垃圾回收器的主要工作区域。java对还可以分为新生代、老年代。但是垃圾回收器的永久代是在方法区中的,不在堆中。
(新生代:新建的对象由新生代分配内存;老年代:存放经过多次垃圾回收器回收仍然存活的对象;永久代:存放静态文件,如java类、方法等,永久代存放在方法区,对垃圾回收没有显著的影响)
3.1.1 新生代:
分为三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。(动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。)经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
GC可分为三种:Minor GC Major GC 和 Full GC
Minor GC :是清理新生代。触发条件:当Eden区满时,触发Minor GC。
Major GC:是清理老年代。是 Major GC 还是 Full GC,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。
Full GC :是清理整个堆空间—包括年轻代和老年代。触发条件:调用System.gc时,系统建议执行Full GC,但是不必然执行;老年代空间不足;方法区空间不足;通过Minor GC后进入老年代的平均大小大于老年代的可用内存;由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
3.1.2 老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
3.1.3 永久代(永久代是在方法区中的,而不在堆中,这里只是为了总结GC的运行机制并和新生代、老年代进行比较才将永久代放在这里写):
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的MetaSpace区域所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制.采用元空间取代永久代的原因:(1)为了解决永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出。(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出(因为堆空间有限,此消彼长)。(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低(4)Oracle 可能会将HotSpot 与 JRockit 合二为一。
3.2 方法区:
所有线程共享
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存的分配需求时,报OutOfMemoryError异常
方法区中有一个运行时常量池,用于存储编译期生成的各种字面量与符号引用,当常量池无法再申请到内存时报OutOfMemoryError异常。
3.3 虚拟机栈:
线程私有,声明周期与线程同步。
存储一些方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
每个虚拟机线程都有一个私有的栈,一个线程的java栈在线程创建的时候被创建。
每个方法执行的同时都会创建一个栈帧,每个方法被调用直至完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
当线程请求的栈深度大于虚拟机允许的深度时报StackOverFlowError异常。
当栈的扩展无法申请到足够的内存时报OutOfMemoryError异常。
3.4 本地方法栈:
主要是为虚拟机使用到的Native方法服务,Native 方法就是一个java调用非java代码的接口,该方法的实现由非java语言实现。Native方法用native修饰,没有方法体,因为方法体中的实现是非java语言的。
有时java需要调用操作系统的一些方法,而操作系统基本都是C语言写的,这时就需要使用到Native方法了。
Native方法关键字修饰的方法是一个原生态的方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI(Java Native Interface)接口调用其他语言来实现对底层的访问。
3.5 程序计数器:
当前线程所执行的字节码的行号指示器,当前线程私有,由于他只是存储行号,一般就是一个数字,所以不会出现OutOfMemoryError异常。
其特点是:如果正在执行java方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址,如果正在执行Native方法,则这个计数器为空(undefined),此内存区域是唯一一个在java虚拟机中没有规定任何OutOfMemoryError异常情况的区域。
使用场景:A线程先获取CPU时间片执行,当执行到一半的时候,B线程过来了,且优先级比A线程的高,所以处理器又去执行B线程了,把A线程挂起,当B线程执行完了以后,再回过头来执行A线程,这时就需要知道A线程已经执行的位置,也就是查看A中的程序计数器中的指令。
总结:java对象存放在堆中,常量存放在方法区的常量池中,虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区,以上区域都是线程共享的。栈是线程私有的,存放该方法的局部变量(基本类型、对象引用)操作数栈、动态链接、方法出口等信息。一个java程序对应一个JVM,一个方法对应一个java栈。
4.1 垃圾收集器的种类:
4.1.1 Serial 收集器:
这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。串行垃圾回收器是为单线程环境而设计的,如果你的程序不需要多线程,启动串行垃圾回收。串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
4.1.2 ParNew收集器:
ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
4.1.3 Parallel 收集器:
Parallel 收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
4.1.4 Parallel Old 收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
4.1.5 CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
4.1.6 G1收集器:
空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。
5.1 java源码编译机制:
java源码是不能被机器识别的,需要经过编译器编译成JVM可以执行的.class字节码文件,再由解释器编译运行,即:Java源文件(.java) -- Java编译器 --> Java字节码文件 (.class) -- Java解释器 --> 执行。流程图如下:
java中字符只以Unicode存在,字符转换发生在JVM和OS交界处
5.2 类加载机制(ClassLoader):
java程序并不是一个可执行文件,是由多个独立的类文件组成。这些类文件并不是一次性全部装入内存,而是依据程序逐步载入。
JVM的类加载是通过ClassLoader及其子类来完成的,累的层次关系和加载顺序可以由下图来描述:
1)Bootstrap ClassLoader
是JVM的根ClassLoader,由C++实现;加载Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现;JVM启动的时候就开始初始化此ClassLoader。
2)Extension ClassLoader
加载java扩展API(lib/ext中的类)
3)App ClassLoader
加载Classpath目录下定义的class
4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、Jboss都是会根据J2EE规范自行实现ClassLoader
注意:加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
双亲委派机制
JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
作用:1)避免重复加载;2)更安全。如果不是双亲委派,那么用户在自己的classpath编写了一个java.lang.Object的类,那就无法保证Object的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。
破坏双亲委派机制
双亲委派机制并不是一种强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。
线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。
JVM(Java虚拟机)从0到1全部合集,强烈建议收藏!
为了方便大家更好的掌握好JVM(Java虚拟机),也为了让知识更系统化,这里我单独把JVM的整个系列一并罗列于此,希望对有用的人有用,也希望能帮助到更多的人。
这篇文章主要讲了以下几点:
- JVM内存结构
- 堆内存(Heap)
- 方法区(Method Area)
- 虚拟机栈(JVM Stack)
- 本地方法栈(Native Stack)
- 程序计数器(PC Register)
- 直接内存
- JVM内存参数设置
- 典型JVM参数配置
这篇文章主要讲了以下几点:
- 标记清除
- 复制算法
- 标记整理
- 分代收集算法
这篇文章主要讲了以下几点:
常见的垃圾收集器有3类
1.新生代的收集器包括:
2.老年代的收集器包括:
3.回收整个Java堆(新生代和老年代)
新生代垃圾收集器
1.Serial串行收集器-复制算法
2.ParNew收集器-复制算法
3.Parallel Scavenge(并行回收)收集器-复制算法
老年代垃圾收集器
1.Serial Old 收集器-标记整理算法
2.Parallel Old 收集器-标记整理算法
3.CMS收集器-标记清除算法
新生代和老年代垃圾收集器
G1收集器-标记整理算法
G1收集器的优势:
G1收集器的运作步骤
这篇文章主要讲了以下几点:
- JVM内存调优
- JVM性能调优方法和步骤
- JVM调优参数参考
这篇文章主要讲了以下几点:
- 什么场景需要JVM调优
- JVM性能监控分析工具
- VisualVM
- Jconsole
- MAT
- JVM内存泄漏分析
这篇文章主要讲了以下几点:
- Minor GC
- Major GC
- 分代GC
- Full GC
这篇文章主要讲了以下几点:
1.G1概述
2.G1堆内存
3.G1回收流程
4.G1 GC模式
5.G1推荐用例
Java运行时内存区域
程序计数器,线程独占,当前线程所执行的字节码的行号指示器,每个线程需要记录下执行到哪儿了,下次调度的时候可以继续执行,这个区是唯一不会发生oom的
栈,线程独占,包含虚拟机栈或native method stack,用于存放局部变量的
堆,线程共享,用于分布对象实例的,后面说的内存管理和垃圾回收基本都是针对堆的
方法区,线程共享,用于存放被虚拟机加载的类,常量,静态变量; Java虚拟机规范,把方法区描述为堆的逻辑部分,所以也被称为“永久代”,在大量使用反射,动态代理,ClassLoader的场景下,要考虑永久代的回收
对于一个简单的,对象生成
Object obj = new Object();
会涉及到3个区,
图中描述两种对象访问方式,有句柄,可以屏蔽对象实际地址的改变(gc时对象地址经常会改变),不用句柄效率更高
垃圾回收
垃圾判定
对于如何判定对象是垃圾,教科书的答案是引用计数,并且也在COM,python中得到较好的应用
引用计数的问题是,难以解决循环引用的问题,
A.instance = B; B.instance =A
这样会导致A,B的引用计数都不为0
所以对于Java,C#,Lisp都是采用GC Roots Tracing的方式,
原理也很简单,就是选取一系列GC Roots对象,只保留从root对象可达的对象,其余都是垃圾
Java中的GC roots包含,
- JavaStack中的引用的对象
- 方法区中静态引用指向的对象
- 方法区中常量引用指向的对象
- Native方法中JNI引用的对象
垃圾回收算法
Mark-Sweep,最原始的想法,先标记出所有垃圾对象,然后回收掉;问题是,效率低,而且会产生大量内存碎片
Copying,最简单的copying,把内存分两半,先用一半,然后回收时,把有效对象copy到另外一半
这个首先只适用于年轻代,即垃圾对象占较高比例的case
再者,空间太浪费了,只能用一半
所以,现在实际使用的版本为,分为一个较大的eden区,两个较小的survivor区
比例一般为8:1:1,其中一个survivor区是空的,这样只浪费10%的空间
能这样做的前提就是,每次只有最多10%的对象存活
对象先放到eden区,eden区满了,进行minor gc,把eden区和有数据的survivor区的存活对象,放到另一个空的survivor区中
两个survivor区,虽然命名为from和to,但是其实没有任何区别,完全对等的
当如果survivor区的空间不够,就需要放到年老代(称为handle promotion,即年老代要为survivor提供担保,你那空间不够,可以放我这,类似贷款担保),如果年老代的空间也不够或不能接受Handle promotion失败,就需要full gc去回收年老代
Mark-Compact,前面说copying只适用于存活对象比例较低的case,所以适合年轻代,但对于年老代这样的,用copying肯定是不行的;
Mark-compact,mark还是一样的,只是在回收时,会把对象做平移,消除碎片
垃圾收集器
Serial收集器
最简单的,单线程,收集时会stop the world,所有用户线程暂停
可以用于收集新生代或老年代
收集新生代,用copying算法
收集老年代,用标记-整理,称为serial old
简单高效,适用于单CPU环境;是虚拟机Client模式的默认收集器
缺点,会stop the world
ParNew收集器
只是serial的多线程版本,适用于多核环境,如果在单核的机器上,效率还不如serial
优势是,可以配合CMS收集器使用,因为CMS作为老年代的收集器,只能配合Serial或ParNew作为新生代的收集器
CMS(Concurrent Mark Sweep)收集器
以最短停顿时间为目标的收集器,适合用于网站或B/S系统的服务端,重视服务响应速度的场景。
该收集器相对比较复杂,整个过程分为,
1. 初始标记(CMS initial mark),stop the world,标记GC Roots直接关联到的对象,速度很快
2. 并发标记(CMS concurrenr mark), 并行进行GC Roots Tracing的过程
3. 重新标记(CMS remark),stop the world,由于并发标记和用户线程是并发执行的,所以需要对标记进行最后的修正,消耗时间会大大小于并发标记时间
4. 并发清除(CMS concurrent sweep),最后进行sweep
缺点,
a. CPU敏感,频繁GC会导致CPU卡死
b. 无法处理浮动垃圾(floating garbage),在并发清理阶段产生的新垃圾无法在这次完成回收
c. 需要预留较大的内存,由于CMS收集过程是和用户应用并发进行的,所以不能等到老年代快被占满再做,需要提前进行收集,默认是设为68%,这是比较保守的设定,可以减少以降低gc的次数;但是如果在CMS收集过程中,出现用户应用程序内存不够的情况,会发生”Concurrent Mode Failure”,这样虚拟机只能用后备方案,serial old收集器进行年老代的收集(因为应用已经无法并发执行),这样应用停顿时间就会很长,所以需要设置合理的比例
d. 因为采用sweep,会有大量碎片
Parallel Scavenge收集器
新生代收集器,设计目的是达到可控制的throughput,CPU运行用户代码时间/CPU总消耗时间,说白了,就是保证CPU更多的执行用户代码而非gc
适合后台运算,不太需要交互的场景
但鱼和熊掌不可兼得,throughput和GC停顿时间是需要tradeoff的
降低新生代的空间大小,缩小gc的间隔,都可以减少GC的停顿时间,但也会降低throughput
并且该收集器,支持UseAdaptiveSizePolicy参数, 这样不需要使用者指定新生代大小,Eden和Survivor比例,年老代晋升年龄等,虚拟机会根据运行性能监控去优化调整
Parallel Old收集器
前面提到的parallel sacvenge收集器是无法与CMS收集器配合使用的,所以之前只能配合Serial Old来收集老年代,导致效率低
Parallel Old作为Serial Old的多线程版本,可以更好的和parallel sacvenge收集器配合,真正达到throughput优先的收集
G1(Garbage First)收集器
新一代的收集器,不同于之前的收集器,会收集整个新生代或老年代
G1会把整个Java堆划分为多个固定大小的区域,并跟踪垃圾堆积程度,并每次收集垃圾最多的区域,可以大大提高收集效率
JVM收集器对应参数
内存分配和回收策略
对象往往在新生代的Eden区分配,Eden区空间不够,发起minor gc,会将eden和一个survivor区的存活对象copy到另一个survivor区,如果另一个survivor区空间不够,存入老年代
大对象会直接进入老年代,比如很长的字符串或很大的数组,大对象对于JVM内存分配是个坏消息,因为大对象需要找到连续内存,否则会触发gc,所以短命的大对象是需要尽量避免的
长期存活的对象进入老年代,对象在新生代每经历一次minor gc,年龄加1, 默认达到15岁会进入老年代
每次Minor GC时,虚拟机会检测每次晋升到老年代的平均大小是否大于老年代当前剩余大小,如果小于,则进行full gc
Minor GC、Major GC、Full GC的区别
今天主要谈谈JVM GC的类型和策略,特别是大家经常混淆的Minor GC、Major GC、Full GC,年轻代GC、老年代GC,之间有什么区别和联系。
Minor GC
JVM堆内存被分为两部分:年轻代(Young Generation)和老年代(Old Generation)。
1.年轻代
年轻代是所有新对象产生的地方,当年轻代内存空间被用完时,就会触发垃圾回收,这个垃圾回收叫做Minor GC。
年轻代被分为3个部分——Enden区和两个Survivor区,年轻代空间的要点:
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
2.年老代
年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象,通常会在老年代内存被占满时进行垃圾回收。
Major GC
老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。
Minor GC和Major GC其实就是年轻代GC和年老年GC的俗称。而在Hotspot VM具体实现的收集器:Serial GC, Parallel GC, CMS, G1 GC中,大致可以对应到某个Young GC和Old GC算法组合。
分代GC
针对HotSpot VM的实现,其实GC的准确分类可以分为:
- 分代GC
- Full GC
以及后续的G1的分区收集本质其实还是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。
分代GC并不收集整个GC堆的模式,而是只专注分代收集
- Young GC:只收集年轻代的GC
- Old GC:只收集年老代的GC(只有CMS的concurrent collection是这个模式)
- Mixed GC:收集整个young gen以及部分old gen的GC(只有G1有这个模式)
Full GC
Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。