Java Virtual Machine:Java虚拟机,用来保证Java语言跨平台
Java虚拟机可以看做是一台抽象的计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域
Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式(class文件格式所关联)
Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行
所谓java能实现跨平台,是因为在不同平台上运行不同的虚拟机决定的,因此java文件的执行不直接在操作系统上执行,
而是通过jvm虚拟机执行,我们可以从这张图看到,JVM并没有直接与硬件打交道,而是与操作系统交互用以执行java程序。
好处:
这张图是jvm的组成图,分为四部分:
类加载器
执行引擎
本地接口
运行时数据区
Program Counter Register 程序计数器(寄存器)
程序计数器是一小块的内存区域,可以看做当前线程执行字节码的行号指示器,在虚拟机的概念模型里,字节码解释工作就是通过改变这个计数器的值来选取下一个要执行的字节码指令。
比如分支控制,循环控制,跳转,异常等操作,线程恢复等功能都是通过这个计数器来完成。由于jvm的多线程是通过线程的轮流切换并分配处理器执行时间来实现的。因此,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
因此,为了线程切换后能回到正确的执行位置,每条线程都需要自己独有的程序计数器,多条线程计数器之间互不影响,独立存储。我们称这类内存区域为线程私有的内存区域。
如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行native方法,则这个计数器则为空(undefined)此内存区域是Java中虚拟机中唯一一个没有规定任何OutOfMemoryError的内存区域。
作用,保存当前执行指令的地址,一旦指令执行,程序计数器将更新到下一条指令
特点
Java Virtual Machine Stacks (Java 虚拟机栈)
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
与程序计数器一致,Java虚拟机栈也是线程私有的,生命周期与线程相同。
虚拟机栈描述的是方法的执行内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,方法出口等信息。每一个方法从执行到结束的过程,就对应一个栈帧从入栈到出栈的过程。
局部变量表存放了编译器可知的四类八种基本数据类型,对象引用(refrence),它不等同于对象本身,可能是指向对象起始地址的引用指针。
局部变量表的内存分配在编译期已经完成分配了,其中64位长度的long和double会占用两个局部变量空间,其余的数据类型只占一个。当进入一个方法时,这个方法需要在栈中分配多大的内存空间是完全能够确定的,方法运行期间不改变局部变量表的大小。
如果线程在栈中申请的深度大于虚拟机所允许的深度,将出现StackOverFlowError异常; 如果虚拟机栈可以动态扩展(当前大部分虚拟机支持动态扩展,当然也允许固定长度的虚拟机栈),如果扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常。
问题辨析
案例1: cpu 占用过多
定位
组成:局部变量表、操作数栈、动态链接、方法返回地址
局部变量表:
存放局部变量的列表;
一个局部变量可以保存类型为boolean、byte、char、short、float、reference和returnAddress的数据两个局部变量可以保存一个类型为long和double的数据;
局部变量使用索引来进行定位访问,第一个局部变量的索引值为零;
操作数栈:
也称为操作栈,它是一个后进先出的栈;
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作;
一个完整的方法执行期间往往包含多个这样出栈/入栈的过程;
简单理解, 操作数栈是线程实际的操作台;
动态链接:
简单的理解为指向运行时常量池的引用;
在class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用;
本地方法栈的功能和特点类似于虚拟机栈,也是线程私有的。
不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的Java方法
如何去服务native方法?
native方法使用什么语言实现?
怎么组织像栈帧这种为了服务方法的数据结构?
虚拟机规范并未给出强制规定,因此不同的虚拟机实可以进行自由实现
对于大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块区域,且Java堆是被所有线程所共享的一片区域,在虚拟机启动时创建。
该区域的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配空间。这一点在JVM规范上描述的是:所有的对象实例以及数组都要在堆上分配空间。
Java堆是垃圾收集器管理的管理的主要区域,因此很多时候被称为GC堆。从内存分配的角度讲,由于现在的垃圾回收机制都是分代垃圾回收,所以堆中可以再划分为老年代和新生代,再细的划分为Eden区,Survivor区,其中Survivor区又可细分为From Survivor区和To Survivor区。根据JVM的规范规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。
就像我们的磁盘一样,既可以是固定大小的,也可以是可扩展的。不过当前主流的都采用可扩展的策略(采用-Xmx 和 -Xms控制)。如果在堆中没有完成内存分配,且堆也没有可扩展的内存空间,则会抛出OutOfMemoryError异常。
Heap 堆
特点
案例
垃圾回收后,内存占用仍然很高
方法区与java堆一样,有各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据。
Java虚拟机相对而言对方法区的限制非常宽松,除了和堆一样不需要连续的空间和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。
相对而言,垃圾回收在这个区域算比较少见了,但并非数据进入方法区以后就可以实现永久存活了,这个区域的回收目标主要是常量池的回收和对类型的卸载,一般来说,这个区域的回收成绩是比较难以让人满意的。尤其是类型的卸载,条件相当苛刻。根据Java虚拟机规范规定,当方法区无法满足内存分配时,将抛出OutOfMemoryError异常。
1.8 以前会导致永久代内存溢出
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
1.8 之后会导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法和接口的信息外,还有一项信息是常量池。用于存放编译器各种字面量和符号的引用,这部分内容将在类加载后进入到常量池中存储。
一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件的常量池一个最大的特性就是动态性,Java语言并不要求常量一定在编译期间产生,也就是说并非预置入Class文件中常量池的内容才能进入常量池,在运行期间也可能将新产生的常量放进常量池,这种特性被利用最多的就是String的intern()方法。既然运行时常量池属于方法区的一部分,自然具备方法区的约束,所以当内存申请不到的时候也会抛出OutOfMemoryError异常。
调整 -XX:StringTableSize=桶个数
考虑将字符串对象是否入池
直接内存并不属于Jvm运行时数据区的一部分,但是这部分内存区域被频繁的调用,也可能发生OutOfMemoryError异常,所以一起讨论。显然本机的直接内存不会受到Java堆分配内存的影响,但是既然是内存,肯定要受到本机总内存的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存。使得各个区域的内存总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
Java堆溢出
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
虚拟机栈和本地方法栈溢出
直接内存溢出
引用计数器算法简单概括为:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时刻,当计数器为0 的时候,该对象不再被引用。客观的说,引用计数器的实现简单,判定效率也高,大部分场景下是一个不错的选择。但是,当前主流的Jvm均没有采用标记清除算法,原因在于,它很难解决对象之间互相循环调用的情况。
在主流的商用程序语言(如C#, Java)的主流实现中,都是通过可达性分析来判断对象是否存活,这个算法的思想就是通过一系列的成为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到"GC Roots"没有任何引用链相连,则证明此对象是不可用的。表示可以回收
如图所示,虽然Obj5, Obj6, Obj7互有关联,但是他们到GC root没有任何引用链,所以判定为需要被回收的对象。
常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。
在Java中,可以作为GC Roots的对象包括下面几种:
无论是通过引用计数器判断的引用数量,还是通过可达性分析判断出的引用链是否可达,判定对象是否存活都跟引用有关。
在JDK1.2以前,引用被定义为当一个reference类型的数据代表的是另外一块内存的起始地址,该类型的数据被称之为引用,这种定义很纯粹,但是也很狭隘,一个对象在这种定义下只有被引用和没有被引用两种状态。对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这类对象,当内存足够的时候,将它存放在内存中,当内存空间进行垃圾回收后显得还是内存紧张时,可以回收这部分对象,很多系统的缓存功能都符合这样的应用场景。
因此在JDK1.2以后对引用进行重新的扩充,分为强引用,软引用,弱引用,虚引用4中,这四种引用的强度依次递减。
强引用:
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
强引用是在代码中普遍存在的,类似于Object obj = new Object(),只要强引用一直存在,垃圾收集器就永远不会回收被引用的对象。
软引用(SoftReference):
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
软引用用来描述一些还有用但并非必须的对象,对于软引用关联着的对象,当内存溢出异常发生之前,通过垃圾回收进行二次回收。如果二次回收完成之后,系统内存依然不够,才会抛出内存溢出异常,在jdk1,2以后用SoftReference类来实现软引用。
弱引用(WeakReference):
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
弱引用也是用来描述非必须对象的,但是它的强度相比于软引用来说更弱一些,它仅仅能生存到下一次垃圾回收之前。当垃圾收集时,无论内存是否足够,弱引用的对象都要被回收,在jdk1.2以后用WeakReference类来实现弱引用
虚引用(PhantomReference):
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获取一个实例对象。为一个对象设置弱引用的唯一目的就是该对象在垃圾回收时受到一个系统通知,Jdk1,2以后用PhantomReference实现虚引用。
终结器引用(FinalReference):
生存还是死亡?
即使在可达性分析中,没有引用链到达GC Roots,也并非是“非死不可”的。这个时候对象处于缓刑阶段,要正式宣告死亡,至少要经历两次标记的过程。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否要执行finalize()方法,当对象么有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为不必要执行finalize()方法。
如果该对象被判定要执行finalize()方法,那么这个对象会被放在一个叫F-Queue的队列中,并在稍后有一个虚拟机自行创建的,优先级较低的线程去执行它,这里的执行是指会触发finalize()方法,但并不会等待它执行结束。
这样做的原因是如果一个对象在执行finalize()时非常缓慢,或者执行了死循环,这样就会导致F-Queue中的其他对象处于等待中,严重的会导致整个垃圾回收系统崩溃。finalize()是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行二次标记,如果对象要在finalize()中拯救自己的话,只能重新与引用链上的任意一个对象建立关联即可,比如把对象自己(this关键字)赋值给其他成员变量或者对象,那在第二次标记时就被移出即将回收的集合,如果没有关联上,基本可以确定要被回收了。
最基础的收集算法是标记-清除算法,如同它的名字一样,分为标记和清除两个阶段。第一步标记出所要回收的对象,在标记完成后统一回收所有被标记的对象。如何标记已经在上面说过了,之所以说它是最基本的垃圾收集算法,原因在于其他的算法也是基于这种思路并对其不足做以改进得到的。
主要问题有两个,第一个是效率问题,标记和清除的效率都不高。第二个是空间分配问题,标记清除后会产生大量的不连续的内存空间,空间碎片太多可能会导致以后程序在运行过程中需要给较大对象分配空间时,无法找到足够的内存空间,而不得不提前进行一次垃圾收集动作。如图所示,会产生大量的垃圾碎片,导致空间的利用率不高。
速度较快
会造成内存碎片
速度慢
没有内存碎片
复制算法在存活对象比例比较高的情况下要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的区域,则需要额外的空间进行分配担保,以应对内存中100%对象都存活的极端情况,所以老年代一般不选用这种算法。
根据老年代的特点,有人提出了另一种标记-整理算法,标记过程与标记-清除算法一致,但后续步骤不是对可回收对象直接进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界外的对象。示意图如下:
不会有内存碎片
需要占用双倍内存空间
为了解决效率问题,一种称为复制的收集算法出现了,它将可用内存分为大小相等的两块,每次只使用其中的一块,当这一块内存区域用完了,就将还存活的对象复制到另一块内存中,然后再把已使用的空间一次性清理掉,这样每次都是对半个区域进行回收,内存分配时也就不用考虑碎片等问题了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
只是这种做法将原来的内存缩小为一半,代价太高了。
现在的商用虚拟机都采用这种方法来回收新生代,IBM专门研究表明,新生代中的对象98%都是"朝生夕死"的,所以并不需要按照1:1划分内存区域,而是将内存分为一块较大的区域给Eden和两块较小的区域给Survivor, 当回收时,将Eden和Survivor区中还存活的对象一次性复制到另一块Survivor区,然后将Eden和Survivor区进行一次性清理。Hotspot区默认的Eden和Survivor的比例为8:1,也就是说新生代的可用内存为90%,只有10%的内存会被划分为保留内存。当然,大多数情况下是98%,但我们不能保证每次回收的存活对象都小于10%,当Survivor区不够用时,需要依赖其他区域进行分配担保。如果另外一块Survivor区已经不够用了,对象可通过内存担保机制直接进入到老年代。
对象首先分配在伊甸园区域
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
当前商用的垃圾收集器都采用的是分代垃圾回收,这种算法没有什么新的思想,只是根据对象的存活周期将内存分为几块,一般是将java堆分为新生代和老年代,这样就可以根据各个代的对象特点选用最适当的回收算法。在新生代,每次垃圾回收都有大量的对象死去,只有少量存活,这样就适合采用复制算法。只需要付出少量的对象复制成本就可以完成垃圾回收,而老年代因为存活率高,没有其他内存进行分配担保,就必须使用标记-清理或者标记-整理进行回收。
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
-XX:+UseSerialGC = Serial + SerialOld
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
定义:Garbage First
适用场景
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
超大堆内存,会将堆划分为多个大小相等的 Region
整体上是 标记+整理 算法,两个区域之间是 复制 算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
XX:InitiatingHeapOccupancyPercent=percent (默认45%)
会对 E、S、O 进行全面垃圾回收
-XX:MaxGCPauseMillis=ms
SerialGC
ParallelGC
CMS
G1
pre-write barrier + satb_mark_queue
优点:节省大量内存
缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
预备知识
答案是不发生 GC
查看 FullGC 前后的内存占用,考虑下面几个问题
数据是不是太多?
数据表示是否太臃肿?
是否存在内存泄漏?
新生代的特点
越大越好吗?
-Xmn
Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is
performed in this region more often than in other regions. If the size for the young generation is
too small, then a lot of minor garbage collections are performed. If the size is too large, then only
full garbage collections are performed, which can take a long time to complete. Oracle
recommends that you keep the size for the young generation greater than 25% and less than
50% of the overall heap size.
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...
以 CMS 为例
-XX:CMSInitiatingOccupancyFraction=percent
Serial收集器是最基本,发展最悠久的收集器,在JDK1.3.1之前是虚拟机新生代垃圾回收的唯一选择。这个收集器是一个单线程的。它的单线程的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成收集工作,最重要的是,它进行垃圾收集时,其他工作线程会暂停,直到收集结束。这项工作由虚拟机在后台自动发起和执行的,在用户不可见的情况下将所有工作线程全部停掉,这对于很多应用程序来说是不可容忍的。我们可以设想一下,我们的计算机在运行1个小时就要停止5分钟的时候,这是什么情况?对于这种设计,虚拟机设计人员表示的也是非常委屈,因为不可能边收集,这边还要不断的产生垃圾对象,这样是清理不完的。
所以从1.3一直到现在,虚拟机的开发团队一直为减少因垃圾回收而产生的线程停顿所努力着,所出现的虚拟机越来越优秀,但直到现在,依然没有完全消除。
讲到这里,貌似Serial收集器已经是"食之无味弃之可惜"了,但实际上,它依然是虚拟机在Client模式下,新生代默认的垃圾收集器。它有相对于其他垃圾收集器的优势,比如由于没有线程之间切换的开销,专心做垃圾收集自然能够收获最高的线程利用效率。在用户桌面应用背景下,一般分配给虚拟机的内存不会太大,收集几十兆或者一两百兆的新生代对象,停顿时间完全可以控制在几十毫秒到一百毫秒之间,这个是可以接受的,只要不是频繁发生。因此,Serial收集器在Client模式下,对于新生代来说依然是一个很好的选择。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其余可控参数,收集算法,停止工作线程,对象分配原则,回收策略等与Serial收集器完全一致。
除了多线程实现垃圾收集之外,其他没有什么太多创新之处,但是它确实Server模式下的新生代的首选的虚拟机收集器。其中一个重要的原因就是除了Serial收集器外,只有它能与CMS配合使用。在JDK1.5时期,HotSpot推出了一款在强交互应用划时代的收集器CMS,这款收集器是HotSpot第一款真正意义上的并发收集器,第一次实现了垃圾回收与工作线程同时工作的可能性,换而言之,你可以边污染,边收集。
不过CMS作为老年代的收集器,却无法与1.4中发布的最新的新生代垃圾收集器配合使用,反之只能使用Serial或者Parnew中的一个。ParNew收集器可以使用-XX:+UseParNewGC强行指定它,或者使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器。
ParNew收集器在单CPU环境下绝对不会有比Serial收集器更好的效果,甚至优于存在线程交互开销,该收集器在通过超线程技术实现的两个CPU的环境下都不能保证百分之百超越Serial收集器。当然,随着CPU数量的增加,对于GC时系统的有效资源利用还是很有好处的。在CPU非常多的情况下,可以使用-XX:ParallelGCThreads来限制垃圾回收线程的数量。
Parallel Scavenge收集器是一个新生代收集器,采用复制算法,又是并行的多线程垃圾收集器。它的关注点与其它收集器的关注点不一样,CMS等收集器的关注点在于缩短垃圾回收时用户线程停止的时间,而Parallel Scavenge收集器则是达到一个可控制的吞吐量,所谓吞吐量就是CPU运行用户线程的时间与CPU运行总时间的比值,即 吞吐量 = (用户线程工作时间)/(用户线程工作时间 + 垃圾回收时间),比如虚拟机总共运行100分钟,垃圾收集消耗1分钟,则吞吐量为99%。停顿时间越短越适合与用户交互的程序,良好的响应速度能提高用户体验,但是高吞吐量则可以高效率的利用CPU的时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的程序。
有两个参数控制吞吐量,分别为最大垃圾收集时间: -XX:MaxGCPauseMills, 直接设置吞吐量的大小: -XX:GCTimeRatio
-XX:+UseAdaptiveSizePolicy
自适应策略也是Parallel Scavenge收集器区别去Parnew收集器的重要一点
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,这个收集器的主要目的也是在与给Client模式下使用。如果在Server模式下,还有两种用途,一种是在jdk5以前的版本中配合Parallel Scavenge收集器使用,另一种用途作为CMS的备用方案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法,这个收集器在jdk6中才开始使用的,在此之前Parallel Scavenge收集器一直处于比较尴尬的阶段,原因是,如果新生代采用了Parallel Scavenge收集器,那么老年代除了Serial Old之外,别无选择,由于老年代Serial在服务端的拖累,使得使用了Parallel Scavenge收集器也未必能达到吞吐量最大化的效果,由于单线程的老年代无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至不如Parallel Scavenge收集器 + CMS。直到Parallel Old收集器出现后,"吞吐量优先收集器"终于有了名副其实的组合,在注重吞吐量优先和CPU资源敏感的场合,可以采用Parallel Scavenge收集器 + Parallel Old收集器
CMS收集器是一种以获取最短停顿时间为目标的收集器。从名字(Concurrent Mark Sweep)上就可以看出,采用的标记-清除算法,它的过程分为4个步骤:
只有初始标记和重新标记需要暂停用户线程。
初始标记 — 仅仅关联GC Roots能直接关联到的对象,速度很快
并发标记 — 进行GC Roots Tracing的过程,
重新标记 — 为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录
并发清除
由于整个过程中耗时最长的并发标记和并发清除过程收集器都能与用户线程一起工作,所以总的来说,CMS的内存回收过程与用户线程一起并发执行的
CMS收集器的三大缺点:
首先,G1的设计原则就是简单可行的性能调优
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
内存分配
Young垃圾回收
Mix垃圾回收
jmap工具命令: jmap -dump:live,format=b,file=heap.bin 4308
Jconsole是一种基于JMX的可视化监视,管理工具。
在jdk的bin目录下可以看到,然后双击它打开监控界面。进来以后我们可以看到这个界面,概述页签主要是虚拟机主要运行数据的概览。有四个图,分别为堆,线程,类和CPU占有率,内存页签用于监视受收集器管理的虚拟机内存(java堆和永久代的)变化趋势,