jvm位置

  jvm是位于操作系统上的平台,也正是因为这样,java才是跨平台的编程语言。

jvm结构体系

  • java栈:服务对象是jvm中java方法。
      栈 Stack:主管java程序的运行,在线程创建的时候创建,它的生命周期是跟随线程的生命周期,线程销毁,该线程的栈也随之销毁,所以对于栈来说不存在垃圾回收问题。8种基本类型的变 量+对象的引用变量+实例方法都是在函数的栈内存中分配。
      主要存储方法入参、出参、函数变量和出栈、入栈的操作。
  • 本地方法栈:服务对象是jvm中native方法。
  • 程序计数器:每个线程都有一个程序计数器,是线程私有的,就是一个指针。用来指向下一条即将要执行指令的地址,由执行引擎读取下一条指令,使用空间非常小,几乎忽略不计。
  • 执行引擎:执行引擎负责解释命令,提交操作系统执行
  • Native Interface 本地接口:本地接口的作用是融合不同的编程语言为 Java 所用
  • 方法区:方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定 义的方法的信息都保存在该区域,此区属于共享区间。 静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中。
  • 运行每个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈。

对象的结构

  • Header(对象头):自身运行时数据(Mark Word)
    1.哈希值
    2.GC分代年龄
    3.锁状态标志
    4.线程持有锁
    5.偏向锁ID
    6.偏向时间戳
    7.类型指针
    8.数组长度:只有数组对象才有
  • InstanceData
    相同宽度的数据分配到一起(long、double)
  • Padding (对齐填充)
    8个字节的整数倍。

Mark Word

字节码操作指令

字节码与数据类型

  1. 在虚拟机的指令集中,大多数的指令包含了其操作所对应的数据类型信息。
  2. iLoad:从局部变量表中加载int型数据到操作数栈。
  3. 大多数指令包含类型信息。
  4. 类型多,指令少。

加载与存储指令

  1. 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈直接来回传输。
  2. 将局部变量表加载到操作数栈: iload lload fload dload aload。
  3. 将一个数值从操作数栈存储到局部变量表:istore :lfda。
  4. 将一个常量加载到操作数栈:bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst。
  5. 扩充局部变量表的访问索引的指令:wide。

运算指令

  1. 运算或算术指令用于对两个操作数栈上的值进行某种特定的运算,并把结果存储到操作数栈顶。

类型转换指令

  1. 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
  2. 宽化类型处理和窄化类型处理
  3. L2b i2c i2s l2i

对象创建与访问指令

  1. 创建类实例的指令:new
  2. 创建数组的指令:newarray anewarray multianewarray
  3. 访问类字段:getfield putfield getstatic putstatic
  4. 把数组元素加载到操作数栈的指令:aload
  5. 将操作数栈的值存储到数组元素:astore
  6. 取数组长度的指令:arraylength
  7. 检查实例类型的指令:instanceof checkcast

操作数栈管理指令

  1. 操作数栈指令用于直接操作操作数栈
  2. 操作数栈的一个或两个元素出栈:pop pop2
  3. 复制栈顶一个或两个数值并将复制或双份复制值重新压入栈顶:dup dup2 dup_x1 dup_x2
  4. 将栈顶的两个数值替换:swap

控制转移指令

  1. 控制转移指令可以让java虚拟机有条件或无条件的从指定的位置指 令而不是控制转移指令的下一条指令继续执行程序。可以认为控制 转移指令就是在修改pc寄存器的值
  2. 条件分支:ifeq iflt ifle ifne ifgt ifnull ifcmple
  3. 复合条件分支:tableswitch lookupswitch
  4. 无条件分支:goto goto_w jsr jsr_w ret

方法调用指令

  1. Invokespecial
    只能调用三类方法:方法;private方法;super.method()。因为这三类方法的调用对象在编译时就可以确定。
  2. invokevirtual
    是一种动态分派的调用指令:也就是引用的类型并不能决定方法属于哪个类型。
  3. Invokinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口的对象,找出适合的方法进行调用
  4. Invokestatic指令用于调用类方法

方法返回指令

  1. 方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

  1. 在Java程序中显示抛出异常的操作(throw 语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java 虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。

栈+堆+方法区的交互关系


  HotSpot是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址。

Heap 堆

  一个JVM实例只存在一个堆内存,堆内存的大小是可 以调节的。类加载器读取了类文件后,需要把类、方法、 常变量放到堆内存中,保存所有引用类型的真实信息,以 方便执行器执行,堆内存分为三部分:

  • Young Generation Space 新生代
  • Tenure generation space 老年代
  • Permanent Space 永久代

方法区

  永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会 被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
  如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对 永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar 包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载, 最终导致Perm区被占满。

  • Jdk1.6及之前: 有永久代, 常量池1.6在方法区。
  • Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆。
  • Jdk1.8及之后: 无永久代,常量池1.8在元空间。
      方法区(Method Area),是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
      对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方 法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的 字符串常量池移走。
      常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、 方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方 法区的运行时常量池中存放。

类加载机制

  JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
  java的类加载是动态的!它并不会一次性把所有类加载到内存中再运行。而是保证程序运行的基础类全加载到jvm中。其它的类在需要时才加载。

加载过程

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。

加载
  1. 通过类的全限定名来获取定义此类的二进制字节流。
  2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构。
  3. 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
验证
  1. 文件格式验证:基于字节流验证。
  2. 元数据验证:基于方法区的存储结构验证。
  3. 字节码验证:基于方法区的存储结构验证。
  4. 符号引用验证:基于方法区的存储结构验证。
准备

为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如:

public static int value = 123;

此时在准备阶段过后的初始值为0而不是123;将value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法之中.特例:

public static final int value = 123;

此时value的值在准备阶段过后就是123。

解析

把类型中的符号引用转换为直接引用。

  • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

主要有以下四种:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
初始化

  初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
  java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):

  1. 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
  2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
  3. 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
  4. 虚拟机启动时,用户会先初始化要执行的主类(含有main)
  5. jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。

