java虚拟机与程序的生命周期

在如下几种情况下,java虚拟机将结束生命周期:

  • 执行了System.exit()方法

  • 程序正常执行结束

  • 程序在执行过程中遇到了异常或错误而异常终止

  • 由于操作系统出现错误而导致java虚拟机进程结束

加载.class文件的方式:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将java原文件动态编译为.class文件

线程运行诊断

  1. Cpu 占用过多

    • 用top定位那个进程对cpu的占用过高。
    • ps H -eo pid,tid,%cpu | grep 进程id(用ps进一步定位是那个线程引起的cpu占用过高)
    • Jstack 进程id
      • 可以根据线程id找到问题的线程,进一步定位问题代码的源码行数。
  2. 堆内存

    • jps 查看当前系统中有哪些java进程
    • Jmap 查看堆内存占用情况 jmap -heap 进程id
    • Jconsole 图形界面
  3. StringTable特性

    • 常量池中的字符串仅是符号,第一次用到时才变成对象。
    • 利用串池的机制,来避免重复创建字符串对象。
    • 字符串拼接的原理是StringBuilder(1.8)
    • 字符串常量拼接的原理是编译器优化。
    • 可以使用intern方法,主动将串池中还没有的字符串放入串池
      • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池;两种情况都会把串池中的对象返回。
      • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池;两种情况都会把串池中的对象返回。
    • 性能调优
      • 调整-XX:StringTableSize=桶个数
      • 考虑将字符串对象是否入池

四种引用

  • 强引用

    只有所用GC Roots 对象都不强引用该对象,该对象才能被垃圾回收。

  • 软引用(SoftReference)

    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象

    • 可以配合引用队列释放软引用对象自身

      //-Xmx20m -XX:+PrintGCDetails -verbose:gc
      public static void soft(){
              List<SoftReference<byte[]>> list = new ArrayList<>();
              ReferenceQueue<byte[]> queue = new ReferenceQueue();
              for (int i = 0; i < 5; i++) {
                  //关联了引用队列,当软引用关联的byte[]被回收时,软引用自己会加入到queue中去
                  SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
                  System.out.println(ref.get());
                  list.add(ref);
                  System.out.println(list.size());
              }
              //从队列中获取无用的软引用对象并移除
              java.lang.ref.Reference<? extends byte[]> ref = queue.poll();
              while (ref != null){
                  list.remove(ref);
                  ref = queue.poll();
              }
              System.out.println("循环结束:"+list.size());
              System.out.println("============================");
              for (SoftReference<byte[]> softReference : list) {
                  System.out.println(softReference.get());
              }
          }
  • 弱引用(WeakReference)

    • 只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

    • 可以配合引用队列释放弱引用对象自身

      public static void weak(){
              List<WeakReference<byte[]>> list = new ArrayList<>();
              //ReferenceQueue<byte[]> queue = new ReferenceQueue();
              for (int i = 0; i < 6; i++) {
                  //关联了引用队列,当软引用关联的byte[]被回收时,软引用自己会加入到queue中去
                  WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
                  System.out.println(ref.get());
                  list.add(ref);
                  for (WeakReference<byte[]> weakReference : list) {
                      System.out.println(weakReference.get()+"  ");
                  }
                  System.out.println(list.size());
              }
              //从队列中获取无用的软引用对象并移除
      //        java.lang.ref.Reference<? extends byte[]> ref = queue.poll();
      //        while (ref != null){
      //            list.remove(ref);
      //            ref = queue.poll();
      //        }
              System.out.println("循环结束:"+list.size());
              System.out.println("============================");
          }
  • 虚引用(PhantomReference)

    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存。
  • 终结器引用(FinalReference)

    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用他的finalize方法,第二次GC时才能回收被引用对

