初识JVM

一、JAVA程序的跨平台特性

  1. 在Java虚拟机中执行的指令, 称之为Java字节码指令

  2. 下面显示了同一个Java程序,被编译为一组Java字节码的集合之后,可以通过Java虚拟机运行于不同的操作系统上, 它以Java虚拟机为中介, 实现了跨平台的特性
    在这里插入图片描述

二、JVM的基本结构

JVM基本结构

1.类加载⼦系统

负责从⽂件系统或者⽹络中加载Class信息

2.方法区

  • 加载的Class信息并存放到方法区内存空间

  • 运⾏时常量池(字符串字⾯量和数字常量)也会放到其中

  • 公有的

3.Java堆

  • 虚拟机启动时建立

  • 基本上所有的对象实例都放在Java堆中

  • 公有的

4.Java栈、虚拟机栈

  • 与方法调用息息相关

  • Java栈——栈帧

    • 方法入参

    • 局部变量

    • 中间计算的临时数据

    • 常量池指针

    • 正常方法返回处理

    • 异常的状态处理

  • 私有的——方法和线程息息相关

5.本地方法栈

  • 与Java栈类似且私有

  • native method都放在本地方法栈中

6.计数器

  • 普通方法

    指向正在被执行的字节码的位置

  • native方法

    undefined

7.直接内存

  • 与NIO是息息相关的

  • 是堆外内存,直接向系统申请内存空间

  • 与Java堆相比

    • 优点

      读写访问快

    • 缺点

      内存空间申请上速度慢

  • 应⽤场景

    内存空间申请次数少,并且访问较为频繁的情况

三、JVM类加载流程

类加载流程

1.主动加载的方式

  • new对象

  • 利用反射、clone

  • 初始化子类时, 父类会被初始化

  • 调用类的静态方法

2.类加载内容

  • 加载Classloader

    读取文件

    • 通过类的全路径名, 获取到类的二进制数据流

    • 获得类信息后进行解析, 转化为方法区内部的数据结构

    • 创建java.lang.Class的实例

  • 验证

    判断字节码是否合法、规范

    查看指令: javap -verbose Xxx.class

  • 准备

    分配相应的内存空间

  • 解析

    将符号引用转化为直接引用

  • 初始化

    类已经被加载到系统中了, 开始执行字节码

3.符号引用和直接引用

  • 符号引用

    以一组符号来描述应用的目标直接的关系

    D:\...\target\classes>javap -verbose Student.class
    Classfile /D:/.../target/classes/Student.class
      Last modified 2021-11-26; size 1039 bytes
      MD5 checksum abe7a514b71443d5b5136583dbc4dead
      Compiled from "Student.java"
    public class day1.Student
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #12.#35        // java/lang/Object."<init>":()V
       #2 = Fieldref           #11.#36        // day1/Student.name:Ljava/lang/String;
       #3 = Fieldref           #11.#37        // day1/Student.age:I
       #4 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
       #5 = Class              #40            // java/lang/StringBuilder
       #6 = Methodref          #5.#35         // java/lang/StringBuilder."<init>":()V
       #7 = String             #41            // name=
       #8 = Methodref          #5.#42         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #9 = Methodref          #5.#43         // java/lang/StringBuilder.toString:()Ljava/lang/String;
      #10 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
      	.
      	.
      	.
    
  • 直接引用

    通过对Class文件加载到内存后, 对符号引用的转换, 最终得到直接引用(即地址)

  • 为什么有符号引⽤

    • java类被编译成Class⽂件后, 并不知道具体的引⽤地址, 所以就以符号引⽤作为代替

    • 解析阶段, 才把符号引⽤转换为了真正的地址, 即直接引⽤

四、JMM内存模型

1.程序计数器

  • 是当前线程所执行的字节码的行号指示器, 指向虚拟机字节码指令的位置

  • 被分配了一块较小的内存空间

  • 针对于方法

    • 非Native方法

      是当前线程执行的字节码的行号指示器

    • Native方法

      是undefined

  • 每个线程都有自己独立的程序计数器, 所以该内存是线程私有的

  • 该区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域

