跳转至

jvm学习笔记

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

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

  • 执行了System.exit()方法

  • 程序正常执行结束

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

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

加载.class文件的方式:

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

线程运行诊断

  1. Cpu 占用过多
  2. 用top定位那个进程对cpu的占用过高。
  3. ps H -eo pid,tid,%cpu | grep 进程id(用ps进一步定位是那个线程引起的cpu占用过高)
  4. Jstack 进程id

    • 可以根据线程id找到问题的线程,进一步定位问题代码的源码行数。
  5. 堆内存

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

  9. StringTable特性

  10. 常量池中的字符串仅是符号,第一次用到时才变成对象。

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

四种引用

  • 强引用

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

  • 软引用(SoftReference)

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

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

    java //-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)

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

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

    java 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进程的结束。

      ```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 list = new ArrayList<>(); list.add(new byte[_8M]); list.add(new byte[_8M]); } }).start(); System.out.println("主线程睡一秒钟"); Thread.sleep(1000l);

      }
      

      } ```

  3. 垃圾回收器

    • 串行

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

      • 多线程

      • 堆内存较大,多核CPU

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

      shell -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的单次时间最短

      shell -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
      • 整体上是标记+整理算法,两个区域之间是复制算法

      • 配置

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

垃圾回收调优

#### 新生代调优

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

class文件结构

参考文档

shell 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]; }

## 执行流程

java 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 查看字节码

```java 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."":()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 #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 // "":()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."":()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

  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

  13. return

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

构造方法

  1. ()V

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

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

java 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方法会在类加载的初始化阶段被调用

  1. ()V

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

  1. 方法调用

```java 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();
   }

} ```

java 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

  1. 多态

当执行invokevirtual指令时

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

  6. 异常

java public class Demo { public static void main(String[] args) { int i = 0; try { i = 10; }catch (Exception e){ i = 20; } } }

java 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. 加载

  1. 链接

  2. 验证

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

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

    符号引用解析为直接引用

  5. 初始化

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

    • 初始化时机

    • 主动使用(六种):

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

```java 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;
 }

} ```

  1. 线程上下文类加载器

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

```java 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() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } //1.使用serviceLoader机制加载驱动,即SPI AccessController.doPrivileged(new PrivilegedAction() { 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);
   }

```

  1. 自定义类加载器

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

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

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

    java 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("类文件未找到"); } } }