JAVA基础知识复习-JVM篇

1,013 阅读9分钟

简介

JVM(Java Virtual Machine)是运行Java字节码的虚拟机,由一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器构成。

过程

外部编译器将.java(源文件)编译成.class(字节码文件),类加载子系统加载.class文件,数据存入运行时数据区,即时编译器(实现跨平台)将.class文件编程成机器码,机器码通过本地接口库调用操作系统本地方法库执行程序

内存模型

类比物理机,在JVM中,线程引擎对标CPU,工作内存对标高速缓存,主存对主存。当多个处理器同时对Java主存进行操作的时候就会发生不一致现象。单应用下,这种不一致可以由Java并并发包提供的方法解决。例如AtomicLong,底层是通过CAS(compareAndSwap)来实现线程的同步,是在一个死循环内不断的尝试修改目标的值,直到修改成功。分布式下则可以通过redis等组件来完成。

内存结构

线程共享区与JVM共存亡,线程私有区域与线程共存亡,堆外内存可以避免Java堆和Native堆之间来回复制的性能开销,在并发编程中被广泛应用。

组件概要

  • 程序计数器:线程私有,记录实时字节码指令地址,执行Native则为空,唯一没有Out Of Memory的区域
  • 栈:线程私有,每个⽅法在执⾏时都会床创建⼀个栈帧(Stack Frame)⽤于存储局部变量表 、 操作数栈 、 动态链接 、 ⽅法出⼝等信息。每⼀个⽅法从调⽤直⾄执⾏结束,就对应着⼀个栈帧从虚拟机栈中⼊栈到出栈的过程
  • 本地方法区:线程私有,类似栈,区别是执行Native方法
  • 堆:线程共享,存放创建的对象和产生的数据
  • 方法区:线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK1.8之后成为元数据区,原方法区的常量池和静态变量存在堆中,元数据区则在堆外内存!!!!!

常量池

#string pool中存的是引⽤值⽽不是具体的实例对象,具体的实例对象是在堆中开辟的⼀块空间存放的
#在堆中会有⼀个”abc”实例,全局StringTable中存放着”abc”的⼀个引⽤值
String str1 = "abc";
String str2 = "abc";
#如果常量池没有“abc”则⽣成两个实例,⼀个是”abc”的实例对象,并且StringTable中存储⼀个”abc”的引⽤值,还有⼀个是new出来的⼀个”abc”的实例对象
String str3 = new String("abc");
String str4 = str2.intern();
System.out.println(str1==str2);//true
System.out.println(str1==str3);//false
System.out.println(str1==str4);//true

垃圾回收

对象已死判定⽅法

  • 引用计数法:对象添加一个引用,引用计数+1,反之-1,为0则认为可以回收。存在循环引用问题
  • 可达性分析:以一系列GC Roots的点作为起点向下搜索,当一个对象到任何一个GC Roots都没有引用链则认为可以回收(两次标记)。

垃圾回收算法

  • 复制算法:分为两个区域,只用一个区域,每次将存活的放入另一个区域。内存浪费问题
  • 标记清除算法:效率低,内存碎片问题
  • 标记整理算法:标记清除的基础上,将对象放在内存的一端
  • 分代收集算法
    • 新生代:复制算法/...
    • 老年代: 标记清除/标记整理/...

进入老年代的途径

  • 复制算法S区放不下
  • 对象过大,XX:PretenureSizeThreshold设置,一般为2KB~128KB
  • 存活时间过久,XX:MaxTenuringThreshold设置,默认15(15次GC仍然存活)

垃圾收集器

  • 新生代
    • Serial:单线程,复制算法,Client默认
    • ParNew:Serial多线程版(默认CPU等值线程数),Server默认
    • Parallel Scavenge:多线程,复制算法,吞吐量(GC时间/总时间)
  • 老年代
    • CMS:标记清除,初始标记和重新标记需要STW,其他过程不需要,为了实现最短垃圾回收停顿时间
    • Serial Old:单线程,标记整理,Client默认
    • Parallel Old:多线程,标记整理
    • G1:标记整理,内存分区,通过优先级列表回收垃圾最多的区域,保证吞吐量的前提下实现最短垃圾回收停顿时间。

引用类型

  • 强引⽤
    • 类似于 Object obj = new Object(); 创建的,只要强引⽤在就不回收
  • 软引⽤
    • SoftReference 类实现软引⽤。在系统要发⽣内存溢出异常之前,将会把这些对象列进回收范围之中进⾏⼆次回收
  • 弱引⽤
    • WeakReference 类实现弱引⽤。对象只能⽣存到下⼀次垃圾收集之前。在垃圾收集器⼯作时,⽆论内存是否⾜够都会回收掉只被弱引⽤关联的对象
  • 虚引⽤
    • PhantomReference 类实现虚引⽤。⽆法通过虚引⽤获取⼀个对象的实例,为⼀个对象设置虚引⽤关联的唯⼀⽬的就是能在这个对象被收集器回收时收到⼀个系统通知

网络编程模型

阻塞I/O模型

用户发出I/O请求->等待内存数据就绪(阻塞)->I/O->返回I/O结果

非阻塞I/O模型(基于轮询)

用户发出I/O请求->轮询(一个selector对应一个socket)判断内存数据就绪(非阻塞)->I/O->返回I/O结果

多路复用I/O模型(基于轮询)

用户发出I/O请求->轮询(一个selector对应多个socket)判断内存数据就绪(非阻塞)->I/O->返回I/O结果,在连接数量众多且消息体不大的情况下有很大的优势