2.虚拟机栈&本地方法栈

  • 虚拟机栈
    虚拟机栈内部结构及其流程

    • 为执行Java方法服务的, 是描述方法执行的内存模型

    • 线程私有的

    • 每次函数调用的数据都是通过栈传递的

    • 栈中保存的主要内容是栈帧, 数据结构是先进后出

      • 入栈 —— 方法调用

      • 出栈 —— 结果返回

      • 栈顶 —— 正在执行的函数

    • 每个方法在执行的同时都会创建一个栈帧用于存储局部变量表操作数栈帧数据区, 动态链接等信息

  • 本地方法栈

    • 为native方法服务的

2.1.栈内存测试

  • 无参

    public class StackOverflowTest1 {
    
        private static int count=0;
    
        public static void main(String[] args) {
           try {
                while (true) {
                    count();
                }
           } catch (Throwable e) {
                System.out.println("count = " + count);
                throw e;
            }
        }
    
        private static void count() {
            count++;
            count();
        }
    
    }
    
    • 设置最大栈内存为-Xss160K

      count = 1900
      Exception in thread "main" java.lang.StackOverflowError
      	at stack.StackOverflowTest.count(StackOverflowTest.java:30)
      	at stack.StackOverflowTest.count(StackOverflowTest.java:31)
      
    • 设置最大栈内存为-Xss256K

      count = 2737
      Exception in thread "main" java.lang.StackOverflowError
      	at stack.StackOverflowTest.count(StackOverflowTest.java:30)
      	at stack.StackOverflowTest.count(StackOverflowTest.java:31)
      
  • 有参

    public class StackOverflowTest2 {
    
        private static int count = 0;
    
        public static void main(String[] args) {
            try {
                while (true) {
                    count(1L, 2L, 3L, 4L, 5L);
                }
            } catch (Throwable e) {
                System.out.println("count = " + count);
                throw e;
            }
        }
    
        private static void count(long arg1, long arg2, long arg3, long arg4, long arg5) {
            long num1 = 1;
            long num2 = 2;
            long num3 = 3;
            long num4 = 4;
            long num5 = 5;
            long num6 = 6;
            long num7 = 7;
            long num8 = 8;
            count++;
            count(arg1, arg2, arg3, arg4, arg5);
        }
    
    }
    
    • 设置最大栈内存为-Xss160K

      count = 532
      Exception in thread "main" java.lang.StackOverflowError
      	at stack.StackOverflowTest2.count(StackOverflowTest2.java:27)
      	at stack.StackOverflowTest2.count(StackOverflowTest2.java:36)
      
    • 设置最大栈内存为-Xss256K

      count = 755
      Exception in thread "main" java.lang.StackOverflowError
      	at stack.StackOverflowTest2.count(StackOverflowTest2.java:27)
      	at stack.StackOverflowTest2.count(StackOverflowTest2.java:36)
      

3.Java堆

  • 运行数据时, 几乎所有的对象实例都在Java堆中

  • 完全自动化管理, 通过垃圾回收机制, 垃圾对象会被自动清理, 不需要显示地释放

  • 堆是垃圾收集器进行GC的最重要的内存区域

  • Java堆划分

    • 新生代((Eden区, S0区, S1区)(默认8:1:1))(1/3)

    • 老年代(2/3)

  • 分配机制

    • 绝大数情况下, 对象首先被分配在Eden区

    • 在一次新生代GC后, 如果对象还存活则会进入S0区或S1区

    • 之后, 每进行一次新生代回收, 对象如果存活, 年龄就会加一

    • 当对象年龄达到一定条件后, 就会被认为是老年代对象, 从而进入老年代