加载源

  1. 文件
  • class文件
  • jar
  1. 网络
  2. 二进制流
  3. 数据库
  4. 其它文件
  • jsp

垃圾回收算法

引用计数法

  在对象中添加一个引用计数器,当有地方引用这个对象的时候, 计数器+1,当失效的时候,计数器-1。因为可能存在对象相互引用,会造成对象泄露(内存泄漏),所以此方法已经废弃。

可达性分析

  判断对象在GCRoot对象中是否存在引用。
作为GCRoot的对象
• 虚拟机栈(局部变量表中的)
• 方法区的类属性所引用的对象
• 方法区的常量所引用的对象
• 本地方法栈所引用的对象

复制算法


  Minor GC会把Eden中的所有活的对象移动到 Survivor区,如果Survivor区中放不下,剩下的活的 对象被移动到Old区,收集后Eden为空。
  当对象在Eden(包括一个Survivor区域)出生后,再经 过一次Minor GC后,如果对象还存活,并且能够被另 外一块Survivor区域所容纳,则使用复制算法将这些 存活的对象复制到另外一块Survivor区中,然后清理 所使用的Eden以及Survivor区域,并且将这些对象的 年龄设置为1,以后对象在Survivor区,每熬过一次 Minor GC,对象年龄+1,当对象年龄到15岁,这些 对象就成为老年代。

标记清除算法

标记整理(标记压缩)

内存分配策略

• 优先分配Eden区
• 大对象直接分配到老年代
-XX:PretenureSizeThreshold
• 长期存活的对象分配老年代
-XX:MaxTenuringThreshold=15
• 空间分配担保
-XX:+HandlePromotionFailure
检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
• 动态对象年龄对象 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一 半,年龄大于或等于该年龄的对象就可以直接进入老年代。
-XX:TargetSurvivorRatio

逃逸分析和栈上分配

逃逸分析

public class Test {    
    private String name;    
    private Test test;    
    //逃逸    
    public Test getInstance(){        
        return test == null?new Test():test;   
    }    
    //逃逸    
    public void setTest(){        
        this.test = new Test();    
    }    
    //栈上分配    
    public String demo(){        
        Test t = new Test();        
    return t.name;   
    }
}

GC日志

[GC (Allocation Failure) [PSYoungGen: 883189K->104504K(1250816K)] 1004965K->232414K(1425920K), 0.1738086 secs] [Times: user=0.25 sys=0.05, real=0.17 secs] 
[GC (Allocation Failure) [PSYoungGen: 1250360K->121850K(1267712K)] 1378270K->266656K(1442816K), 0.1905867 secs] [Times: user=0.16 sys=0.03, real=0.19 secs] 
[Full GC (Ergonomics) [PSYoungGen: 121850K->0K(1267712K)] [ParOldGen: 144805K->168099K(274944K)] 266656K->168099K(1542656K), [Metaspace: 18935K->18935K(1067008K)], 0.2430193 secs] [Times: user=1.45 sys=0.02, real=0.24 secs] 

PSYoungGen:新生代垃圾收集器
ParOldGen:老年代垃圾收集器
Metaspace:元空间
883189K(新生代垃圾回收前大小)->104504K(新生代垃圾回收后大小)(1250816K)(新生代总大小)
1004965K(垃圾回收前堆大小)->232414K(垃圾回收后堆大小)(1425920K)(堆总大小)0.1738086 secs(回收时间)
[Times: user=0.25 sys=0.05, real=0.17 secs]用户耗时,系统耗时,实际耗时

栈上分配

  未逃逸的对象会直接分配到栈上,这些对象会随着方法的结束而释放空间。GC不会对栈上资源进行回收。所以栈上分配不会使用GC,从而提升jvm性能。

总结

  能在方法内创建对象,就不要再方法外创建对象。毕竟这是为了GC好,也是为了提高性能。

class文件

操作String和StringBuilder区别

反编译指令:javap -v Test.class

