Java JVM基础入门(一):jvm的组成、串池、常量池、常用程序调优参数

JVM

JVM是java的虚拟机,java的运行环境(java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能

JDK、JRE、JVM的关系图

在这里插入图片描述

常见的JVM:

  • oracle的Hotspot是我们通常使用的jvm
  • openJDK的HotSpot在linux系统中可能用的多些

组成部分

在这里插入图片描述

  • ClassLoader类加载器
  • jvm的内存结构
  • 执行引擎

JVM内存结构

堆内存

Heap堆

  • 通过new关键字,创建的对象都会放入到堆内存

特点:

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

堆内存溢出

一般情况下,堆中存在的对象太多而无法再创建新的对象时,就会出现内存溢出的错误。OutMemoryError:java heap space

可以通过参数调整jvm堆内存大小-Xmx8G

StringTable

字符串常量池,在1.8时是放入到heap堆内存中的,主要存储一些字符串的常量,在每次创建新的字符串对象前,会去字符串常量池中找一下,如果没找到,再次创建新的字符串对象。

当有这样一段代码,我们进行反编译一下

public class Test2 {
    
    
    public static void main(String[] args) {
    
    
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
    }
}

在这里插入图片描述

如上图,两个字符串对象相加,会先创建一个StringBuilder对象将这些字符串对象拼接起来,然后再toString(),创建一个新的String对象放入到堆内存中。

而不同的是,当两个字符串的常量相加时,它不会创建StringBuilder对象,而是直接去字符串常量池中找有没有这个组合完毕的字符串,没有的话直接创建一个放入到字符串常量池中

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";

如上代码,也就是s4的值是需要创建StringBuilder对象拼接完再toString()得来,而s5的值则是直接去字符串常量池中找的,因为s3的"ab"已经存在于字符串常量池中了,那么s5就不会再创建新的"ab"对象了。

而这个原因就是javac在编译期间的优化,结果已经在编译期间就确定为"ab"了。

StringTable特性

  • 常量池中字符串仅是符号,第一次用到时才变为对象

  • 利用串池的机制,避免重复创建字符串对象

  • 字符串变量的拼接原理是StringBuilder对象(1.8)

  • 字符串常量拼接的原理是编译器优化

  • 可以使用intern()方法,主动将串池中还没有的字符串对象放入串池

    // 串池中 ["a", "b"]
    public static void main(String[] args) {
          
          
        String s = new String("a") + new String("b");
        // 在堆中,new String("a") new String("b") new String("ab")
    
        String s2 = s.intern(); // 将s这个字符串对象尝试放入串池,如果有则不会放入,如果没有则将字符串对象放入串池,且当前字符串对象的引用变为串池中的了,最后都会返回串池中的对象
        System.out.println(s2 == "ab"); // s2此时就是串池中的 "ab"		true
        System.out.println(s == "ab");  // s此时已经被放入串池中了		true
    }
    

StringTable的位置

1.6是在永久代的常量池里面

1.8是在堆内存中的,是为了防止内存溢出,在堆内存进行GC时,会对StringTable也进行垃圾回收,释放内存。

StringTable垃圾回收

可以在程序运行时加一些参数,查看StringTable的统计信息,和GC详情

-Xmx100m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

StringTable性能调优

  • 设置程序启动参数

    设置StringTable的bucket桶数量(StringTable就是一个哈希表,由数组+链表组成的,数组中一个元素就是一个桶),增加桶的数量可以减少哈希冲突。默认是6w个桶

    -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics

  • 考虑是否将字符串对象入池

    如果存在大量相同字符串时,可以选择将字符串进行入池,防止创建冗余的字符串对象。

虚拟机栈

栈:线程运行需要的内存空间,每个入栈的元素都是一个栈帧(一个个的方法运行需要的内存:参数,局部变量、返回地址等)

栈的数据结构:先进后出,后进先出。压栈和出栈

  • 一个线程有一个线程栈
  • 每个栈有多个栈帧(Frame)组成,对应每次方法调用需要的内存
  • 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法

栈内存溢出

  • 栈帧过多,导致栈内存溢出(如递归死循环)

    常见的如:部门下有多个员工,同时员工又要绑定部门,完事后转换为json数据,就会出现栈溢出,这时候就改为单向关联即可了

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

本地方法栈

java可以通过调用本地方法接口调用其他语言实现的一些功能,避免了重复造轮子。

程序计数器

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

在这里插入图片描述

作用:记住下一条jvm指令(二进制字节码.class文件中的内容)的地址;当解释器开始解释某一条jvm指令时,程序计数器会把下一条jvm指令的地址记住,当解释器解释完毕后,再次从程序计数器中找新的指令地址开始执行。

由于cpu中的寄存器读取速度快,而且程序读取jvm指令会很频繁,所以直接使用(物理)cpu中的寄存器当做程序计数器来使用。

特点:

  • 线程私有
  • 不会存在内存溢出

方法区

方法区就是一个概念上的东西。方法区主要存储一些类信息、静态变量、常量池等,且在java1.6和java1.8中又有些区分。

在这里插入图片描述

java1.6是以PermGen永久代的方式实现的。

在java8中,对方法区有调整,但同时方法区还是概念上的东西。

方法区中的数据保存在本地的内存中(操作系统内存),像类信息、类加载器、常量池等都是在元空间中。而字符串表不再存放在常量池中,而是在Heap堆内存中了。

常量池

通过javap -v 类文件,对类进行反编译,我们可以深入了解到java代码的工作机制。可以查看到详细的信息(类基本信息、常量池、类方法的定义、包含了虚拟机指令)

这里可以看到一些详细信息

在这里插入图片描述

这里可以看到解释器是怎么执行的

在这里插入图片描述

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到需要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息会放入运行时常量池,并且把里面的符号地址变为真实地址

方法区内存溢出

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

    -XX:MaxPermSize=8m,报错java.lang.OutOfMemoryError:PermGen space

  • 1.8之后会导致原空间内存溢出,模拟一下,执行时设置下原空间大小

    -XX:MaxMetaspaceSize=8m,报错java.lang.OutOfMemoryError:Metaspace

    /**
     * @Date 2023/4/23 17:49
     * @Created by wlh
     * 模拟元空间内存溢出,可以设置元空间的大小
     * -XX:MaxMetaspaceSize=8m
     */
    public class Test1 extends ClassLoader {
          
              // 可以用来加载类的二进制字节码
    
        public static void main(String[] args) {
          
          
            int j = 0;
            try {
          
          
                Test1 test = new Test1();
                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
                    byte[] code = cw.toByteArray();
                    // 执行加载类
                    test.defineClass("Class" + i, code, 0, code.length);
                }
    
            } finally {
          
          
                System.out.println(j);
            }
    
        }
    }
    
    ====================================返回结果=====================
    Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
    3331
    	at java.lang.ClassLoader.defineClass1(Native Method)
    	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    	at com.wlh.test.Test1.main(Test1.java:26)
    