4.方法区

  • 逻辑定义, 是JVM的规范, 所有虚拟机必须遵守的

  • 是JVM所有线程共享的、用于存储类信息、常量等

  • 方法区大小决定着系统可以保存多少个类

  • JDK8之前——永久代

    • 内存的永久保存区域, 主要存放Class和Meta(元数据)的信息, Class在被加载的时候被放入永久区域, 它和存放实例的区域不同, GC不会在主程序运行期对永久区域进行清理, 所以这也导致了永久代的区域会随着加载的Class的增多而胀满, 最终抛出OMM异常

      如果场景使用了动态代理等, 会产生大量的类时, 设置合适的永久代大小有利于系统的稳定

    • 设置初始永久代大小 —— -XX:permSize

      -XX:permSize=5m

    • 设置最大永久代大小 —— -XX:MaxPermSize(默认64MB)

      -XX:MaxPermSize=5m

  • JDK8之后——元空间

    • 永久代已经被移除, 被"元数据区"(元空间)所取代

    • 元空间的本质和永久代类似, 元空间与永久代之间的最大的区别在于: 元空间并不在虚拟机中, 而是使用的堆外直接内存

    • 与永久代不同, 如果不指定大小, 默认情况下, 虚拟机会耗尽所有的可用系统内存

    • 设置元空间初始大小 —— -XX:MetaspaceSize

      -XX:MetaspaceSize=40m

    • 设置最大元空间大小 —— -XX:MaxMetaspaceSize

      -XX:MaxMetaspaceSize=40m

5.槽位复用

当前方法内局部变量跳出作用域后, 后续变量将不会创建新的槽位, 而是复用先前的槽位

例如:

slotvalue状态
1a跳出自身作用域(失效)
1b复用

6.对象分配

  • 栈上分配

    JVM提供的一种优化技术——对线程私有对象, 可以尝试打散分配到栈上, 而不是分配到堆上

    • 优点

      分配到栈上, 调用结束后会自行销毁, 而不用GC去回收

    • 缺点

      栈空间相对较小, 不适合大对象进行栈上分配

    • 开启条件(缺一不可)

      -XX:+PrintFlagsFinal, 可查看所有系统参数

      • 逃逸分析(默认开启)
        逃逸分析默认开启

        逃逸: 变量脱离了当前函数体, 即在函数体外部也有引用

      • 标量替换(默认开启)
        标量替换

        • 标量

          不可被进⼀步分解的量, 例如int, long, byte…

        • 聚合量

          可被进⼀步分解的量, 例如创建的对象…

        把聚合量分解为标量, 对应的成员变量存储在栈帧或寄存器上

      • server模式(默认模式)
        java默认开启模式

  • TLAB——(-XX:±UseTLAB)(默认开启)

  • 堆上分配

五、JVM垃圾回收

1.什么是垃圾回收

垃圾回收动作, 通常称之为GC(Garbage Collection)

  • 垃圾

    特指存在于内存中的、不会再被使用的对象

  • 回收

    清除内存中的"垃圾"对象

2.可触及性

在GC时, 根据可触及性来确定对象是否可被回收的
即: 从根节点开始是否可以访问到某个对象, 也说明这个对象是否被使用

  • 状态

    • 可触及

      从根节点开始,可以到达某个对象

    • 可复活

      对象引用被释放,但是可能在finalize()函数中被初始化复活

    • 不可触及

      由于finalize()只会执行一次,所以,错过这一次复活机会的对象,则为不可触及状态

  • 状态测试

    public class GcAccessibilityTest {
    
        private static GcAccessibilityTest gcAccessibilityTest;
    
        public static void main(String[] args) {
            // 创建实例
            // 可触及
            gcAccessibilityTest = new GcAccessibilityTest();
    
            for (int i = 0; i < 2; i++) {
                System.out.println("--------------------num = " + i + "--------------------");
                // 第一次循环: 可复活
            	// 第二次循环: 不可触及
                // 将gcAccessibilityTest对象设置为垃圾对象
                gcAccessibilityTest = null;
                // 通知JVM可以执行GC了
                System.gc();
                try {
                	// 睡眠100ms
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (gcAccessibilityTest == null) {
                    System.out.println("gcAccessibilityTest is null");
                } else {
                    System.out.println("gcAccessibilityTest is not null");
                }
            }
        }
    
        /**
         * finalize只会被调用一次,给对象唯一一次重生的机会
         * 关闭资源还是得使用try-catch-finally
         */
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize is called!");
            super.finalize();
            // 使对象复生, 添加引用
            gcAccessibilityTest = this;
        }
    }
    
    // 结果
    --------------------num = 0--------------------
    finalize is called!
    gcAccessibilityTest is not null
    --------------------num = 1--------------------
    gcAccessibilityTest is null
    

