内存结构

1、程序计数器(寄存器)

作用:用于记住下一条JVM指令的内存地址,物理上是一个寄存器,因为指令需要频繁地读取,所以JVM设计时将CPU(运算器 、 控制器 、 寄存器组 和 内部总线 构成)中读取速度最快的寄存器单元用作程序计数器

特点:

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

2、虚拟机栈

2.1 定义

​ 线程运行需要的内存空间,每一个栈由多个栈帧组成,包含参数,局部变量,返回地址等

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

垃圾回收主要在堆空间内完成

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

不是,栈内存空间划分太大会降低线程的数量,因为物理空间是有限的,栈空间越大只能加快某些方法的递归调用,不会提升效率。

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

局部变量存在于每个线程的栈帧内,而如果是静态成员变量则非线程安全。

如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。

如果是局部变量引用了对象,并逃离方法的作用范围,那么需要考虑线程安全问题了。

2.2 栈溢出(StackOverflow)

1、栈帧过多(递归方法调用)

2、栈帧过大(一个栈帧里面局部变量太多,这种情况不常见)

可通过-Xss size设置栈空间大小,一般默认大小

2.3 线程运行诊断

案例一、cpu占用过高

定位:

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

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

3、jstack 线程id

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

案例二、程序运行很长时间没有结果

可能原因:程序死锁,可利用jstack去排查

3、本地方法栈

Java无法执行而去调用底层的C/C++代码

4、堆

Heap,通过new关键词创建对象都会放在堆内存中

特点:

  1. 线程共享,需要考虑线程安全问题
  2. 有垃圾回收机制(GC)

4.1 堆内存溢出

当某个对象不再使用后JVM会进行垃圾回收,但是如果长时间使用并增大内存可能导致堆内存溢出

public class OOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        String str = "kexing";
        try {
            while(true){
                str = str + str;
                list.add(str);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray0(Unsafe.java:1278)
    at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1271)
    at java.base/java.lang.StringConcatHelper.newArray(StringConcatHelper.java:458)
    at java.base/java.lang.StringConcatHelper.simpleConcat(StringConcatHelper.java:423)
    at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder)
    at java.base/java.lang.invoke.DelegatingMethodHandle$Holder.reinvoke_L(DelegatingMethodHandle$Holder)
    at java.base/java.lang.invoke.Invokers$Holder.linkToTargetMethod(Invokers$Holder)
    at site.kexing.heap.OOM.main(OOM.java:17)

4.2 查看堆内存数据

1、通过jps查看进程

2、jhsdb jmap --heap --pid 16279 查看堆内存数据

Heap Usage:
G1 Heap:
   regions  = 2012
   capacity = 2109734912 (2012.0MB)
   used     = 1561400 (1.4890670776367188MB)
   free     = 2108173512 (2010.5109329223633MB)
   0.07400929809327628% used


G1 Young Generation:
Eden Space:
   regions  = 0
   capacity = 4194304 (4.0MB)
   used     = 0 (0.0MB)
   free     = 4194304 (4.0MB)
   0.0% used
Survivor Space:
   regions  = 0
   capacity = 0 (0.0MB)
   used     = 0 (0.0MB)
   free     = 0 (0.0MB)
   0.0% used
G1 Old Generation:
   regions  = 3
   capacity = 6291456 (6.0MB)
   used     = 1561400 (1.4890670776367188MB)
   free     = 4730056 (4.510932922363281MB)
   24.817784627278645% used

5、方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

JDK1.7及以前,习惯把方法区称为永久代,而JDK1.8开始,使用元空间取代了永久代。

➢本质上,方法区和永久代并不等价。仅是对hotspot而言的。《Java 虚拟规范》对如何实现方法区,不做统一要求。例如: BEA JRockit/IBM J9中不存在永久代的概念。
➢现在来看,当年使用永久代,不是好的idea。导致Java程序更容易OOM (超过-XX :MaxPermSize.上限)

5.1、内存溢出

JDK1.8以前永久代内存溢出:java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
JDK1.8之后元空间内存溢出:java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspaceSize=8m

5.2、运行时常量池

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

运行时常量池,常量池是.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

5.3、StringTable

JDK1.6StringTable存放在永久代中,因为永久代中只能通过Full gc进行垃圾回收,而StringJava中又是需要频繁使用,所以JDK8StringTable放在堆空间新生代中

public class Demo {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = "ab";
        String c = a + b;
        System.out.println(ab == c);
    }
}

查看字节码(JDK8):

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         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: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4
        29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        32: aload_3
        33: aload         4
        35: if_acmpne     42
        38: iconst_1
        39: goto          43
        42: iconst_0
        43: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        46: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 8: 9
        line 10: 29
        line 11: 46
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      47     0  args   [Ljava/lang/String;
            3      44     1     a   Ljava/lang/String;
            6      41     2     b   Ljava/lang/String;
            9      38     3    ab   Ljava/lang/String;
           29      18     4     c   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 42
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo.java"

可以发现String c = a + b;底层是使用StringBuilder操作,new StringBuilder().append("a").append("b").toString();,使用toString生成一个新String对象。

StringTable特性
  • 利用串池机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串拼接时编译器会进行优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
StringTable调优
  • StringTable底层其实是一个散列表,数组+链表结构,为了避免大面积hash碰撞,可以扩大桶位数量,设置虚拟机参数:-XX:+PrintStringTableStatistics -XX:StringTableSize=200000
  • 如果应用里有大量字符串并且存在很多重复的字符串,可以考虑使用intern()方法将字符串入池,而不是都存在Eden区中,这样字符串仅会占用较少的空间。
最后修改:2021 年 06 月 16 日 06 : 00 PM
如果觉得我的文章对你有用,请随意赞赏