JVM

初识JVM

JVM概念

JVM是Java Virtual Machine的简称。意为Java虚拟机

虚拟机指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统

有哪些虚拟机

  • VMWare
  • Visual Box
  • JVM

VMWare或者Visual Box都是使用软件模拟物理CPU的指令集

JVM使用软件模拟Java 字节码的指令集

Java和JVM的历史

1996年 SUN JDK 1.0 Classic VM

  纯解释运行,使用外挂进行JIT

1997年 JDK1.1 发布

  AWT、内部类、JDBC、RMI、反射

1998年 JDK1.2 Solaris Exact VM

  JIT 解释器混合

  Accurate Memory Management 精确内存管理,数据类型敏感

  提升的GC性能

2000年 JDK 1.3 Hotspot 作为默认虚拟机发布

2002年 JDK 1.4 Classic VM退出历史舞台

2004年发布 JDK1.5 即 JDK5 、J2SE 5 、Java 5

  泛型 注解 装箱 枚举 可变长的参数 Foreach循环

JDK1.6 JDK6

  脚本语言支持 JDBC 4.0 Java编译器 API

2011年 JDK7发布

  延误项目推出到JDK8 G1 动态语言增强 64位系统中的压缩指针 NIO 2.0

2014年 JDK8发布

  Lambda表达式 语法增强 Java类型注解

2016年JDK9 模块化

JVM规范

JVM主要定义二进制class文件和JVM指令集等

Class 文件格式

数字的内部表示和存储

returnAddress 数据类型定义

  指向操作码的指针。不对应Java数据类型,不能在运行时修改。Finally实现需要

定义PC

方法区

整数的表达

  补码

Float的表示与定义

VM指令集

  类型转化

    l2i

  出栈入栈操作

    aload astore

  运算

    iadd isub

  流程控制

    ifeq ifne

  函数调用

    invokevirtual invokeinterface  invokespecial  invokestatic 

JVM需要对Java Library 提供以下支持:

  反射 java.lang.reflect

  ClassLoader

  初始化class和interface

  安全相关 java.security

  多线程

  弱引用

JVM的编译

  源码到JVM指令的对应格式

  Javap

  JVM反汇编的格式

    <index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]

  

void spin() {
  int i; 
  for (i = 0; i < 100; i++) { ;
     // Loop body is empty
   }
 } 
0   iconst_0       // Push int constant 0
1   istore_1       // Store into local variable 1 (i=0)
2   goto 8         // First time through don't increment
5   iinc 1 1       // Increment local variable 1 by 1 (i++)
8   iload_1        // Push local variable 1 (i)
9   bipush 100     // Push int constant 100
11  if_icmplt 5    // Compare and loop if less than (i < 100)
14  return         // Return void when done

 

JVM运行机制

JVM启动流程

JVM基本结构

PC寄存器

  • 每个线程拥有一个PC寄存器
  • 在线程创建时 创建
  • 指向下一条指令的地址
  • 执行本地方法时,PC的值为undefined

属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

方法区

  • 保存装载的类信息
    • 类型的常量池
    • 字段,方法信息
    • 方法字节码
  • 通常和永久区(Perm)关联在一起

方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

JDK6时,String等常量信息置于方法 JDK7时,已经移动到了堆

 

Java堆

  • 和程序开发密切相关
  • 应用系统对象都保存在Java堆中
  • 所有线程共享Java堆
  • 对分代GC来说,堆也是分代的
  • GC的主要工作区间

Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

Java栈

  • 线程私有
  • 栈由一系列帧组成(因此Java栈也叫做帧栈)
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用创建一个帧,并压栈

属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程。

 

 

 操作数栈 Java没有寄存器,所有参数传递使用操作数栈

public static int add(int a,int b){
int c=0;
c=a+b;
return c;
}
 0:   iconst_0 // 0压栈
 1:   istore_2 // 弹出int,存放于局部变量2
 2:   iload_0  // 把局部变量0压栈
 3:   iload_1 // 局部变量1压栈
 4:   iadd      //弹出2个变量,求和,结果压栈
 5:   istore_2 //弹出结果,放于局部变量2
 6:   iload_2  //局部变量2压栈
 7:   ireturn   //返回

Java栈 – 栈上分配

什么是栈上分配?

   栈上分配主要是指在Java程序的执行过程中,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。 一般而言,创建对象都是从堆中来分配的,这里是指在栈上来分配空间给新创建的对象。

什么是逃逸?

   逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。

逃逸分析

   在JDK 6之后支持对象的栈上分析和逃逸分析,在JDK 7中完全支持栈上分配对象。 其是否打开逃逸分析依赖于以下JVM的设置:

-XX:+DoEscapeAnalysis

栈上分配与逃逸分析的关系

  进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从JVM内存模型中的堆来分配。

