3.程序计数器

Program Counter Register

【线程私有】

3.1.作用

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现 由于线程执行是通过抢占cpu资源来进行的,会不断切换执行的线程, 为了记录线程执行到哪一步了,所以有了程序计数器记录!所以是【线程私有】的!

记录正在执行的字节码指令的编号!【记录当前线程运行到哪一步】

虚拟机运行时内存 - 图1

3.2.特点

线程私有

前面提到了,程序计数器也是为了记录线程执行的进度,在处理器切换线程时恢复到执行的进度!

所以每个线程都需要有一个程序计数器来记录执行进度!

不会OOM

程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。

Native

在执行Native方法时,程序计数器值为空

Native方法大多是通过C实现并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。

4.虚拟机栈

【线程私有】

4.1.定义

线程运行需要的内存空间【每个线程有一个栈】

虚拟机运行时内存 - 图2

活动栈帧:正在执行的方法

栈由一个个的【栈帧】(Frame)组成

  • 栈帧(Frame):每个方法运行时需要的内存(参数)(局部变量)(返回地址)

每次调用方法就压栈,持续完毕则出栈

4.2.栈帧

每个方法执行的同时会创建一个栈帧

栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息。

虚拟机运行时内存 - 图3

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

局部变量表存放:

  • 编译器可知的基本数据类型(8大基本类型)
  • 对象引用(reference类型)
  • returnAddress类型(它指向了一条字节码指令的地址)

PS:这里的基本类型对象引用只针对局部变量!如果是成员变量定义的,他们存储在中【线程共享】

reference

局部变量表中存放的对象引用

简单来说,栈中只是存放了一个地址,这个地址指向堆中的【对象实例】!

4.3.特点

垃圾回收

  • 垃圾回收不涉及栈内存
    每次方法调用结束就会出栈!所以并不需要垃圾回收!

栈内存

-Xss size

虚拟机运行时内存 - 图4

  • 栈内存越大越好?
    不是的!栈内存越大,反而线程数量越少!
  • 一般只需要采用系统默认栈内存即可

局部变量

  • 方法内的局部变量是否线程安全
    • 如果方法内局部变量没有逃离方法的作用范围,它是【线程安全】的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,则可能【线程不安全】(如方法2、3)

虚拟机运行时内存 - 图5

4.4.栈溢出

什么情况会栈溢出?StackOverFlow

  • 栈帧过多(如:递归调用)
  • 栈帧过大!(很少见)
  • 例子:
    背景:有两个类,Emp员工,Dept部门。他们互相引用了!
    那么使用Json将它们转换为json字符串时,就会出现循环引用
  • 如何解决?:
    可以在Emp类中的部门属性上添加注解@JsonIgnore在转换时忽略这个引用

5.本地方法栈

Native Method Stacks

Java通过本地方法调用底层功能!

Native

说明:凡是带了native关键字的,说明java的作用范围达不到了,他就会去调用底层c语言的库

会进入本地方法栈—->调用本地方法接口 JNI (Java Native Interface)

Java Native Interface 本地方法接口

JNI的作用:【扩展java的使用,融合不同的编程语言为Java所用】

在内存区域中开辟了一块表及区域: Native Method Stack(本地方法栈) —> 登记native方法

会在最终执行的时候,通过 JNI 加载本地方法库中的方法

6.堆Heap

【线程共享】

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

堆内存溢出

堆内存溢出:java.lang.OutOfMemeryError: Java heap space

参数-Xmx

诊断

控制台输入即可

1.jps工具

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

2.jmap工具

查看堆内存占用情况

`jmap -head [线程id]`

3.jconsole工具

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

4.jvisualvm

虚拟机运行时内存 - 图6

7.方法区

元空间—本地内存

7.1.定义

jdk1.8

  • 线程共享
  • 存储类结构数据
    • 成员变量
    • 成员方法、构造方法
  • 方法区在虚拟机启动时被创建
  • 方法区也会导致内存溢出