垃圾回收

  1. 相关VM参数

    含义 参数
    堆初始大小 -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
  2. 特性

    • 大对象直接放到老年代。

    • 线程中的OOM不会引起java进程的结束。

      public class Gc {
          private static final int _512KB = 512 * 1024;
          private static final int _1M = 1 * 1024 * 1024;
          private static final int _6M = 6 * 1024 * 1024;
          private static final int _7M = 7 * 1024 * 1024;
          private static final int _8M = 8 * 1024 * 1024;
          public static void main(String[] args) throws InterruptedException {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      List<byte[]> list = new ArrayList<>();
                      list.add(new byte[_8M]);
                      list.add(new byte[_8M]);
                  }
              }).start();
              System.out.println("主线程睡一秒钟");
              Thread.sleep(1000l);
      
          }
      }
  3. 垃圾回收器

    • 串行

      • 单线程
      • 堆内存较小,适合个人电脑
      • -XX:+
    • 吞吐量优先

      • 多线程

      • 堆内存较大,多核CPU

      • 单位时间内让STW的时间最短

        -XX:+UseParallelGC(开启后会自动使用parallelOldGC)   -XX:+UseParallelOldGC
        -XX:ParallelGCThreads=n (设置垃圾回收线程数,默认和cpu核数相同)
        -XX:UseAdaptiveSizePolicy  自适应调整新生代大小  
        -XX:GCTimeRatio=ratio    1/1+ratio(默认为99,一般设置为19,表示100分钟内允许5分钟的时间可以用来进行垃圾回收) 为垃圾回收时间所占的比例
        -XX:MaxGCPauseMillis=ms  每次垃圾回收使用的最大时间
    • 响应时间优先

      • 多线程

      • 堆内存较大,多核CPU

      • 尽可能让STW的单次时间最短

        -XX:+UseConcMarkSweepGC(老年代) --> 如果失败,会退化成SerialOld垃圾回收器  
              -XX:UseParNewGC (新生代)
              -XX:ParallelGCThreads=n (设置垃圾回收线程数,默认和cpu核数相同)
              -XX:ConcGCThreads=n/4
              -XX:CMSInitiatingOccupancyFraction=percent 执行垃圾回收的内存占比
              -XX:+CMSScavengeBeforeRemark
    • G1(Garbage First 取代cms回收器)

      • 2017年JDK9 默认的垃圾回收器

      • 适用场景

        • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
        • 超大堆内存,会将堆划分为多个大小相等的Region
        • 整体上是标记+整理算法,两个区域之间是复制算法
      • 配置

        -XX:+UseG1GC
        -XX:G1HeapRegionSize=size
        -XX:MaxGCPauseMillis=time

垃圾回收调优

新生代调优

  1. 新生代特点
    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价为0
    • 大部分对象用过即死
    • Minor GC 的时间远远低于Full GC
  2. 调优
    • 新生代能容纳所有[并发量*(请求-响应)]的数据

class文件结构

参考文档

ClassFile {
    u4             magic;
    u2             minor_version;
    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];
}

执行流程

package test;
public class HelloWorld {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

javap 查看字节码

Classfile /Users/haominglfs/IdeaProjects/leetcode/out/production/leetcode/test/HelloWorld.class
  Last modified 2020-1-10; size 602 bytes
  MD5 checksum 470a732b94645485adf09a48f8e9bdd2
  Compiled from "HelloWorld.java"
public class test.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // test/HelloWorld
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Ltest/HelloWorld;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               HelloWorld.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               test/HelloWorld
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public test.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 6
        line 7: 10
        line 8: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "HelloWorld.java"

图解分析

  1. 加载常量池到运行时常量池,方法字节码加载到方法区

  2. bipush 10

    • 将一个byte压入操作数栈(其长度会补齐4个字节)

    • sipush将一个short压入操作数栈

    • ldc 将一个int压入操作数栈

    • Ldc2_w 将一个long压入操作数栈(分两次压入,因为long是两个字节)

    • 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入常量池

  3. Istore_1 将操作数栈顶数据弹出,存入局部变量表的slot1

  4. ldc #3 从常量池加载#3数据到操作数栈

  5. istore_2

  6. load_1

  7. Iload_2