public class OnStackTest {
    public static void alloc(){
        byte[] b=new byte[2];
        b[0]=1;
    }
    public static void main(String[] args) {
        long b=System.currentTimeMillis();
        for(int i=0;i<100000000;i++){
            alloc();
        }
        long e=System.currentTimeMillis();
        System.out.println(e-b);
    }
}
-server -Xmx10m -Xms10m
-XX:+DoEscapeAnalysis -XX:+PrintGC

输出结果 5

-server -Xmx10m -Xms10m  
-XX:-DoEscapeAnalysis -XX:+PrintGC
……
[GC 3550K->478K(10240K), 0.0000977 secs]
[GC 3550K->478K(10240K), 0.0001361 secs]
[GC 3550K->478K(10240K), 0.0000963 secs]
564

栈上分配条件

  • 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
  • 直接分配在栈上,可以自动回收,减轻GC压力
  • 大对象或者逃逸对象无法栈上分配

栈、堆、方法区交互

public   class  AppMain    
 //运行时, jvm 把appmain的信息都放入方法区 
public   static   void  main(String[] args)  
    //main 方法本身放入方法区。 
    {
        Sample test1 = new  Sample( " 测试1 " );  
         //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面 
        Sample test2 = new  Sample( " 测试2 " ); 
        test1.printName(); 
        test2.printName(); 
    } 
}        
public   class  Sample       
 //运行时, jvm 把appmain的信息都放入方法区 
private  name;     
     //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里 
    public  Sample(String name) 
    {
         this .name = name; 
    } 
    //print方法本身放入 方法区里。
    public   void  printName() 
    {
        System.out.println(name); 
    }
}

内存模型

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

主内存

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

工作内存

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中 如果需要在其他线程中立即可见,需要使用 volatile 关键字

Java内存模型的承诺

 

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。

理解指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

编译器优化的重排

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

指令并行的重排

现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

内存系统的重排

由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题,下面分别阐明这两种重排优化可能带来的问题

编译器重排

下面我们简单看一个编译器重排的例子

线程 1             线程 2
1: x2 = a ;      3: x1 = b ;
2: b = 1;         4: a = 2 ;

两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现x1 = 1 和x2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况

线程 1              线程 2
2: b = 1;          4: a = 2 ; 
1:x2 = a ;        3: x1 = b ;

这种执行顺序下就有可能出现x1 = 1 和x2 = 2 的情况,这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

处理器指令重排

先了解一下指令重排的概念,处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下

  • 取指 IF
  • 译码和取寄存器操作数 ID
  • 执行或者有效地址计算 EX
  • 存储器访问 MEM
  • 写回 WB

CPU在工作时,需要将上述指令分为多个步骤依次执行(注意硬件不同有可能不一样),由于每一个步会使用到不同的硬件操作,比如取指时会只有PC寄存器和存储器,译码时会执行到指令寄存器组,执行时会执行ALU(算术逻辑单元)、写回时使用到寄存器组。为了提高硬件利用率,CPU指令是按流水线技术来执行的

常用JVM配置参数

