JVM第一卷

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM第一卷相关的知识,希望对你有一定的参考价值。

JVM第一卷


引言

  1. 什么是 JVM ?
  2. 学习路线

定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

比较:

jvm jre jdk


学习路线


内存结构

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

1. 程序计数器


定义

Program Counter Register 程序计数器(寄存器)

作用,是记住下一条jvm指令的执行地址

特点

  • 是线程私有的,即每个线程都有自己的程序计数器
  • 不会存在内存溢出

作用

0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // -- 
4: aload_1 // out.println(1);
5: iconst_1 // -- 
6: invokevirtual #26 // -- 
9: aload_1 // out.println(2);
10: iconst_2 // -- 
11: invokevirtual #26 // -- 
14: aload_1 // out.println(3); 
15: iconst_3 // -- 
16: invokevirtual #26 // --
19: aload_1 // out.println(4); 
20: iconst_4 // -- 
21: invokevirtual #26 // -- 
24: aload_1 // out.println(5);
25: iconst_5 // -- 
26: invokevirtual #26 // -- 
29: return


2.虚拟机栈


定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

代码演示

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 
    public static void main(String[] args)
    
           mehtod1();
    
    public static void mehtod1()
    
        int a=1,b=2;
        method2(a,b);
    

    private static int method2(int a, int b) 
        int c=a+b;
        return c;
    



问题辨析

  • 垃圾回收是否涉及栈内存?

栈内存就是一次次的方法调用产生的栈帧内存,而栈帧内存在每一次方法调用后,都会被弹出栈,即自动被回收掉,不需要垃圾回收来管理栈内存

  • 栈内存分配越大越好吗?

栈内存可以在运行时,用过一个虚拟机参数-Xss来指定大小
栈内存越大,线程数越少;

如果内存有500m,我们设置每个线程的栈内存为2m,那么只能同时最多运行250个线程;

如果设置为1m,那么可以同时最多运行500个线程;

由此可知,栈内存设置的越大,反而会影响运行的效率;

栈内存越大,只能够提高方法更多次的递归调用

  • 方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

局部变量只要没有逃离方法的作用范围,便是线程安全的:

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 
    public static void main(String[] args)
    
        for(int i=0;i<3;i++)
        
            new Thread(()->
                mehtod1();
            ).start();
        
    

    public static void mehtod1()
    
          int i=0;
          for (int j=0;j<10;j++)
          
              i++;
          
        log.debug("i= ",i);
    

输出

[Thread-0] [DEBUG] [20220109104708639毫秒] 消息:i= 10
[Thread-1] [DEBUG] [20220109104708639毫秒] 消息:i= 10
[Thread-2] [DEBUG] [20220109104708639毫秒] 消息:i= 10

栈内存溢出

  • 栈帧过多导致栈内存溢出 -----无限递归

  • 栈帧过大导致栈内存溢出


线程运行诊断

案例1: cpu 占用过多

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main 
    public static void main(String[] args)
    
        new Thread(()->
            while(true)
        ,"大忽悠线程1号").start();
    


定位

  • 用top定位哪个进程对cpu的占用过高

top命令提供了实时的对系统处理器的状态监视.它将显示系统中CPU最“敏感”的任务列表.该命令可以按CPU使用.内存使用和执行时间对任务进行排序

  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

ps 为我们提供了进程的一次性的查看,它所提供的查看结果并不动态连续的;如果想对进程时间监控,应该用 top 工具
H是列出当前进程下所有线程信息,-eo是选择自己感兴趣的属性进行展示

  • jstack 进程id

可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号



案例2:程序运行很长时间没有结果,死锁现象

/**
 * @author 大忽悠
 * @create 2022/1/8 15:20
 */
@Slf4j
public class Main

    private static Object a=new Object();
    private static Object b=new Object();
    public static void main(String[] args)
    
      new Thread(()->
           synchronized (a)
           
               try 
                   Thread.sleep(Long.parseLong("1000"));
                catch (InterruptedException e) 
                   e.printStackTrace();
               
               synchronized (b)
               
                   System.out.println("鸡汤来喽12...");
               
           
      ,"大忽悠线程1号").start();

        new Thread(()->
            synchronized (b)
            
                try 
                    Thread.sleep(Long.parseLong("1000"));
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                synchronized (a)
                
                    System.out.println("鸡汤来喽2...");
                
            
        ,"大忽悠线程2号").start();
    


3.本地方法栈

本地方法就是java调用非java代码的接口,并不是所有的 JVM都支持本地方法, 因为 Java虚拟机规范上, 并没有明确要求本地方法的使用语言和具体实现方法. Hotspot VM是本地方法栈和虚拟机栈合二为一的虚拟机

  • 本地方法栈是管理本地方法运行的, 本地方法是通过 C语言实现的, 在 Execution Engine执行时加载本地方法库(Native Method Library)
  • 与虚拟机栈相同: 没有 GC, 不同线程间是隔离的(线程私有的)
  • 当一个线程调用本地方法时,他就进入了一个全新的不再受虚拟机限制的世界,他和JVM有同样的权限。
  • 本地方法可以访问JVM运行时数据区,可以直接使用寄存器,可以直接从堆中分配任意数量的内存。