public class Test {   
    public static void main(String[] args)    
    {        
        f1();        
        f2();    
    }    
    public static void f1(){        
        String str = "";        
        for(int i=0;i<10;i++){            
            str = str + i;        
        }        
        System.out.println(str);    
    }    
    public static void f2(){        
        StringBuilder str = new StringBuilder();        
        for(int i=0;i<10;i++){           
            str.append(i);        
         }        
        System.out.println(str);    
    }
 }
  • f1()方法反编译:
  • f2()方法反编译:

      比较字节码问世间可以得出:String进行拼接,每次都会创建一个StringBuilder对象,然后堆StringBuilder对象进行操作。所以StringBuilder效率更高。
  • StringBuilder不是线程安全的,StringBuffer是线程安全的。

i++和++i性能比较

public class Test {
    public static void main(String[] args)
    {
        f1();
        f2();
    }
    public static void f1(){
        for(int i=0;i<10;i++){ }
    }
    public static void f2(){
        for(int i=0;i<10;++i){ }
    }
}



比较两个方法的字节码指令,可以发现两个方法的字节码指令完全。

i++和++i的不同

public class Test {
    public static void main(String[] args)
    {
        f1();
        f2();
    }
    public static void f1(){
		int a = 1;
		a=a++;
		System.out.println(a);
    }
    public static void f2(){
        int a = 1;
		a=++a;
		System.out.println(a);
    }
}

try-finally

public static String f1(){
    String str = "hello";
    try{
        return str;
    }finally{
        str = "word";
    }
}

代码会返回什么?
  会返回“hello”。

class文件结构

  Class 文件是一组以 8 位字节为基础单位的二进制流,各个数 据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加 任何分隔符,这使得整个Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前(Big-Endian)的方式分割成若干个8位字节进行存储。
Class 文件只有两种数据类型:无符号数和表。
链接: https://docs.oracle.com/javase/specs/jvms/se8/html

文件结构

1.魔数
2. Class文件版本
3.常量池
4.访问标志
5.类索引,父类索引,接口索引集合
6.字段表集合
7.方法表集合
8.属性表集合

文件格式

ClassFile {
u4 magic; // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE
u2 minor_version; // 分别为Class文件的副版本和主版本
u2 major_version;
u2 constant_pool_count; // 常量池计数
cp_info constant_pool[constant_pool_count-1]; //
u2 access_flags; // 类访问标识
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 实现的接口数
u2 interfaces[interfaces_count]; // 实现接口信息
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 包含的字段信息
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 包含的方法信息
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 各种属性
}

垃圾收集器

Serial(串行的)

  单线程的垃圾收集器。在Serial垃圾收集器开始工作的时候,所有的用户线程都会暂停。等到垃圾收集完成后,用户线程开始继续执行。

性能及适用场景

  效率非常高,因为是单线程模式,没有线程的上下文切换。适用与内存小的服务,客户端等,在此内存小的服务中,因为内存小,所以Serial执行时间很短,用户线程暂停时间也很短。

ParNew(新式的)

  是Serial的多线程版本。

Parallel Scavenge(并行模式)

  (重点)server端默认的垃圾收集器。

  • 复制算法
  • 多线程收集器
  • 达到可控制的吞吐量
  • 吞吐量=执行用户代码时间/(执行用户代码时间+垃圾回收使用的时间)
  • -XX:MaxGCPauseMillis 垃圾收集停顿时间
  • -XX:GCTimeRatio 吞吐量大小(0,100)

CMS(Concurrent Mark Sweep)

标记清除

工作过程

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

优点

  • 并发收集
  • 低停顿

缺点

  • 占用cpu资源
  • 无法处理浮动垃圾
  • 出现Concurrent Mode Failure
  • 空间碎片

G1(Garbage First)

标记压缩

  1. 历史
  • 2004年sun公司发表G1论文
  • JDK7开始使用G1
  1. 优势
  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿
  1. 步骤
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

总结

  1. G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大。
  2. G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
  3. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
  4. G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

G1内存模型


G1垃圾收集器将内存区域划分为多个大小相等的内存块(Region),虽然还保留了老年代和新生代的概念,但是老年代和新生代不再是物理隔离的了,它们都是一部分Region的组成,内存不再是连续的。

G1由一张表存储着Region的使用情况,Table分为多个card,每个crad都会记录一部分Region的使用情况。

G1的分代模型


G1分区模型也分为young区和old区。

G1的分区模型

收集集合(Cset)

  • 收集集合代表每次暂停时GC回收的一系列目标分区。

性能调优

工具及命令

常用jvm工具:
MAT(eclipse memory analyzer)、
Java VisualVM

  1. jps(java process status)
  • jps -l 查看进程主类。
  • jps -m 运行传入主类得参数。
  • jps -v 虚拟机参数
  1. jstat
    jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
  • 命令
    jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
  1. jinfo
    命令可以用来查看 Java 进程运行的 JVM 参数.
  2. jmap
    可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
  3. jhat
    使用jmap可以生成Java堆的Dump文件。生成dump文件之后就可以用jhat命令,将dump文件转成html的形式,然后通过http访问可以查看堆情况。
  4. jstack
    是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。
  5. jconsole
    从Java 5开始 引入了 JConsole。JConsole 是一个内置 Java 性能分析器,可以从命令行或在 GUI shell 中运行。您可以轻松地使用 JConsole(或者,它更高端的 “近亲” VisualVM )来监控 Java 应用程序性能和跟踪 Java 中的代码。