JVM学习笔记一(内存结构)

150 阅读15分钟

内存结构

image-20210616104707371.png

  • 程序计数器

  • 虚拟机栈

  • 本地方法栈

  • 方法区

1.程序计数器

1) 定义

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

作用:是记录下一条 jvm 指令的执行地址行号。 特点:

  • 是线程私有的
    • CPU会为每个线程分配时间片,当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
    • 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
  • 不会存在内存溢出
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
  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

2.虚拟机栈

定义

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

问题辨析

  • 垃圾回收是否涉及栈内存?
    • 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
  • 栈内存的分配越大越好吗?
    • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  • 方法内的局部变量是否是线程安全的?
    • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全
    • 如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

内存溢出

Java.lang.stackOverflowError 栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

发生原因

  • 虚拟机栈中,栈帧过多(无限递归)
  • 每个栈帧所占用过大

线程运行诊断

CPU占用过高

  • Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
    • top命令,查看是哪个进程占用CPU过高
    • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看是哪个线程占用CPU过高
    • jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的需要转换

3、本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法

4、堆

定义

通过new关键字创建的对象都会被放在堆内存

特点

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

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出

可以使用 -Xmx8m 来指定堆内存大小。

堆内存诊断

  • jps 工具 查看当前系统中有哪些 java 进程

    D:\idea项目\jaavBasic\javaDemo>jps
    2128 RemoteMavenServer
    1236 RemoteMavenServer
    21572 Launcher
    10872
    14424 Demo5
    23400 Launcher
    20908 Launcher
    3228 Jps
    
    
  • jmap 工具 查看堆内存占用情况 jmap - heap 进程id

    D:\idea项目\jaavBasic\javaDemo>jmap -heap 14424
    Attaching to process ID 14424, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.251-b08
    
    using thread-local object allocation.
    Parallel GC with 10 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 4127195136 (3936.0MB) //最大堆内存
       NewSize                  = 85983232 (82.0MB)
       MaxNewSize               = 1375731712 (1312.0MB)
       OldSize                  = 171966464 (164.0MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 21807104 (20.796875MB)
       CompressedClassSpaceSize = 1073741824 (1024.0MB)
       MaxMetaspaceSize         = 17592186044415 MB
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 65011712 (62.0MB)
       used     = 16988376 (16.201377868652344MB)
       free     = 48023336 (45.798622131347656MB)
       26.13125462685862% used
    From Space:
       capacity = 10485760 (10.0MB)
       used     = 0 (0.0MB)
       free     = 10485760 (10.0MB)
       0.0% used
    To Space:
       capacity = 10485760 (10.0MB)
       used     = 0 (0.0MB)
       free     = 10485760 (10.0MB)
       0.0% used
    PS Old Generation
       capacity = 171966464 (164.0MB)
       used     = 0 (0.0MB)
       free     = 171966464 (164.0MB)
       0.0% used
    
    3168 interned Strings occupying 259976 bytes.
    
  • jconsole 工具 图形界面的,多功能的监测工具,可以连续监测

    jconsole
    
  • jvisualvm 工具

5、方法区

定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的

结构

20200608150547.png

方法区内存溢出

  • 1.8 之前会导致永久代内存溢出
    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小
  • 1.8 之后会导致元空间内存溢出
    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
  • 场景
    • spring
    • mybatis

常量池

二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)

javap -v Demo8.class
Classfile /D:/idea项目/jaavBasic/javaDemo/out/production/Day08Demo/com/hyy/demo1/Demo8.class
  Last modified 2021-6-22; size 546 bytes
  MD5 checksum 4a4d813fdaa8a69dd018ec3f900b0c48
  Compiled from "Demo8.java"
public class com.hyy.demo1.Demo8
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:  //常量池
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/hyy/demo1/Demo8
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/hyy/demo1/Demo8;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Demo8.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/hyy/demo1/Demo8
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.hyy.demo1.Demo8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hyy/demo1/Demo8;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 12: 0
        line 13: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "Demo8.java"

运行时常量池

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

常量池与串池的关系

串池StringTable

特征

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
  • 注意:无论是串池还是堆里面的字符串,都是对象

用来放字符串对象且里面的元素不重复

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

常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

0: ldc           #2                  // String a
2: astore_1
3: ldc           #3                  // String b
5: astore_2
6: ldc           #4                  // String ab
8: astore_3
9: return
当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,**并放入串池中**(hashtable结构 不可扩容)

当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
  • 最终StringTable [“a”, “b”, “ab”]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

使用拼接字符串变量对象创建字符串的过程

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
		//拼接字符串对象来创建新的字符串
		String ab2 = a+b; //new StringBuilder().append().append().toString()   new String("ab");
	}
}
  • 通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

  • 最后的toString方法的返回值是一个新的字符串,但字符串的和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

String s5 ="a"+"b"; //javac在编译期进行优化,结果已经能确定。

 29: ldc           #4                  // String ab
  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

intern方法 1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

public class Main {
	public static void main(String[] args) {
		//"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
		//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
		String st2 = str.intern();
		//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
		String str3 = "ab";
		//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
		System.out.println(str == st2);  //true
		System.out.println(str == str3); //true
	}
}
public class Main {
	public static void main(String[] args) {
        //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
		String str3 = "ab";
        //"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
        //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
		String str2 = str.intern();
        //false
		System.out.println(str == str2);
        //false
		System.out.println(str == str3);
        //true
		System.out.println(str2 == str3);
	}
}

intern方法 1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

StringTable 的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

StringTable 垃圾回收

-Xmx10m 指定堆内存大小 -XX:+PrintStringTableStatistics 打印字符串常量池信息 -XX:+PrintGCDetails -verbose:gc 打印 gc 的次数,耗费时间等信息

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

Heap
 PSYoungGen      total 2560K, used 1668K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 81% used [0x00000000ffd00000,0x00000000ffea1220,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3146K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13120 =    314880 bytes, avg  24.000
Number of literals      :     13120 =    563216 bytes, avg  42.928
Total footprint         :           =   1038184 bytes
Average bucket size     :     0.656
Variance of bucket size :     0.656
Std. dev. of bucket size:     0.810
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1749 =     41976 bytes, avg  24.000
Number of literals      :      1749 =    157136 bytes, avg  89.843
Total footprint         :           =    679216 bytes
Average bucket size     :     0.029
Variance of bucket size :     0.029
Std. dev. of bucket size:     0.171
Maximum bucket size     :         3

StringTable 性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池 可以通过 intern 方法让堆内存中的重复的对象放到串池中,减少堆内存。

直接内存

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

文件读写流程

image-20210622170213999.png

使用了DirectBuffer

image-20210622170604719.png

直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率

释放原理

直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放

直接内存的回收机制总结
  • 使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法
  • ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存