java中本地方法的体现: native标识

  • 标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程。)
  • 一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似。当一个native method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这非些基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现。有一点要牢牢记住:我们可以在一个native method的本地实现中访问所有的java特性,但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。
  • 如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法,同样的如果一个本地方法被fianl标识,它被继承后不能被重写。

为什么要使用Native Method

  • 与java环境外交互:

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

  • 与操作系统交互:

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。


  • JVM怎样使Native Method跑起来:

我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。

如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的


4.堆

定义

Heap 堆

  • 通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

对象可以当做垃圾被回收的条件是没有人在使用该对象,如果我不断的产生对象,并且这些对象有人在使用他们,这些对象就会占用堆内存而不释放

堆内存溢出演示:

       int i=0;
        try
        
          List<String> list=new ArrayList<>();
          String a="hello";
          while(true)
          
              list.add(a);
              a=a+a;
              i++;
          
        catch (Throwable e)
        
              e.printStackTrace();
            System.out.println(i);
        



注意:Stirng对象是不可变对象,因此每次string操作后得到的是一个新的对象


堆内存诊断

  1. jps 工具

查看当前系统中有哪些 java 进程

  1. jmap 工具

查看堆内存占用情况 jmap - heap 进程id;
只能查询某一时刻堆内存占用情况,不能对堆内存做连续监测

Windows 操作系统系统 IDEA 中 jmap 命令 Error

使用演示:

/**
*
 * 展示堆内存
 */
public class Main

    public static void main(String[] args) throws InterruptedException 
        System.out.println("1.....");
        Thread.sleep(30*1000);
        //10 MB
        byte[] array=new byte[1024*1024*10];
        System.out.println("2....");
        Thread.sleep(30*1000);
        //让堆内存可以被垃圾回收
        array=null;
        //手动调用,进行垃圾回收
        System.gc();
        System.out.println("3....");
        Thread.sleep(1000000L);
     


  1. jconsole 工具

图形界面的,多功能的监测工具,可以连续监测

  1. jvirsualvm–JVM可视化工具

垃圾回收后,内存占用仍然很高。

执行GC之后,堆内存只释放了30M左右。

输入jvirsualvm打开JVM可视化工具,然后复制当前堆内存的快照信息,进行分析排查

这样对堆快照的分析,就可以看出问题所在

jdk自带监控程序jvisualvm的使用

jdk工具之JvisualVM、JvisualVM之一–(visualVM介绍及性能分析示例)


5. 方法区

定义

官网定义

下面是Method Area的中文翻译:

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化和接口初始化中使用的特殊方法(第 2.9 节)。

方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。本规范不要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小,也可以根据计算需要扩大,如果不需要更大的方法区域,可以缩小。方法区的内存不需要是连续的。

Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。

以下异常情况与方法区相关:

如果方法区域中的内存无法满足分配请求,Java 虚拟机将抛出 OutOfMemoryError。


组成

方法区是一个概念,不同的jvm对其的实现不同

Oracle JDK 8之前方法区的实现与JDK8之后


方法区内存溢出

演示:

1.8 之后会导致元空间内存溢出

元空间默认没有内存上限设置,因此最好手动设置上限,才能观察到内存溢出的情况 -XX: MaxMetaspaceSize=8m

/**
*  演示元空间内存溢出
 *  -XX: MaxMetaspaceSize=8m
 */
public class Main extends ClassLoader//可以用来加载类的二进制字节码

    public static void main(String[] args)
       int j=0;
       try
           Main main=new Main();
           for (int i = 0; i < 10000; i++,j++) 
               //ClassWriter作用是生成类的二进制字节码
               ClassWriter cw=new ClassWriter(0);
               //版本号 修饰符--public 类名 包名 父类 接口
               cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
               //返回二进制字节码
               byte[] code = cw.toByteArray();
              //执行类的加载
               main.defineClass("Class"+i,code,0,code.length);
           
       finally 
           System.out.println(j);
       
     


* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace 
*  -XX:MaxMetaspaceSize=8m

1.8 以前会导致永久代内存溢出

* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space 
* -XX:MaxPermSize=8m

常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

实例演示

//下面的程序想要运行,首先要编译成二进制字节码
//二进制字节码包含以下几个部分:
//类的基本信息,常量池,类方法定义包含了虚拟机指令
public class Main

    public static void main(String[] args)
        System.out.println("Hello World");
     

javap -v 编译后的字节码文件: 该指令可以反编译二进制字节码文件,输出二进制字节码包含的信息


类方法定义:


  public com.dhy.Main(); //默认提供的构造方法
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0以上是关于JVM第一卷的主要内容,如果未能解决你的问题,请参考以下文章

Netty网络编程第一卷

Objective-C高级第一卷

学习Java第一卷--态度的转变

《概率》第一卷( 修订和补充第三版)施利亚耶夫著 周概荣译本 勘误

Open Metering System 2021-12 标准 附件第一卷 中文版

Open Metering System 2021-12 标准 附件第一卷 中文版