java目前使用的是hot spot jvm引擎
自带JIT编译器,可以将频繁执行的方法侦测为热点代码,增加执行速度。
以前的编译器,每行都要将代码编译为字节码文件,所以非常慢。
逃逸分析
也就是将局部的生命周期的对象暴露给外部。
class A {
public static B b;
public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸
b = new B();
}
public B methodPointerEscape() { // 方法返回值,发生逃逸
return new B();
}
public void instancePassPointerEscape() {
methodPointerEscape().printClassName( this ); // 实例引用传递,发生逃逸
}
}
class B {
public void printClassName(A a) {
System.out.println(a. class .getName());
}
}
逃逸分析的jvm解决方案
按照以往的jvm的实现原理就是将变量分配内存在堆上,然后将对象的指针压入到栈中。由于GC回收遍历引用树,对象过多影响效率。
逃逸优化jvm解决方案
直接将变量实例内存分配在栈上,当线程结束后,回收变量。栈是每一个线程独立拥有的,堆是线程共享的。
如果在方法上有同步锁的话,在逃逸分析时,如果只有一个线程访问,那么将自动去掉同步锁。
Jvm内存分配
线程计数器
用来记录线程中上次运行的位置,在切换后保证线程恢复到上次运行的位置。线程技术器是线程私有的。各条线程之间互不影响。
Java虚拟机栈
栈帧 :局部变量表,操作数栈,动态链接,方法出口
局部变量表:boolean byte char short int float long double 对象引用reference(存储对象引用指针也可能是一个句柄)
局部变量表在编译时期就确定需要的内存,long double占用两个变量地址,其他都占用一个变量地址。当进入一个方法所需要的空间是固定的。
当线程在请求时超过栈最大深度,请求不到空间为变量分配内存就会StackOverFlowError异常,前提是固定大小栈内存,现在大部分的虚拟机都是支持动态扩展的。如果动态扩展不够申请到足够大的空间的话,也会OOM异常。
句柄:相当于定位一段代码或者变量位置的值,像我们打开门需要门把手,但是如果多个门把手会引发混乱。
本地方法栈
运行native方法,提供本地服务。类似java虚拟机栈,也会抛出StackOverFlowError和OOM异常
Java堆
对象实例存放的位置,也是GC回收器主要回收的地方。当java堆内存不能再为对象分配足够的内存的话,将抛出OOM异常。
当然也不是所有的对象实例都会分配在堆上,会有逃逸对象,标量替换,直接在栈中分配。逃逸分析原来的解决方案在堆上创建一个对象实例,将引用到处传递,在GC进行回收的时候遍历引用树,严重影响速度。
java堆中会被分成多个代,如新生代,老年代等这些只是为了标记出来方便GC进行垃圾回收。
方法区
是堆的一部分,但是逻辑上进行区分,不同的jvm实现不同,有的对这部门的垃圾回收不是那么严格,这一部分的垃圾回收应该是比较严苛的,因为关系到类的卸载。
用于存放类加载后的信息,如一些类信息,常量,静态变量。方法区也是线程共享的一块区域,他也被称为元空间或者永久代,例如我们在使用eclipse的时候重复启动较大的项目,如我们公司项目有三万多个类,就会抛出元空间permanent 的内存溢出。当无法申请到足够的空间的时候会抛出OOM异常。
运行时常量池
运行时常量池是方法区的一部分,主要存放的有类的版本,字段,方法,接口等描述信息还有用于存放编译时期的字面量和符号引用等。字面量有显示初始化的字符串以及自动装箱拆箱中定义的字面量如integer是-128-127
运行时常量池并不是只存放编译器,由于java中的类具有动态性,当然java中也提供动态将常量放入到常量池中如string的intern()方法。
直接内存
如jdk1.4添加的NIO是直接操作内存,这部分内存是独立于堆之外的内存,确切的来说他不属于虚拟机的内存。NIO是一个基于通道以及缓冲区的直接操作内存的新的input/output类。虽然这个堆外内存不受虚拟机的内存限制,但是内存都是存在于计算机的内存范围之内的,如果内存无法再动态扩展那么也会产生OOM异常。
对象的新建
这里的对象新建不指数组对象(数组对象的地址是数组第一个对象的地址),class对象。新建对象new 克隆或者序列化
克隆:分为浅拷贝和深拷贝
浅拷贝如果一个对象中有另外的对象那么拷贝的是另外对象的引用
深拷贝 要实现clone方法 那么就是新建一个那个另外的对象
检测到new关键字先去方法区查找是否有该类。
指针碰撞分配对象地址:在堆内存中如果分配的对象是规整的,java堆内存如果是规整的,用过的在一边,没有用过的在另外一边。对于对象也是规整的那么分配的时候就是将对象大小的内存向空闲内存移动指定大小。(有可能会有线程问题)
空闲列表分配对象地址:对于java堆中的内存如果不是规整的话,那么jvm就要维护一张空闲列表,在分配地址时在空闲列表中找到足够的分配空间,然后更新空闲列表。
我们知道在java中创建对象的动作是非常频繁的,那么对于这种多线程的操作就要注意如果产生更改或者新增的操作就可能会发生线程安全问题。可能在指针还未给对象A分配完地址,又去用原来的指针去给对象B分配地址,这个时候就会产生问题。
那么jvm还有一种实现方式,使用线程本地缓冲区TLAB,把分配内存的动作按照线程分为不同的空间进行,每个线程在java堆中预先预留一些内存就是线程本地缓存区了,哪个线程需要分配地址的话就在哪个线程的TLAB上分配,如果这块内存用完了,就要进行同步锁定了。为线程分配TLAB的动作也是线程不安全的。jvm是否使用TLAB模式进行对象的分配,-XX:+/-UseTLAB设定。
后序步骤还有包括对对象的初始化,在对象创建之初,所有的属性都是默认值。
创建对象hotspot中的解释器创建步骤:(大致)
1.检测到new关键字时,确保常量池是否加载过此类
2.确保对象的所属属性已经被初始化
3.取对象的长度
4.记录是否需要将对象所有字段置为默认值
5.是否在TLAB中分配地址
6.直接在eden(年轻代)分配对象
7.通过cas方式分配地址,并发失败的话重试retry,知道成功
8.如果需要将对象置为默认值
9.是否使用偏向锁设置对象头信息
10.将对象引入栈,执行下一条指令
对象内存的分布
1.对象头
存放对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id
偏向时间戳。这部分数据分为32位和64位虚拟机,分别为32bit和64bit
官方称为"mark word"
对象头的另一部分是类型指针,指向类元数据的指针,通过这个指针判断这个对象属于哪个类型。
如果是数组的话,还要有一部分存储数组的长度。
2.实例数据
这一部分内容存储的是我们对象属性的实际值,这里存储的顺序是收到jvm的定义和我们定义变量的顺序来决定的。
longs/doubles ints shorts/chars bytes/booleans oops
相同宽度的字段都会被分在一起,在满足这个前提下,父类中定义的变量会出现在子类中。
如果compactFields参数为true的话 那么子类之中较窄的变量也可能会插入到父类的变量间隙中
3.对齐填充
hotspot中的变量长度一般都是8字节的整数倍,对齐填充就是如果对象大小不是8字节的整数倍,自动通过对齐填充进行填充。
对象地址访问
句柄类型访问对象使用句柄定位对象地址,句柄中存放对象实例和类型地址 reference只需要指向句柄即可,那么在对象变更时只需要改变句柄记录地址即可。例如在垃圾回收时对象可能被移动。这样是一种稳定的对象地址访问方式
指针访问对象reference直接记录对象实例的地址,直接指针访问速度较快,减少了一次指针定位的性能开销,由于java是面向对象的语言,对于对象访问时非常频繁的,积少成多。
OOM异常
1.java堆内存溢出
-xms指最小内存 -xmx如果和xms设置为一样大的话就是禁止内存水平扩展
-XX:+HeapDumpOnOutOfMemoryError 可以让发生内存溢出之时将异常dump出当前内存转储快照。
保证对象可达不会被GC回收,当对象数量过多无法在内存中分配内存时就会出现OOM异常
2.虚拟机栈和本地方法区栈的内存溢出
线程请求深度大于栈最大深度 抛出Stack Overflow
虚拟机在扩展栈时无法申请到足够的空间 抛出OutOfMemoryError
在单线程时抛出的都是Stack Overflow
在多线程时,由于每个栈对于线程来说都是独有的。栈空间的大小是用操作系统的内存大小-堆内存大小-方法区大小。这个时候如果每个栈空间通过参数设置固定大小,那么多线程数量就是固定的。如果想提高线程数量就要通过设置栈空间内存大小来提高,调低栈空间的大小就可以提高线程数量。
3.方法区和运行时常量池溢出
String.intern()是一个native方法,如果字符串常量已经包含一个等于此string对象的字符串,返回这个池中的string对象,如果没有的话 ,先将其添加到池中,然后将引用返回。
intern
jdk1.6环境下这段代码会是两个false 而jdk1.7会是一个true一个false
在jdk1.6环境下,是因为intern会把首次遇到的字符串实例复制到永久代中,返回字符串引用和堆中的引用必然不是一个。
而jdk1.7中是会记录首次出现的实例引用,因此第一个为true 返回的是同一引用
下一条因为在tostring之前java字符串已经作为关键字在常量池中了,所以返回的实例是常量池中的引用和堆上不同。
方法区存的是类相关信息,所以加载很多类的情况下方法区会占用很多内存。这个是会经常出现在我们的实际运用中,spring,hibernate框架都是通过动态代理产生类到方法区中。如果使用cglib动态增强类这些类信息都会加载进方法区。如果jsp过多也会出现内存溢出,因为jsp第一次被访问时会编译成为一个类。
cglib4.直接内存溢出
使用NIO可能会出现该问题,因为NIO操作的是本机内存是独立于堆内存之外的。
特点DUMP文件内存较小。
垃圾收集器和内存分配策略
都知道的我们java程序员是不用去管理内存的,为开发提供了很大的遍历,但是作为一名优秀的程序员必须要去了解内存的分配策略和无用对象是如何被回收的。这对我们去排查内存泄漏和内存泄漏问题时,当我们面对更高的并发量时都会运用到这一部分的知识。
线程所私有的内存,程序计数器,本地方法栈,虚拟机栈都是随着栈帧的入栈和出栈内存就会被自动的回收。所以需要关注的,最麻烦的一部分也就是线程共享的内存,也就是堆以及方法区中的对象。
1.对象已死吗?
如何判断对象是否死亡,也就是说无用对象的判断,常用的有几种算法。
1.引用计数算法
在最经典的算法,也是大多数人都知道的算法,引用计数算法,简单点来说的话,就是如果该对象有被其他地方引用的话,那么引用数量+1,如果减少一个引用-1,直到该对象上没有引用,那么判断该对象已经死亡。
public class ReferenceCountGC {
public Object instance = null;
private static final int _1MB = 1024*1024;
private byte[] bigsize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountGC instanceA = new ReferenceCountGC();
ReferenceCountGC instanceB = new ReferenceCountGC();
instanceA.instance = instanceB;
instanceB.instance = instanceA;
instanceA = null;
instanceB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
GC日志
查看GC日志,我们发现虽然instanceA和instanceB互相引用,但是GC并没有因此而不回收他们两个,那么证明GC判断对象是否存活并不是使用这种算法。
如何查看GC?
使用eclipse时,在jdk的配置中添加 一些配置即可
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2018-05-16T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
2.可达性分析算法
以GCROOT为根节点判断,其他对象是否在引用链上,如果在引用链上,判定该对象存活,否则的话该对象为无用对象。
黑乎乎的是GCROOT对象。
GC引用判断对象是否存活
什么是GCROOT对象?什么样的对象可以成为GCROOT对象呢?
1.虚拟机栈 栈帧中的本地变量表 中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(也就是native方法)引用的对象
那么在java中什么叫引用?
在jdk1.2之前,他的定义是指使用引用去记录内存中的一块地址。在jdk1.2后,他类似缓存,在内存不紧张的时候就记录内存中地址,而当内存紧张时,就会被回收。抛弃这些对象。
引用的类型及划分的界限
强引用:指代码中普遍存在的,A a = new A(); 这种类型的引用,只要有强引用那么该引用就不会被回收。
软引用:描述一些 有用但是非必须对象,但是它的强度比强引用弱一些,在系统将要发挥内存溢出异常之前,将会将这些对象列进 回收范围之中进行第二次回收,如果这次回收没有清除而使内存足够的话,那么抛出异常
SoftReference类来实现软引用
弱引用:被弱引用关联的对象只能生存到下一次发生垃圾回收之前,无论内存是否足够都会被回收。
WeakReference类来实现弱引用
虚引用:最弱的一种引用,无法通过虚引用取得一个对象实例,为一个对象设置虚引用关联的目的就是在对象被回收时收到一个系统通知。
PhantomReference
对象生存还是死亡?
对象的自我拯救
要真正宣判一个对象死亡,必须要经过两次标记的过程,如果第一次分析该对象是否在GCROOT上,如果不存在的话,判断对象是否有必要执行finalize()方法,当该对象没有覆盖finalize()方法或者finalize()方法被执行过以后那么这一次对象如果不在GCROOT上是对象真正死亡的时刻。
另一方面,如果该对象有必要执行finalize()方法的话,将该对象加入到一个F-Quence队列中,并且虚拟机会开辟一条线程finalizer去单独执行他,但是jvm并不保证这个finalize方法能够执行完,因为如果我们在finalize方法中让其陷入死循环或者其他不安全操作,jvm会崩溃,所以F-Quence队列中的其他任务会一直陷入等待,所以是不能保证finalize方法全部执行完,如果在finalize方法中被回收对象与GCROOT上的对象建立联系的话,那么该对象就自我拯救成功,从回收集合中移除,负责的话对象会被回收。所以finalize方法是一个自我拯救的机会,能否拯救成功要看自己进行了什么操作,如将thisfu赋值给某个类变量是可以拯救成功的。下面上代码,看如何拯救和拯救的次数。
public class EscapseGC {
private static EscapseGC save_hook = null;
public void isAlive(){
System.out.println("是的,我还存活着。");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("我在自我拯救中!");
EscapseGC.save_hook = this;
}
public static void main(String[] args) throws Throwable {
save_hook = new EscapseGC();
//触发第一次死亡
save_hook = null;
//触发第二次死亡
System.gc();
//自我拯救
Thread.sleep(500);
if(save_hook!=null){
save_hook.isAlive();
}else{
System.out.println("我已经死亡!");
}
//在自我拯救之后触发无法再次自我拯救
save_hook = null;
System.gc();
Thread.sleep(500);
if(save_hook!=null){
save_hook.isAlive();
}else{
System.out.println("我已经死亡!");
}
}
}
运行结果
[GC 1331K->648K(124416K), 0.0008937 secs]
[Full GC 648K->464K(124416K), 0.0069402 secs]
我在自我拯救中!
是的,我还存活着。
我已经死亡!
从上图中我们可以看到第二次并没有执行自我拯救方法,jvm检测到他已经不是第一次执行了。但是第一次是成功的。
虽然这个方法有自我拯救的效果,但是执行该方法是要产生很大的性能开销以及不安全的代价,所以尽量不使用它。
回收方法区
方法区中存放着类信息以及常量信息,但是对于jvm来说类的加载和卸载都需要很严谨的条件,对于我们使用hibernate,spring,osgi等等动态产生代理类,回收方法区才能保证不抛出内存不够的异常。判断常量无用较简单,只需要判断常量是否有引用指向他即可,但是对于类是否无用需要判断很多方面
1.该类的所有实例均被回收,既堆中无该类的实例。
2.加载该类的classloader均被回收。
3.该类对应的java.lang.class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
垃圾回收算法
1.标记-清除算法
从这个回收算法的名字我们大概能猜测出他的工作方式,也就是将需要回收的对象进行标记,然后在完成标记之后将对象回收,但是这个回收方式有很大的缺陷,回收之后会有很多的空间是被浪费的,空间是断裂的,当堆中无法为所需空间大小分配大小就会触发另一次的垃圾回收。
标记-清除
2.复制算法
复制算法是为了解决标记-清除算法中出现空间碎片的问题,将内存划分为等大的大小,每次只使用其中一块,在回收之前将不需要回收的移动到另外一块闲置内存,然后将需要回收的那块内存全部回收,这样就不会产生内存碎片。但是在jvm中的新生代的对象大多数都是朝生夕死的,他们的寿命很短,所以不需要1:1的内存空间分配,这样eden内存和survivor内存的比重大概是8:1。但是有两个survivor内存,也就是说每次分配对象的内存是一个survivor内存空间+eden内存空间。即总空间的90%。
不是每一次都能保证占比为1的survivor内存空间都能放下所有存活的空间,当他不够用时,我们就需要依赖老年代进行分配担保。他们会被直接分配到老年代。
3.标记-整理算法
当回收的频率很频繁的时候,复制算法就会效率很低,并且他浪费了50%的内存。所以在老年代的回收使用标记-整理算法来回收垃圾。如果不想浪费50%的空间,就需要有额外的担保空间,以应对被使用的内存中100%对象都存活的情况。
标记-整理算法就是将存活对象移动到内存的一端。然后直接清除掉端边界以外的内存。
标记-整理
4.分代收集算法
将对象分为新生代和老年代,新生代回收频率高且朝生夕死的特点使用复制算法,老年代的回收频率低就是用标记-整理算法。
HotSpot的算法
Q:枚举根节点GCROOT是什么?
在判断对象是否存活用到了GCROOT这个名词,那么什么是GCROOT,什么样的内存可以作为根节点?
可以作为GCROOT根节点的一般有全局性的引用(例如常量或者类静态属性) 与执行上下文(栈帧中的本地变量表)。现在主流的jvm方法区一般都有几百M,逐个检查引用肯定要很长时间,所以检查引用有特殊的地点停顿。
在这个停顿的时刻必须保证所有的对象关系不能改变,否则就失去了停顿的意义。这个时刻在Java中称为“Stop The World”,即使在号称不停顿的CMS收集器中,枚举根节点时也是必须停顿的。
在停顿之后,虚拟机如何获得对象引用,遍历整个全局性引用与执行上下文?不要担心,jvm的HotSpot实现中,使用了oopmap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就将对象内多少偏移量上是什么类型的数据类型计算出。在JIT编译时,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样在GC扫描时就可以直接获取这些信息。
Q:在哪些地点进行对枚举根节点的扫描
扫描根节点都是在安全点进行扫描,什么地方可以成为安全点?
在oopmap数据结构的的协助下,hotspot可以快速定位到GCROOTS但是如果为每一条指令都生成oopmap来保证引用关系的实时更新,那么性能将会开销很多在这一部分工作上。
实际上hotspot并没有为每一条指令都生成oopmap,前面已经提到在特定的地点生成数据结构。这些地点称之为安全点,安全点的不能太少导致gc等待时间过长,也不能频繁导致过分增大运行时的负荷。所以安全点的选定基本在程序"是否具有让程序长时间执行的特征"
长时间的执行的特征大概是在方法调用、循环跳转、异常跳转等所以这些功能的指令才会产生安全点。
对于安全点来说 ,如何在gc时让所有的线程(不包括执行jni的线程)都跑到最近的安全点再停顿下来,一种是抢先式中断,和主动式中断。
抢先式中断就是当发生gc时,当有线程不在安全点时,就继续让这些线程跑到最近的安全点,现在几乎没有虚拟机采用抢先式中断。
主动式中断,不直接操作线程,只是设置一个标志,所有的线程去轮询这个标志,发现轮询标志为真时,就自己挂起中断,轮询标志是和安全点重合的。
Q:安全区域
安全点似乎能完美解决扫描GCROOT的问题,但是他不能保证保证安全点在合适的时间间隔就执行一次,在线程在sleep或者是blocked状态的话,这时线程是无法做出对jvm的中断请求的响应的。走到安全点就中断,jvm也不会为线程分配cpu时间。对于这种情况需要安全区域来解决。
安全区域指在一段代码之中,引用关系不会发生变化。这个区域中的任何地方开始gc都是安全的,可以将安全区域看做扩展的安全点。
在线程执行安全区域中的代码时,首先标识自己进入到了安全区域。这样的话,JVM在这段安全区域中执行GC的话,就不用去管标识自己在安全区域中的线程了,在线程离开安全区域时,他要检查系统是否已经完成了枚举根节点或者是整个GC过程。如果完成了可以继续执行,否则就必须等待直到收到可以离开安全区域的通知。
垃圾收集器
垃圾收集器各个厂商的实现的机制和算法都不尽相同。所以他们之间有很大的差别。这里讨论的垃圾收集器基于jdk 1.7 update14之后的hotspot虚拟机(在这个版本正式提供了商用的g1垃圾回收器)
hotspot虚拟机的所有收集器
如果两个收集器之间是实线连接证明两个收集器是可以搭配使用的,收集器所处区域表示他们是新生代还是老年代。
这里更证明了没有完美的收集器,hotspot之中有这么多收集器是为了应对各种场景。
serial收集器
serial收集器是最基本的,历史最悠久的收集器。这个收集器是一个单线程的收集器,这个单线程并不代表只会使用一个cpu或者一条线程去完成垃圾回收工作,指的是在进行垃圾收集时,其他所有的工作线程都会被停止,也就是java中的"stop the world"
serial系列收集器工作方式
上图分别是serial和serial old收集器的收集方式和使用的算法。stop the world就像清理磁盘时,一边清理一边电脑产生新的垃圾,这样磁盘是清理不完了。
serial收集器对于其他收集器来说,与其他的收集器单线程想比他更加简单和高效。对于限定单个cpu环境来说,serial不会和其他线程交互的额外性能开销,所以是一个高效的收集器,在client模式下的虚拟机来说是一个很好的选择。client模式也就是客户端,类似桌面应用。
parnew收集器
ParNew收集器其实是在serial收集器的多线程版本,其余所有包括可控参数,收集算法以及对象分配的规则和回收策略都和serial收集器一样
可用的可控参数
(-xx:survivorRatio)新生代中survivor比率,默认和新生代的比率是1:8
(-xx:HandlePromotionFailure)是否准许担保失败
(-xx:PretenureSizeThreshold)设置大对象进入老年代的阈值,次设置支队serial 和parNew有效
等...
parNew的工作方式
Tips:新生代的对象大多数生命周期都非常的短,如果在临近的一次minor gc中,首先会检查老年代的最大连续可用空间是否
能够装下新生代所有的对象,如果可以的话,此次的minor gc是安全的。但是如果有很多对象存活或者有相
对大对象的存在,在用复制算法进行整理内存的时候,如果survivor区中空间不够存放这些对象的话,检查虚
拟机的配置HandlePromotionFailure,是否准许担保失败,如果准许的话,就会计算以往每一次进入老年代的
平均值,如果小于这个平均值就能够担保成功,直接进入老年代。如果大于这个平均值的话,老年代会进行
一次full gc为此次minor gc腾出足够的空间,这也是可能导致系统卡顿的一大原因。
该垃圾回收器的搭配和适用范围
此垃圾回收器大多数用在server中,由上面的图来看除了serial的垃圾回收器和并发的垃圾回收器可以配合使用以外,就只有parnew垃圾回收器可以配合和他使用。他也是使用cms的垃圾收集器之后默认使用的新生代收集器。
-xx:+UseConcMarkSweepGC后制定的收集器
也可以使用参数-xx:+UseParNewGC强制使用
在ParNew进行垃圾回收的时候他默认使用与cpu相同数量的线程数来进行垃圾回收。
可以使用-xx:ParallelGCThreads来限制 默认的垃圾回收的线程数
Parallel Scavenge收集器
该收集器是一个新生代收集器,也是使用复制算法的并行多线程垃圾收集器,看上去和ParNew的垃圾收集器一致。
但是该收集器的专注点和其他收集器不同,cms收集器关注的如何缩短垃圾回收时如何缩短用户线程的停顿时间。而Parallel Scavenge是达到一个可以控制的吞吐量。所谓吞吐量是 (cpu运行用户代码时间/cpu总消耗时间)。
在这里就是(运行用户代码)/(运行用户代码+垃圾回收时间)如果虚拟机运行了100分钟,其中垃圾回收花费了1分钟,那吞吐量就是99%。
该收集器提供两个参数来控制吞吐量。-xx:MaxGCPauseMillis是最大的垃圾停顿时间
-xx:GCTimeRatio设置吞吐量大小。
设置最大的垃圾停顿时间>0,收集器会尽量在该时间内将垃圾回收,不要认为垃圾停顿时间越小越好,因为垃圾回收时间是牺牲吞吐量和新生代空间来换取的。在新生代空间变小的时候虽然垃圾回收时间变短,但是垃圾回收的频率却越来越高。
GCTimeRatio的参数值是一个 值应当大于0小于100的整数,他是吞吐量的倒数。如果该参数设置为19
的话,那么最大的GC时间就占总时间的5% = 1/(1+19)
该收集器称为吞吐量优先收集器。该收集器还有一个参数-xx:+UseAdaptiveSizePolicy参数是一个开关参数,当这个参数打开后,-Xmn不需手动指定新生代大小 -xx:SurvivorRatio eden和survivor区的比例, -xx:PretenureSizeThreshold 直接晋升老年代的大小等参数,将会根据系统自动自适应调整。
网友评论