官方文档中提到 理论上方法区是堆的一部分 但是并不强制要求虚拟机这样设计

例如HotSpot在JDK8以后,JVM的方法区直接在【本地内存】中定义了(元空间)

JDK8 HotSpot JVM 使用本地内存来存储类元数据信息并称之为:元空间(Metaspace); ps:jDK8以前 方法区在【堆内存】的【永久代】中

虚拟机运行时内存 - 图7

7.2.内存溢出

代码演示

代码演示【jdk1.8】

需要添加参数-XX:MaxMetaspaceSize=10m

/**
 * 演示元空间内存溢出
 * -XX:MaxMetaspaceSize=10m
 */
public class OOMDemo extends ClassLoader { //用来加载类的二进制字节码
    public static void main(String[] args) {
        int j=0;
        try{
            OOMDemo demo = new OOMDemo();
            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();
                //执行类的加载 读取字节码。加载类
                demo.defineClass("Class"+i,code,0,code.length); //Class对象
            }
        }finally {
            System.out.println(j);
        }
    }
}

发现内存溢出了!OutOfMemoryError:Metaspace ====> Metaspace

image-20210717110855108

在JDK1.6中报错结果:

java.lang.OutOfmemoryError: PermGen space ===>PermGen space 永久代

image-20210717110922952

实际场景

  • spring ===> cglib 生产 代理类
  • Mybatis ====> cglib 产生 mapper接口实现类

cglib与上诉代码演示中的ClassWriter类似,都是动态生产字节码 所以经常尝试大量类,在可能会造成内存溢出,但是在1.8以后使用本地内存后出现的情况降低很多!

7.3.常量池

class常量池

一个java程序都需要被编译为【二进制字节码】

而一般【二进制字节码】包含:1.类基本信息 2.常量池 3.类方法定义 4.虚拟机指令

通过javap命令反编译*.class文件可以查看这些信息

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

class常量池存放:

  • 字面量(Literal):1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等; 如String s="ab"中的ab
  • 符号引用(Symbolic References):1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符

    Java在编译时,无法得到真正的内存入口地址,只有等待虚拟机加载Class【动态链接】时,再从常量池获得对应的符号引用,解析、翻译到具体的内存地址中!

常量池中的内容将在类加载后存放到方法区的运行时常量池中!!!

7.4.运行时常量池

【类加载】时生成的【直接引用】等信息

当类被加载,它的常量池信息就会放入【运行时常量池】,并把里面的符号地址转变为真实地址 (类加载过程:链接-解析)

JVM在执行某个类时,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。

当类加载到内存中后,jvm会将class常量池中的内容存放到运行时常量池中,运行时常量池也是每个类都有一个。

在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池(StringTable),以保证运行时常量池所引用的字符串与字符串常量池中是一致的

7.5.字符串常量池

StringTable ——> 存放在【堆】(1.8)

Tips:当执行时遇到新字符串才会在字符串常量池中创建。【懒加载】

//StringTable ["a","b","ab"]
public class StringTableDemo {
    //常量池中对信息,都会被加载到运行时常量池中,此时 a b ab 都是常量池中到符号,还没有变成java字符串对象
    //ldc #2 会把 a符号变成"a"字符串对象
    //ldc #3 会把 b符号变成"b"字符串对象
    //ldc #4 会把 ab符号变成"ab"字符串对象
    public static void main(String[] args) {
        String s1="a";
        String s2="b";
        String s3="ab";
        String s4="a"+"b";
        String s5=s1+s2; //new StringBuilder().append("a").append("b").toString();
    }
}

javap

虚拟机运行时内存 - 图10

虚拟机运行时内存 - 图11

  • s1=”a” s2=”b” s3=”ab”在创建后被存入StringTable字符串常量池

分析String s4="a"+"b"【编译期常量】