    ![image-20200110183557906](/Users/haominglfs/Library/Application Support/typora-user-images/image-20200110183557906.png)

  8. iadd

  9. Istore_3

  10. getstatic #4

    将堆中的System.out对象的引用压入操作数栈

  11. iload_3

  12. Invokevirtual #5

    • 找到常量池第五项

    • 定位到方法区java/io/PrintStream.println:(I)V方法

    • 生成新的栈帧(生成locals、stack等)

    • 传递参数,执行新栈帧中的字节码

    • 执行完毕,弹出栈帧

    • 清除main操作数栈内容

      ![image-20200113142925906](/Users/haominglfs/Library/Application Support/typora-user-images/image-20200113142925906.png)

  13. return

    • 完成main方法调用,弹出main栈帧
    • 程序结束

构造方法

  1. ()V

    static int i = 10;
    static {
      i = 20;
    }
    static {
      i = 30;
    }

    编译器会按照从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法()V

    0: bipush        10
    2: putstatic     #2                  // Field i:I
    5: bipush        20
    7: putstatic     #2                  // Field i:I
    10: bipush        30
    12: putstatic     #2                  // Field i:I
    15: return

    ()V方法会在类加载的初始化阶段被调用

  2. ()V

    编译器会按照从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

  3. 方法调用

    public class Demo {
        public Demo(){}
        private void test1(){}
        private final void test2(){}
        public void test3(){}
        public static void test4(){}
    
        public static void main(String[] args) {
            Demo d = new Demo();
            d.test1();
            d.test2();
            d.test3();
            d.test4();
            Demo.test4();
        }
    }
    0: new           #2                  // class test/Demo  堆空间分配内存,引用放入操作数栈
    3: dup															 //复制栈顶引用
    4: invokespecial #3                  // Method "<init>":()V  调用构造方法
    7: astore_1
    8: aload_1
    9: invokespecial #4                  // Method test1:()V
    12: aload_1
    13: invokespecial #5                  // Method test2:()V
    16: aload_1
    17: invokevirtual #6                  // Method test3:()V
    20: aload_1
    21: pop																//入栈又出栈,通过对象调用静态方法会产生两次不必要的指令
    22: invokestatic  #7                  // Method test4:()V
    25: invokestatic  #7                  // Method test4:()V
    28: return

    构造方法、私有方法、final方法使用invokespecial;普通public方法使用invokevirtual(动态绑定);静态方法使用invokestatic

  4. 多态

    当执行invokevirtual指令时

    1. 先通过栈帧中的对象引用找到对象
    2. 分析对象头,找到对象的实际Class
    3. class结构中有vtable,他在类加载的链接阶段就已经根据方法的重写规则生成好了
    4. 查表得到方法的具体地址
    5. 执行方法的字节码
  5. 异常

