【JVM学习-黑马】内存结构篇

本文详细介绍了Java内存的各个组成部分,包括程序计数器、虚拟机栈、本地方法栈、堆内存、方法区(以及JDK1.8后的元空间),并讨论了线程安全、内存溢出、CPU占用过高和线程死锁的排查。此外,还特别讲解了StringTable的工作原理和直接内存的使用及释放策略。
摘要由CSDN通过智能技术生成

程序计数器

作用

记住下一条 JVM 指令的执行地址。 每次执行一条 jvm 指令的同时,也会把下一条 jvm 指令的地址给到程序计数器,之后执行都可以通过程序计数器的地址取下一条指令。
Java 源代码到 cpu 运行的整个流程:
  1. java 源代码先被编译器 javac.exe,编译成字节码文件(.class)
  2. 然后字节码文件再被 java.exe 执行。
  3. java.exe会将字节码文件中的 JVM 指令,通过解释器编译成机器码再交给cpu执行。

特点

  1. 线程私有。 每个线程有一个单独的程序计数器,当线程在时间片还没执行完,通过程序计数器记录上次程序执行到哪个位置。
  2. 不会存在内存溢出。

虚拟机栈(一般指Java栈)

&栈帧的关系

  • 栈:用于存储栈帧,先进后出,类似弹夹。
  • 栈帧: 线程每执行一个方法,就会产生一个栈帧存储到栈。当方法执行完后,就会释放该栈帧的内存。栈帧用于存储局部变量,局部变量随栈帧销毁而消失。

定义

  • 每个线程运行时所需要的内存,就是虚拟机栈。
  • 每个线程只有一个活动栈帧,也就是线程运行到对应的那个方法。

FAQ

1. 垃圾回收是否涉及栈内存?
  • 不会。因为方法执行完之后,就会自动释放栈帧的内存。
2. 栈内存分配越大越好吗?
  • 不是。 因为物理内存是固定的,假设每个栈内存设置1M,物理内存总共50M,那么最多就能同时运行 50个线程。如果设置为2M,那么最多只能执行25个线程。 栈内存设置大了,可以存放更多栈帧,线程可以递归调用更多次,但是运行效率不会提升。
3. 方法内的局部变量是否线程安全?
  • 如果方法内的局部变量没有在方法范围之外,那么就是线程安全的。
  • 如果局部变量引用对象,且出了方法范围之外,那么可能线程不安全(基本类型不会)。
    // m1的StringBuilder线程安全,因为没有逃离方法之外。
    public static void m1() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }


    // m2的StringBuilder线程不安全,因为m2的sb参数来自于外界
    public static void m2(StringBuilder sb) {
        sb.append(1); 
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }


    // m3的StringBuilder线程不安全,因为m3的sb参数返回到外界
    public static StringBuilder m3() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3); 
        return sb;
    }