虚拟机运行时内存 - 图12

  • 直接通过 ldc #4 去获取常量池中的”ab”了!

这是javac在编译期间的优化!!

分析String s5=s1+s2

虚拟机运行时内存 - 图13

  • 发现s5是创建了一个StringBuilder对象!
  • 然后通过加载s1和s2将它们append进去再调用toString
  • 相当于new StringBuilder().append("a").append("b").toString();

总结

还有一点,String s4="a"+"b"是常量相加,在编译期间可以确定他的值(”a”+”b”)不会发生修改了

String s5=s1+s2是变量相加,在编译期间无法确保s1,s2变量在后续的代码中是否会被改变。所以使用StringBuilder拼接

特点

  • Class常量池中的字符串仅是符号,第一次用时才会变成对象
  • 利用【串池】的机制,避免重复创建字符串对象!
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将【串池】中还没有的字符串放入【串池】
    【jdk1.8】该方法将字符串对象尝试放入串池,如果已存在则不会放入,但是会返回串池对象

垃圾回收

代码演示

/**
 * 演示StringTable垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 堆内存最大值       打印字符串常量池示例个数           垃圾回收详细信息
 */
public class StringTableDemo03 {
    public static void main(String[] args) {
        int i=0;
        try{
            for (int j = 0; j < 100; j++) {
                String.valueOf(j).intern();
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

串池数据:【经过测试,初始时为841】

发现添加了100个字符串实例到串池中!

虚拟机运行时内存 - 图14

  • 接下来我们尝试添加2w个字符串

    for (int j = 0; j < 20000; j++) {
      String.valueOf(j).intern();
      i++;
    }
    
  • 虚拟机运行时内存 - 图15

  • 居然只有7000+?往上翻页发现了GC信息!虚拟机运行时内存 - 图16
  • 原来堆内存占用过多,就把无用信息给垃圾回收了!

性能调优

方法1:调整bucket

我们知道StringTable是一个哈希表【数组+链表】

StringTable调优主要就是调整哈希表 【bucket】桶的个数

bucket越多,哈希碰撞越少。查询效率高 bucket少,碰撞多!链表长,效率低

  • -XX:StringTableSize= 调整StringTable桶的个数 【默认为60013】
    • 这里桶的大小直接影响了读取效率
    • 桶的取值范围[1009,2305843009213693951] 最小1009

方法2:考虑是否入池

如果程序中有大量字符串【且存在大量重复】

那么入池能够节约堆内存使用

7.6.直接内存

Direct Memory

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本较高,但读写性能高 ====>相较于普通io
  • 不受JVM内存回收管理
    • 通过unsafe例子的演示可以发现:直接内存的释放由unsafe类来做
    • 而java垃圾回收只会对java内存进行管理

文件缓存区例子:

Java读取文件图:

虚拟机运行时内存 - 图17

图中可以看到java读取磁盘文件不但要在系统内存中开辟【系统缓存区】还要在java堆内存开辟【java缓存区】

使用直接内存:

ByteBuffer.allocateDirect(缓存区大小)

使用直接内存则会在操作系统上开辟一块内存区域(数据缓冲区),而这个内存系统和java都可以访问。提高了效率

内存分配/释放

通过查看DirectByteBuffer的源码发现

  • 在构造方法中就利用unsafe.allocateMemory(size)进行了直接内存的内存分配

内存释放

在其构造方法中还使用了Cleaner==>一个虚引用对象【当他引用的对象被垃圾回收时,则会调用它的方法

  • 这里它引用的对象指【ByteBuffer】(ByteBuffer还是个Java对象,受垃圾回收管理)
  • 调用它的回调方法中也同样使用了unsafe.freeMemory()来释放内存!

禁用显式内存gc

-XX:+DisableExplicitGC

会对直接内存有影响!因为必须等到垃圾回收将调用直接内存的对象回收之后才回释放内存

解决方法:

  • 使用Unsafe类来手动的释放内存