Trace跟踪参数

  • -verbose:gc
  • -XX:+printGC可以打印GC的简要信息 
  • -XX:+PrintGCDetails 打印GC详细信息
  • -XX:+PrintGCTimeStamps 打印CG发生的时间戳
  • -Xloggc:log/gc.log 指定GC log的位置,以文件输出
  • -XX:+PrintHeapAtGC 每次一次GC后,都打印堆信息
  • -XX:+TraceClassLoading 监控类的加载
  • -XX:+PrintClassHistogram 按下Ctrl+Break后,打印类的信息:
  •  num     #instances         #bytes  class name
    ----------------------------------------------
       1:        890617      470266000  [B
       2:        890643       21375432  java.util.HashMap$Node
       3:        890608       14249728  java.lang.Long
       4:            13        8389712  [Ljava.util.HashMap$Node;
       5:          2062         371680  [C
       6:           463          41904  java.lang.Class

    分别显示:序号、实例数量、总大小、类型

 

堆的分配参数

  • -Xmx –Xms 指定最大堆和最小堆
  • -Xmn 设置新生代大小
  • -XX:NewRatio 新生代(eden+2*s)和老年代(不包含永久区)的比值 4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
  • -XX:SurvivorRatio 设置两个Survivor区和eden的比 8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
public static void main(String[] args) {
   byte[] b=null;
   for(int i=0;i<10;i++)
       b=new byte[1*1024*1024];
}
-Xmx20m -Xms20m -Xmn1m  -XX:+PrintGCDetails 
  • 没有触发GC 全部分配在老年代
-Xmx20m -Xms20m -Xmn15m  -XX:+PrintGCDetails
  • 没有触发GC 全部分配在eden 老年代没有使用
-Xmx20m -Xms20m –Xmn7m  -XX:+PrintGCDetails 
  • 进行了2次新生代GC
  • s0 s1 太小需要老年代担保
-Xmx20m -Xms20m -Xmn7m   -XX:SurvivorRatio=2 -XX:+PrintGCDetails
  • 进行了3次新生代GC
  • s0 s1 增大
-Xmx20m -Xms20m -XX:NewRatio=1   
-XX:SurvivorRatio=2 -XX:+PrintGCDetails

 

 

-Xmx20m -Xms20m -XX:NewRatio=1   
-XX:SurvivorRatio=3 -XX:+PrintGCDetails

 

  • -XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件
  • -XX:+HeapDumpPath 导出OOM的路径
  • -XX:OnOutOfMemoryError 在OOM时,执行一个脚本
    • "-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p“
    • 当程序OOM时,在D:/a.txt中将会生成线程的dump 可以在OOM时,发送邮件,甚至是重启程序

 

堆的分配参数 – 总结

  • 根据实际事情调整新生代和幸存代的大小
  • 官方推荐新生代占堆的3/8
  • 幸存代占新生代的1/10
  • 在OOM时,记得Dump出堆,确保可以排查现场问题

永久区分配参数

  • -XX:PermSize -XX:MaxPermSize 设置永久区的初始空间和最大空间 他们表示,一个系统可以容纳多少个类型

栈的分配参数

  • -Xss 
    • 通常只有几百K
    • 决定了函数调用的深度
    • 每个线程都有独立的栈空间
    • 局部变量、参数 分配在栈上
public class TestStackDeep {
    private static int count=0;
    public static void recursion(long a,long b,long c){
        long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
        count++;
        recursion(a,b,c);
    }
    public static void main(String args[]){
        try{
            recursion(0L,0L,0L);
        }catch(Throwable e){
            System.out.println("deep of calling = "+count);
            e.printStackTrace();
        }
    }
}
递归调用
-Xss128K
deep of calling = 701
java.lang.StackOverflowError

-Xss256K
deep of calling = 1817
java.lang.StackOverflowError

 

GC 算法与种类

GC的概念

  • Garbage Collection 垃圾收集
  • 1960年 List 使用了GC
  • Java中,GC的对象是堆空间和永久区

GC算法

引用计数法

比较古老的回收算法。

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数法的问题

  • 引用和去引用伴随加法和减法,影响性能
  • 很难处理循环引用
  • 没有被Java采用

 

 

标记清除

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

 

标记压缩

 标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

 

标记压缩对标记清除而言,有什么优缺点呢?

优点:能够整理内存碎片,避免分配大对象时,空间不够导致FullGC

缺点:压缩阶段,由于移动了可用对象,需要去更新引用

 

复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法

不适用于存活对象较多的场合 如老年代

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

复制算法的最大问题是:空间浪费 整合标记清理思想

 

分代思想

  • 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
  • 根据不同代的特点,选取合适的收集算法
    • 少量对象存活,适合复制算法
    • 大量对象存活,适合标记清理或者标记压缩

  

可触及性

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义

  • 可触及的
    • 从根节点可以触及到这个对象
  • 可复活的
    • 一旦所有引用被释放,就是可复活状态
    • 因为在finalize()中可能复活该对象
  • 不可触及的
    • 在finalize()后,可能会进入不可触及状态
    • 不可触及的对象不可能复活
    • 可以回收

  • 栈中引用的对象
  • 方法区中静态成员或者常量引用的对象(全局对象)
  • JNI方法栈中引用对象
public class CanReliveObj {
    public static CanReliveObj obj;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("CanReliveObj finalize called");
        obj=this;
    }
    @Override
    public String toString(){
        return "I am CanReliveObj";
    }
public static void main(String[] args) throws InterruptedException{
  obj=new CanReliveObj();
  obj=null;   //可复活
  System.gc();
  Thread.sleep(1000);
  if(obj==null){
     System.out.println("obj 是 null");
  }else{
     System.out.println("obj 可用");
  }
  System.out.println("第二次gc");
  obj=null;    //不可复活
  System.gc();
  Thread.sleep(1000);
  if(obj==null){
    System.out.println("obj 是 null");
  }else{
    System.out.println("obj 可用");
  }
}
CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null

 

经验:避免使用finalize(),操作不慎可能导致错误。

优先级低,何时被调用, 不确定 何时发生GC不确定

可以使用try-catch-finally来替代它

Stop-The-World

  • Java中一种全局暂停的现象
  • 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
  • 多半由于GC引起
    • Dump线程
    • 死锁检查
    • 堆Dump

GC时为什么会有全局停顿? 类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。

危害:长时间服务停止,没有响应 遇到HA系统,可能引起主备切换,严重危害生产环境。

 

GC参数

串行收集器

  • 最古老,最稳定
  • 效率高
  • 可能会产生较长的停顿
  • -XX:+UseSerialGC
    • 新生代、老年代使用串行回收
    • 新生代复制算法
    • 老年代标记-压缩

并行收集器

ParNew

  • -XX:+UseParNewGC
    • 新生代并行
    • 老年代串行
  • Serial收集器新生代的并行版本
  • 复制算法
  • 多线程,需要多核支持
  • -XX:ParallelGCThreads 限制线程数量

 

Parallel收集器

  • 类似ParNew
  • 新生代复制算法
  • 老年代 标记-压缩
  • 更加关注吞吐量
  • -XX:+UseParallelGC
    • 使用Parallel收集器+ 老年代串行
  • -XX:+UseParallelOldGC
    • 使用Parallel收集器+ 并行老年代

 

  • -XX:MaxGCPauseMills
    • 最大停顿时间,单位毫秒
    • GC尽力保证回收时间不超过设定值
  • -XX:GCTimeRatio
    • 0-100的取值范围
    • 垃圾收集时间占总时间的比
    • 默认99,即最大允许1%时间做GC

这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

CMS收集器

  • Concurrent Mark Sweep 并发标记清除
  • 标记-清除算法
  • 与标记-压缩相比
  • 并发阶段会降低吞吐量
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC

 

CMS运行过程比较复杂,着重实现了标记的过程,可分为

  • 初始标记
    • 根可以直接关联到的对象
    • 速度快
  • 并发标记(和用户线程一起)
    • 主要标记过程,标记全部对象
  • 重新标记
    • 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  • 并发清除(和用户线程一起)
    • 基于标记结果,直接清理对象

特点

  • 尽可能降低停顿
  • 会影响系统整体吞吐量和性能
    • 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
  • 清理不彻底
    • 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
  • 因为和用户线程一起运行,不能在空间快满时再清理
    • -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
    • 如果不幸内存预留空间不够,就会引起concurrent mode failure

 

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理

  • 整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction

  • 设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

  • 设定CMS的线程数量

GC参数整理

  • -XX:+UseSerialGC:在新生代和老年代使用串行收集器
  • -XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
  • -XX:NewRatio:新生代和老年代的比
  • -XX:+UseParNewGC:在新生代使用并行收集器
  • -XX:+UseParallelGC :新生代使用并行回收收集器
  • -XX:+UseParallelOldGC:老年代使用并行回收收集器
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数
  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
  • -XX:ParallelCMSThreads:设定CMS的线程数量
  • -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
  • -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

  • -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

  • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

  • -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收

  • -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

 

 类装载器

class装载验证流程

  • 加载
  • 链接
    • 验证
    • 准备
    • 解析
  • 初始化

class装载验证流程 -加载

装载类的第一个阶段
取得类的二进制流
转为方法区数据结构
在Java堆中生成对应的java.lang.Class对象

class装载验证流程 -链接 验证

目的:保证Class流的格式是正确的
文件格式的验证

是否以0xCAFEBABE开头
版本号是否合理

元数据验证

是否有父类
继承了final类?
非抽象类实现了所有的抽象方法

字节码验证 (很复杂)

运行检查
栈数据类型和操作码数据参数吻合
跳转指令指定到合理的位置

符号引用验证

常量池中描述类是否存在
访问的方法或字段是否存在且有足够的权限

class装载验证流程 -链接 准备

分配内存,并为类设置初始值 (方法区中)

public static int v=1;
在准备阶段中,v会被设置为0
在初始化的<clinit>中才会被设置为1
对于static final类型,在准备阶段就会被赋上正确的值
public static final int v=1;

 

class装载验证流程 - 链接 解析

class装载验证流程 – 初始化

 执行类构造器<clinit>

static变量 赋值语句
static{}语句

子类的<clinit>调用前保证父类的<clinit>被调用
<clinit>是线程安全的

什么是类装载器ClassLoader

 ClassLoader是一个抽象类
 ClassLoader的实例将读入Java字节码将类装载到JVM中
 ClassLoader可以定制,满足不同的字节码流获取方式
 ClassLoader负责类装载过程中的加载阶段


JDK中ClassLoader默认设计模式– 分类

 BootStrap ClassLoader (启动ClassLoader)
 Extension ClassLoader (扩展ClassLoader)
 App ClassLoader (应用ClassLoader/系统ClassLoader)
 Custom ClassLoader(自定义ClassLoader)
 每个ClassLoader都有一个Parent作为父亲

JDK中ClassLoader默认设计模式 – 协同工作

Thread. setContextClassLoader()
上下文加载器
是一个角色
用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

  

 

posted @ 2018-11-12 22:07  Mr.Aaron  阅读(273)  评论(0编辑  收藏  举报