3.引用级别

  • 强引用

    一般程序中的引用

    Student student = new Student();
    
  • 软引用(java.lang.ref.SoftReferenct)

    当堆空间不足时, 才会被回收, 所以软引用对象不会引起内存溢出

    SoftReference<Student> softReference = new SoftReference<>(new Student());
    
  • 弱引用(java.lang.ref.WeakReferenct)

    GC时, 只要发现存在弱引用, 无论堆空间是否不足, 都会将其回收

    WeakReference<Student> weakReference = new WeakReference<>(new Student());
    
  • 虚引用

    • 与没有引用是一样的

    • 虚引用必须和引用队列在一起使用

    • 作用: 用于追踪GC回收过程, 可以将一些资源释放操作放置在虚引用中执行, 进行记录

4.主要的垃圾回收算法

4.1.引用计数法

对于对象A

  • 只要有任何一个对象引用了A, A的引用计数器就会加1

  • 反之引用失效了, 则引用计数器将减1

  • 只要引用计数器的值变为0, 对象A就不可能再被使用

  • 存在的问题

    • 对于循环引用的情况, 无法进行处理
      引用示意图
    • 引用计数器要求在每次引用产生和消除的时候, 都需要伴随一个加减法操作, 对系统性能会有一定的影响

JVM并未选择此算法作为垃圾回收算法

4.2.标记清除法

分为两个阶段: 标记阶段清除阶段

标记清除法示意图

  • 存在的问题

    • 空间碎片

老年代中, 使用了标记清除法

4.3.复制算法

将原有内存空间分为两块, 每次只使用其中一块内存

例如:

A内存, GC时将存活的对象复制到B内存中, 然后清除掉A内存所有对象, 开始使用B内存

复制算法示意图

  • 优点

    • 没有空间碎片(解决了标记清除算法空间碎片问题)

    • 垃圾对象越多, 效率越高

  • 存在的问题

    内存浪费

新生代中, 使用了复制算法

4.4.标记压缩法

首先标记存活对象, 然后将所有存活对象压缩到内存的一端, 然后再清理所有存活对象之外的空间

标记压缩法示意图

  • 优点

    • 没有空间碎片

    • 没有内存浪费

    • 性价比较高

  • 存在的问题

    • 效率相对较低

老年代中, 同时也使用了标记压缩法

4.5.分代算法

将堆空间划分为新生代和老年代, 根据它们之间的不同特点,执行不同的回收算法,提升回收效率

分代策略

4.6.分区算法

1.将堆空间划分成连续的不同小区间, 每个区间独立使用、回收

2.由于当堆空间大时, 一次GC的时间会非常耗时, 所以可以控制每次回收多少个小区间, 而不是整个堆空间, 从而减少一次GC所产生的停顿

分区算法示意图

5.JVM垃圾收集器

默认启用ParallelOldGC

Available Collectors官方文档

  • 串行回收器——Serial

    • 启用指定收集器

      JVM参数新生代老年代
      -XX:+UserSerialGC串行回收器串行回收器
      -XX:+UserParNewGCParNew串行回收器
      -XX:+UserParallelGCParallelGC串行回收器
  • 并行回收器——ParNew & ParallelGC & ParallelOldGC

    • 启用指定收集器

      JVM参数新生代老年代
      -XX:+UserParNewGCParNew串行回收器
      -XX:+UserParallelGCParallelGC串行回收器
      -XX:+UserParallelOldGC(默认启用)ParallelGCParallelOldGC
  • 并行回收器——CMS

    JVM参数新生代老年代
    -XX:+UseConcMarkSweepGCParNewCMS
  • 新生代GC——G1

    JVM参数作用
    -XX:+UseG1GC启用G1收集器
    -XX:MaxGCPauseMillis指定目标最大停顿时间
    -XX:PartallelGCThreads设置并发线程数量
    -XX:InitiatingHeapOccupancyPercent指定堆的使用率是多少时, 触发并发标记周期(默认45)