    public class Demo {
        public static void main(String[] args) {
            int i = 0;
            try {
                i = 10;
            }catch (Exception e){
                i = 20;
            }
        }
    }
    0: iconst_0
    1: istore_1
    2: bipush        10
    4: istore_1
    5: goto          12
    8: astore_2
    9: bipush        20
    11: istore_1
    12: return
     Exception table:
    from    to  target type
     	2     5     8   Class java/lang/Exception
     LocalVariableTable:
    Start  Length  Slot  Name   Signature
     	9       3     2     e   Ljava/lang/Exception;
    	0      13     0  args   [Ljava/lang/String;
       2      11     1     i   I

    可以看出多出来一个Exception table的结构,[from,to)是前闭后开的监测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号。

    8行的字节码指令astore_2是将异常对象引用存入局部变量表的slot 2 位置。

类加载

加载步骤

  1. 加载

  2. 链接

    • 验证

    • 准备(为static变量分配空间,设置默认值)

      • static 变量在JDK7之前存储在instanceKlass末尾,从JDK7开始,存储于java_mirror的末尾(堆中)
      • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
      • 如果变量是final的基本类型或字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
      • 如果变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
    • 解析

      符号引用解析为直接引用

    • 初始化

      • 初始化即调用()V,虚拟机会保证这个类的[构造方法]的线程安全

      • 初始化时机

        1. 主动使用(六种):
          • 创建类的实例
          • 访问某个类或接口的静态变量,或者对该静态变量赋值
          • 调用类的静态方法
          • 反射(如 Class.forName(“info.haominglfs.test”))
          • 初始化一个类的子类
          • java虚拟机启动时被表明为启动类的类(含有main方法)
        2. 被动使用,除了以上六种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
          • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
          • 类对象.class不会触发初始化
          • 创建该类的数组不会触发初始化
          • 类加载器的loadClass方法
          • Class.forName的参数2为false时
        3. 所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
      • 单例例子

        class Singleton {  //懒惰式单例模式
            private Singleton() {  
            }  
          	//1.首次使用时才会触发初始化
            //2.静态类可以访问外部类的构造方法
            private static class HolderClass {  
                    private final static Singleton instance = new Singleton();  
            }  
          	//由类加载器来保证线程安全
            public static Singleton getInstance() {  
                return HolderClass.instance;  
            }
        }

类加载器

  1. 以jdk8为例:

    名称 加载哪的类 说明
    Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
    Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
    Application ClassLoader classpath 上级为Extension
    自定义类加载器 自定义 上级为Application
  2. 双亲委派

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
      synchronized (getClassLoadingLock(name)) {
        // 1.检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
          long t0 = System.nanoTime();
          try {
            if (parent != null) {
              //2.有上级的话,委派上级loadClass
              c = parent.loadClass(name, false);
            } else {
              //3.如果没有上级(ExtClassLoader),则委派BootstrapClassLoader
              c = findBootstrapClassOrNull(name);
            }
          } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
          }
    			
          if (c == null) {
            //4.每一层找不到,调用findClass方法(每个类加载器自己扩展)来加载
            long t1 = System.nanoTime();
            c = findClass(name);
    
            //5.记录耗时
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
          }
        }
        if (resolve) {
          resolveClass(c);
        }
        return c;
      }
    }
  3. 线程上下文类加载器

    是当前线程使用的类加载器

    public class DriverManager {
    
        // 注册驱动的集合
        private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
        /* Prevent the DriverManager class from being instantiated. */
        private DriverManager(){}
    
        /**
         * Load the initial JDBC drivers by checking the System property
         * jdbc.properties and then use the {@code ServiceLoader} mechanism
         */
        //初始化驱动
        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
    }
    
    private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
    				//1.使用serviceLoader机制加载驱动,即SPI
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    try{
                        while(driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch(Throwable t) {
                    // Do nothing
                    }
                    return null;
                }
            });
    
            println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
            if (drivers == null || drivers.equals("")) {
                return;
            }
      			//2.使用jdbc.drivers定义的驱动名加载驱动
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    //这里的ClassLoader.getSystemClassLoader()就是应用程序类加载器
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
    
    	public static <S> ServiceLoader<S> load(Class<S> service) {
        		//线程上下文类加载器
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }

  4. 自定义类加载器

    • 什么时候需要自定义类加载器

      1. 想加载非classpath随意路径中的类文件
      2. 都是通过接口来使用实现,希望解耦时,常用在框架设计中
      3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
    • 步骤

      1. 继承ClassLoader父类
      2. 要遵从双亲委派模型,重写findClass方法(不是重写loadClass方法,否则不会走双亲委派机制)
      3. 读取类文件的字节码
      4. 调用父类的defineClass方法来加载类
      5. 使用者调用该类加载器的loadClass方法
    • 例子

      public class MyClassLoader extends ClassLoader {
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              String path = "/myclasspath/"+name+".class";
              try {
                  ByteOutputStream os = new ByteOutputStream();
                  Files.copy(Paths.get(path),os);
                  byte[] bytes = os.toByteArray();
                  return defineClass(name, bytes, 0, bytes.length);
              } catch (IOException e) {
                  e.printStackTrace();
                  throw  new ClassNotFoundException("类文件未找到");
              }
          }
      }