栈溢出(StackOverFlowError

        通常是方法递归过深导致的。

cpu占用过高排查步骤

1. top 命令,找到 java pid( 进程号 )
2. ps H -eo pid,tid,%cpu | grep pid ,找到该进程的所有线程,定位哪一个线程引起 cpu 占用问题。

3. jstack pid ,可定位到哪个线程出了问题,并且可以定位到源代码哪一行(步骤 2 tid 换算成 16
制找到才能哪个线程出了问题)

线程死锁排查步骤

1. jstack pid ,翻到最后看到这个信息

2. 查看源码 29 行和 21

3. 线程 1 锁住 a ,睡眠 2 秒。然后线程 2 锁住 b ,再锁住 a ,但是 a 被线程 1 锁了。线程 1 苏醒,然后想锁
b 。两个线程互相等待,导致线程死锁。

本地方法栈

本地方法栈:本地方法运行时用到的内存 ,就是本地方法栈。
有些功能不能由 JAVA 代码直接实现,需要间接使用本地方法来实现,此类方法由 native 修饰。

堆内存能存储对象、数组。

特点

  1. 线程共享,所有线程都能访问堆中的对象。
  2. 存在垃圾回收机制。

堆内存诊断工具

如果环境变量没有配置好,以下命令会用不到

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

jmap:查看堆内存占用情况 (命令:jmap -heap pid)

堆内存配置信息

堆内存使用情况(年轻代)

jconsole:图形化界面

jvisualvm:图形化界面(实用)

 点击“堆 dump”可以某一时刻的堆空间情况进行快照

可查看该时刻有哪些对象占用内存空间比较多

方法区

JDK1.6 时,方法区通过永久代来实现(方法区是一种规范)。该方法区主要存储类的信息,类加载器,运行时常量池等。

到了 JDK1.8 ,永久代被元空间代替,并且不再由 JVM 来管理元空间的内存。StringTable(字符串池)也改成放到堆内存中。

元空间代替永久代的意义:
把方法区放到本地内存后, JVM 的内存也变得充裕了,而且垃圾回收机制也交给了元空间管理,垃圾回 收的效率也相对提高了。

StringTable

1. 常量池中的字符串仅仅是符号,当代码被执行到该字符串时,才被创建为字符串对象,并放入串池。

 

2. 字符串变量加号拼接,实际上是StringBuilder的拼接。

String a = "a"; 
String b = "b"; 
String s1 = "ab"; 
String s2 = a + b; // new StringBuilder().append(a).append(b).toString(); new String("ab")
System.out.print(s1 == s2); // false
s2 StringBuilder 拼接的结果, s2 指向的是堆中 String 对象的地址。
s1 是直接的字面量,指向的是串池中的字符串对象的地址。二者不是同一个对象, false

3. 字符串的常量拼接,在编译期已经优化。

String a = "a"; 
String b = "b"; 
String s1 = "ab"; 
String s2 = "a" + "b";
String s3 = a + b;
System.out.print(s1 == s2); // true
System.out.print(s1 == s3); // false
s2 是两个常量的拼接,在编译期间的结果是不会变的,在编译期间就被确定为是字符串 "ab" ,和执行 s1 时是同一个字符串。
s3 是两个变量的拼接,在编译期间可能会变化,所以需要 StringBuilder 的方式来动态拼接字符串。

4. 串池中的字符串是唯一的。

System.out.print("1");
System.out.print("2"); // 串池中有1和2
System.out.print("1");
System.out.print("2"); // 串池还是只有1和2,因为前两行已经创建过,后面两行不会再创建

5.intern()方法的特性。

intern 方法的作用就是把字符串放入串池中。
JDK 1.7之后

例子1

String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == "ab"); //true
System.out.print(s2 == "ab"); //true
第一行代码执行完, StringTable 中含有 [a,b] ,此时 s1 是指向堆中一个 String 对象的地址。
第二行代码执行完,成功把 ab 放入串池,所以 s1 指向的是串池中的 ab 字符串对象, StringTable 中含有 [a,b,ab], s2 返回的也是串池中的 ab 对象。
例子 2
String s = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == s); //false
System.out.print(s2 == s); //true
在例子 1 的基础上,在第一行加上一行代码。
第一行执行完, StringTable[ab]
第二行执行完, StringTable[a, b, ab]
第三行执行完, ab 字符串已存在于 StringTable ,所以放入串池失败, s1 依旧是堆中的 String 对象,但是 s2是串池中的对象。
JDK1.6
JDK1.7 之后的 intern 方法,和 JDK1.6 不一样。
JDK1.7 之后, intern 方法是把原来的对象放入串池中。
JDK1.6 intern 方法是把原来的对象复制一份,放入串池,因此不是相同的对象。
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == "ab"); //false 
System.out.print(s2 == "ab"); //true
第一行执行完, StringTable[ab]
第二行执行完, StringTable[a, b, ab] s1 对象被复制了一份,放入到串池,所以 s1 仍然是堆中的 String 对象,而s2 返回的是串池的对象。

6. JDK1.7以后,StringTable从永久代迁移到堆内存的意义

JDK1.6 的时候, StringTable 在永久代的回收效率非常低,触发回收的条件非常苛刻,需要 FullGC 才会触发。JDK1.7 后,迁移到了堆中,触发 MinorGC 就可以回收。

7. StringTable的性能调优

方案一:把StringTableSize调大,也就是把桶个数调大。

StringTable的底层其实是一张哈希表,把桶的个 数调大实际就是为了减少哈希碰撞的概率,提高查找字符串的效率。可以类比HashMap,当两个元素的哈希值一样时,就会形成链表,链表的查询速度是比较慢的,如果链表长度过长,那么查询效率就会非常低了。当大量字符串需要高效地放入串池,可以考虑这种方式优化。

方案二:考虑将字符串对象是否入池。

如果需要存放大量字符串,且有大量字符串重复,可以考虑入池,对相同的字符串对象做“去重,减少堆空间占用的内存。

直接内存

定义

        直接内存不属于JVM 管理的内存,属于操作系统的内存。

  •  分配和回收成本高,但是读写性能高。
  • 不受JVM内存回收管理。 

原理

使用byte[]做缓冲区,需要先从磁盘文件,把字节流读取到系统缓存区,然后再读取到JVMbyte[]缓冲区。

使用直接内存,磁盘文件的字节流就可以读取到直接内存,然后 JVM 就可以直接从直接内存读取,少了读取到byte[] 缓冲区这一步骤,所以使用直接内存读写效率会更高。

内存释放

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法。
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleanerclean方法调freeMemory释放直接内存。
演示代码:
private final static int _1GB = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
    Unsafe unsafe = getUnsafe();
    long base = unsafe.allocateMemory(_1GB) ; 
    unsafe.setMemory(base, _1GB, (byte)0);
    System.in.read(); 
    unsafe.freeMemory(base);
    System.in.read();
}

public static Unsafe getUnsafe() {
    try {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        unsafe = (Unsafe)f.get(null);
        return unsafe;
    } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}
禁止显式调用:
  • 所谓的显式调用,是指在 java 代码里使用System.gc()来回收直接内存。
  • 这种方式一般不推荐使用,因为使用这种方式触发的是一次 FullGC,会影响整个系统的性能。
  • 为了禁止程序员使用这种方式回收直接内存,可以使用 -XX:+DisableExcplicitGC 来使 System.gc() 这种方式无效化。
  • 所以涉及到直接内存回收,最好的方式还是使用Unsafe的方式来回收。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值