6.JDK工具

  • JPS

    用于列出Java的进程

    执行语法: jps [-options]
    
  • jstat

    用于查看堆中的运行信息

    执行语法: jstat <-option> [-t] [-h<lines>] <vmid> [<interval> [<count>]
    
  • jinfo

    用于查看运行中java进程的虚拟机参数

    执行语法: jinfo [option] <pid>
    
  • jmap

    用于生成指定java进程的dump文件; 可以查看堆内对象实例的统计信息, 查看ClassLoader信息和finalizer队列信息

    执行语法: jmap [option] <pid>
    
  • jhat

    用于分析jmap生成的堆快照

    执行语法: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
    
  • jstack

    用于导出指定java进程的堆栈信息

    执行语法: jstack [-l] <pid>
    
  • jcmd

    用于导出指定java进程的堆栈信息, 查看进程, GC等

    执行语法: jcmd <pid | main class> <command ...|PerfCounter.print|-f file>
    

附录

  1. 并行回收器JVM参数

    收集器JVM参数作用
    ParNew-XX:ParallelGCThreads指定GC时工作的线程数量
    ParallelGC-XX:MaxGCPauseMillis最大的垃圾收集暂停时间
    -XX:GCTimeRatio设置垃圾收集吞吐量
    -XX:+UseAdaptiveSizePolicy打开自适应垃圾收集策略
    ParallelOldGC-XX:ParallelGCThreads指定GC时工作的线程数量
    CMS-XX:-CMSPrecleaningEnable禁用预清理操作
    -XX:ConcGCThreads设置并发线程数量
    -XX:PartallelCMSThreads设置并发线程数量
    -XX:-CMSInitiatingOccupancyFraction老年代空间使用达到某百分比, 执行CMS(默认68)
    -XX:+CMSCompactAtFullCollectionGC后, 进行一次碎片整理
    -XX:CMSFullGCsBeforeCompaction指定执行多少次GC后, 进行一次碎片整理
  2. JVM常用参数

    执行语法: Java [-options] [package+className] [arg1,arg2,…,argN]

    options作用
    -Xms128m设置初始化堆内存为128M
    -Xmx512m设置最大堆内存为512M
    -Xmn160m设置新生代大小为-Xmn160M(堆空间1/4~1/3)
    -Xss128m设置最大栈内存为128M
    -XX:SurvivorRatio设置新生代eden区与from/to空间的比例关系(默认值8:2, 即8:1:1)
    -XX:PretenureSizeThreshold设置大对象直接进入老年代的阈值(默认值0, 即都先进入eden区)
    -XX:MaxTenuringThreshold设置对象进入老年代的年龄阈值(默认值15)
    -XX:PermSize=64M设置初始永久区64M
    -XX:MaxPermSize=128M设置最大永久区128M
    -XX:MaxMetaspaceSize设置元数据区大小(JDK1.8 取代永久区)
    -XX:+DoEscapeAnalysis启用逃逸分析(Server模式)(JDK1.7开始, 默认开启)
    -XX:+EliminateAllocations开启标量替换(默认开启)
    -XX:+TraceClassLoading跟踪类的加载
    -XX:+TraceClassUnloading跟踪类的卸载
    -Xloggc:gc.log将gc日志信息打印到gc.log文件中
    -XX:+PrintGC打印GC日志
    -XX:+PrintGCDetails打印GC详细日志
    -XX:+PrintGCTimeStamps输出GC发生的时间
    -XX:+PrintGCApplicationStoppedTimeGC产生停顿的时间
    -XX:+PrintGCApplicationConcurrentTime应用执行的时间
    -XX:+PrintHeapAtGC在GC发生前后, 打印堆栈日志
    -XX:+PrintReferenceGC打印对象引用信息
    -XX:+PrintVMOptions打印虚拟机参数
    -XX:+PrintCommandLineFlags打印虚拟机显式和隐式参数
    -XX:+PrintFlagsFinal打印所有系统参数
    -XX:+PrintTLAB打印TLAB相关分配信息
    -XX:+UseTLAB打开TLAB
    -XX:TLABSize设置TLAB大小
    -XX:+ResizeTLAB自动调整TLAB大小
    -XX:+DisableExplicitGC禁用显示GC (System.gc())
    -XX:+ExplicitGCInvokesConcurrent使用并发方式处理显式GC
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值