信号驱动I/O模型(基于通知)

用户发出I/O请求->注册信号函数->内存数据就绪后通知用户线程->I/O->返回I/O结果

异步I/O模型(基于通知)

用户发出I/O请求->注册信号函数->内存数据就绪后且I/O完成,通知用户线程->返回I/O结果

Java I/O

  • File/OutputStream/InputStream/Writer/Reader
  • Serializable

Java NIO

  • Channel:类似流,区别是Channel是双向的
  • Buffer:Channel在文件、网络上对数据的读取/写入都需要经过Buffer
  • Selector:用于检测多个注册的Channel上是否有I/O发生,有则进行I/O.一个Selector可对应多个Socket.
// todo 代码演示NIO

类加载机制

类加载阶段

静态变量

# 准备阶段值0,初始化阶段赋值1
public static int i =1;
# 准备阶段赋值1
public static final int i =1;

双亲委派机制

双亲委派机制保证了类的唯一性和安全性

监控

命令

  • -XX:+PrintFlagsFinal:显示JVM参数
java -XX:+PrintFlagsFinal -version > flags.txt

我们可以通过搜索上文MaxTenuringThreshold,默认值是15即经过15次GC仍然存活则进入老年代

  • jps:查看Java进程

  • jinfo:查看参数值

  • jstat:查看JVM统计信息(类装载,垃圾收集,JIT编译)【GC】

    • jstat -class(类装载)
    • jstat -gc(垃圾收集)
      • S0C S1C S0U S1U (S0,S1的总量和使用量)
      • EC EU (Eden的总量和使用量)
      • OC OU (Old的总量和使用量)
      • MC MU (Metaspace的总量和使用量)
      • CCSC CCSU (压缩类空间的总量和使用量)
      • YGC YGCT (YoungGC的次数和时间)
      • FGC FGCT (FullGC的次数和时间)
      • GCT (GC总时间)
  • jmap:生成堆dump文件以供分析【堆】

  • jstack【线程】

    • 死锁代码
        ...
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    try {
                      log.info("开始执行线程1");
                      int r = 0;
                      synchronized (r1) {
                        r = r1;
                        Thread.sleep(5000);
                        synchronized (r2) {
                          r = r2;
                        }
                      }
                      log.info("线程1结束");
                    } catch (Exception e) {
                      e.printStackTrace();
                    }
                  }
                })
            .start();
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    try {
                      log.info("开始执行线程2");
                      int r = 0;
                      synchronized (r2) {
                        r = r2;
                        synchronized (r1) {
                          r = r1;
                        }
                        log.info("线程2结束");
                      }
                    } catch (Exception e) {
                      e.printStackTrace();
                    }
                  }
                })
            .start();
        ...
    

    • 状态
      • NEW:The thread has not yet started
      • RUNNABLE:The thread is executing in the JVM
      • BLOCKED:The thread is blocked waiting for a monitor lock
      • WAITING:The thread is waiting indefinitely for another thread to perform a particular action
      • TIMED_WAITING:The thread is waiting for another thread to perform an action for up to a specified waiting time
      • TERMINATED:The thread has exited

springboot actuator

springboot admin

jconsole

jvisualvm

GCEasy

下文模拟内存泄漏

FastThread

上文死锁例子分析

HeapHero

下文模拟内存泄漏

JProfiler

MAT

内存溢出和内存泄漏

内存溢出

堆内存不够用了,发生OOM

  • 原因
    • JVM内存过小
    • 程序不严密,产生了过多的垃圾
      • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
      • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
      • 代码中存在死循环或循环产生过多重复的对象实体
      • 启动参数内存值设定的过小
  • 解决
    • 增加JVM的内存大小
    • 优化程序
  • 报错
    • tomcat:java.lang.OutOfMemoryError: PermGen space
    • tomcat:java.lang.OutOfMemoryError: Java heap space
  • JVM内存参数
    • -Xms设置堆的最小值
    • -Xmx设置堆的最大值
    • -Xmn:设置新生代大小
    • -XX:NewRatio:设置新生代和老年代的比值。3,表示年轻代与老年代比值为1:3
    • -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5
    • -XX:MaxTenuringThreshold:设置转入老年代的存活次数。0(默认15),则直接跳过新生代进入老年代
    • -XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
    • -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)
# 举例
整个堆大小=年轻代大小+年老代大小+持久代大小
持久代大小=64m
固定新生代内存也就固定了老年代,官方推荐新生代占3/8
堆最大值/最小值保持一致,避免每次垃圾回收完成后JVM重新分配内存
java -Xmx8g -Xms8g -Xmn3g

内存泄漏

理应被回收的对象而没有被回收,如用完不释放,发生OOM

模拟事故

首先分析多次full gc后老年代内存没有减少,进一步分析堆日志,显然map发现内存泄漏,无法被回收

...
  static Map<String, Student> map = new HashMap<>();
  // 先加100000,此处假定理应释放
  @Bean
  public void put() throws Exception {
    for (int i = 0; i < 100000; i++) {
      map.put(String.valueOf(i), Student.builder().name("模拟内存泄露").age(1).build());
    }
  }
  // 慢慢加,System.gc()触发gc
  @Bean
  public void test()throws Exception {
    for (int k=0;k<10;k++){
      Thread.sleep(2000);
      for (int i = 0; i < 10000; i++) {
        map.put(String.valueOf(i), Student.builder().name("模拟内存泄露").age(1).build());
      }
      log.info("map个数:{}",map.size());
      System.gc();
    }
  }
...