程序启动时手动加载类信息(动态加载类)的场景:

  • Spring
  • mybatis

它们使用的cglib,就需要去加载一些类信息。

常见问题

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

    GC不涉及到栈内存,栈主要是一个个的方法调用,结束后会自动释放内存

  • 栈内存分配越大越好吗

    参数-Xss可以设置线程栈的大小,linux/mac默认都是1024kb,windows是取决于jvm的内存,栈分配空间大些无非就是可以存储更多的方法执行,对程序效率没有什么益处

    假设物理内存是500mb,每个线程大小设置为10mb,那么最多只能有50个线程;若设置为100mb,那么只能有5个线程,所以线程数量会和大小成反比

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

    • 如果方法内局部变量没有逃离方法的作用范围(不是方法参数、没有将此变量return),它是线程安全的
    • 反之则是不安全的,因为外部可以访问到这个变量

线程诊断

CPU过高

在Linux系统中top命令可以查看cpu的使用情况,左侧的PID只能看到进程的ID值

  • 可以使用ps命令查看详细的进程、线程、cpu占用情况,找到有问题的TID(线程id)

    ps H -eo pid,tid,%cpu | grep PID值
    
  • 可以使用jstack PID值查看具体的详细线程信息

    由于信息中的nid都是16进制的,可以去计算器中将有问题的TID转换为16进制,去信息中确认下到底是哪个线程出现了问题。

堆内存诊断

jps工具(命令行)

查看当前系统有哪些java进程,在jps中找到需要的进程ID,注意当前命令窗口的权限是否足够,不够的话是用不了jps的,再一个是检查下java的环境配置的是否有问题。

jmap工具(命令行)

查看堆内存的占用情况 -heap

jmap -heap PID进程号

jconsole工具(图形化)

图形界面,多功能的监测工具,可以连续监测,可以通过命令行进行唤醒打开此工具

在这里插入图片描述

在这里插入图片描述

jvisualvm可视化工具(推荐)

在命令窗口输入jvisualvm命令打开即可

直接内存

定义

Direct Memory:操作系统的内存

  • 常见于NIO操作,用于数据缓冲
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

在这里插入图片描述

进行文件读写时,会从用户态转换为内核态去调用系统的本地方法操作系统资源,那么在系统内存中会创建一个系统的缓冲区,然后再传入到java的缓冲区中进行读写,两次复制没有必要。

改进:

在这里插入图片描述

在系统内存和java的内存中整一个直接内存,磁盘文件数据直接放入到直接内存中即可。共享内存区域,那么在java中是通过ByteBuffer类来实现的。

直接内存释放

int _1GB = 1024 * 1024 * 1024;
ByteBuffer byteBuffer = ByteBuffer.allocate(_1GB);
System.out.println("内存分配完毕...");
System.in.read();
byteBuffer = null;
System.gc();

以上代码执行后,jvm在gc时,会将直接内存回收掉。其实并不是jvm的gc回收了直接内存,而是底层的Unsafe进行内存释放的管理。

ByteBuffer类底层就是使用的Unsafe类的freeMemory()释放的直接内存。

有以下参数可以禁用显式的gc垃圾回收

-XX:+DisableExplicitGC禁用显式full gc

如果有必要,我们可以使用Unsafe来手动管理直接内存,而不要去触发full gc,因为太消耗时间了。

猜你喜欢

转载自blog.csdn.net/weixin_45248492/article/details/130341147