JVM 手把手保姆级教程(1/3):内存结构

想拿高工资,想成为一名合格又优秀的java高级攻城狮,对于JVM的学习是必不可少的。
我本人找过很多课程,学过很多遍,却总是感觉学不太明白,感觉少点什么,我相信很多小伙伴会和我有一样的经历。
还好现在找到一个比较易于理解却不臃肿的视频教程,本笔记就是基于视频教程以及对视频中不易理解的部分进行多方咨询求证,力求写出一篇易于理解,帮助小伙伴们成长的教程,在这里非常感谢黑马的课程。视频:黑马程序员JVM完整教程

第一篇:JVM 手把手保姆级教程(1/3):内存结构
第二篇:JVM 手把手保姆级教程(2/3):垃圾回收
第三篇:JVM 手把手保姆级教程(3/3):类加载与字节码技术&内存模型

1. 程序计数器

1.1 定义

Program Counter Register 程序计数器(寄存器)
java程序运行的逻辑大概是这样的,JVM虚拟就将java源码转化为JVM指令,然后解释器将JVM指令转化为机器码,机器码则可以直接运行在CPU上。程序计数器,在这里的作用就是记住下一条指令的地址,解释器会从这里取出地址并执行相应的操作。

百度百科的解释如下:
在这里插入图片描述

强行解释
一个人要去做饭,那么具体怎么做需要拆解成很多详细步骤,比如买菜、洗菜、生火等等。
我们之所以会按照步骤一步步去完成,是因为我们的大脑早就提前将指令存放到了我们大脑的某一处,而存放指令的这一处就类似我们处理器中的寄存器,也就是我们所说的程序计数器,只不过程序计数器存的不是指令本身,而是指令所在的地址。
当一条指令被取出执行时,程序计数器就会存放下一条指令的地址,以此往复,来完成程序的连续运行。

程序计数器的作用就是存放下一条指令的地址。

1.2 特点

知道了程序计数器的作用是存放下一条指令的地址,那么它有什么特点呢。多线程直接会不会乱取指令地址呢,答案是不会的因为程序计数器是线程私有的。程序计数器,保存的是当前执行的字节码的偏移地址,当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址。因此,永远都不可能内存溢出的

特点可以总结为两点:

  • 程序计数器是线程私有的
  • 不会存在内存溢

2. 虚拟机栈

2.1 定义

Java Virtual Machine Stacks(Java虚拟机栈)

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

动态演示

总共三个方法,main 方法、method1方法、method2方法,其中main调用method1方法,method1又调用method2方法。所以总共又三个栈帧,最上面的那个就是正在运行的,也叫做活动栈帧。当method2调用完毕就会被释放,左下角Frames中也会消失,当全部方法被调用完毕后,左下角Frames中就没有栈帧了。
在这里插入图片描述

常见问题

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

  • 不涉及,栈内存时方法调用的时候产生的栈帧内存,而栈帧内存会在方法调用结束后自动回收。

2)栈内存分配的越大越好吗?

  • 栈内存可以通过 -Xss指定大小,但并不是越大越好。栈内存大可以更深层次的调用方法或者更多次的递归调用,但是由于物理内存大小是固定的,所以会导致线程数变少。

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

  • 如果局部变量没有逃离方法的作用范围,那就是线程安全的
  • 如果局部变量逃离了方法的作用范围,可以被其他线程访问到,就不是线程安全的。如下图:
    在这里插入图片描述
    上图只有m1方法是线程安全的,因为别的线程无法访问到sb变量;m2方法作为参数使用,再调用该方法前主线程或其他线程是可以对该参数进行修改的,所以线程不安全;m3作为返回值,也是可以被其他线程访问到进行修改的,也不是线程安全的。

2.2 栈内存溢出

java中栈内存异出的异常为: java.lang.stackOverflowError

什么情况会导致栈内存溢出?
栈帧过多或这栈帧过大都会导致栈内存溢出。

2.3 线程运行诊断

案例1:cpu占用过多
定位问题:

  • top:命令定位哪个进程对cpu的占用过高。
  • ps H -eo pid,tid,%cpu | grep 进程id:进一步定位哪个线程占用cpu过高,然后将有问题的线程id转换成16进制,用于下一步的对比。
  • jstack 进程id:可以根据进程id查看该进程下的线程信息,然后根据上一步的16进制线程id找到有问题的线程。

案例2:程序运行很长时间没有结果

  • 通过jstack工具,jstack 进程id,根据返回的信息可以查看是否有死锁情况以及出错代码的行号。

3. 本地方法栈

一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
在这里插入图片描述

4.堆

4.1 定义

通过new关键字创建的对象,都会使用堆内存。可以使用-Xml指定堆内存大小。

特点

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

4.2 堆内存溢出

java.lang.OutOfMemoryError:Java heap space(冒号前面代表内存溢出,后面代表堆空间不足导致的)。可以使用 -Xmx 来指定堆内存大小。

tip:调试代码时尽量把堆内存调小一点,这样可以尽早的暴露可能引起堆内存溢出的问题。

4.3 堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 java 进程,会显示java程序的进程Id
  2. jmap 工具
    查看堆内存占用情况:jmap - heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具
    可视化虚拟机,可以使用堆dump功能,来抓取当前堆的快照。然后再快照里可以查找占用内存比较大的对象。

5. 方法区

5.1 定义

《Java 虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或进行压缩。”
但对于 HotSpot JVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于 Java 堆的内存空间。

在 jdk7 及以前,习惯上把方法区称为永久代。jdk8 开始,使用元空间取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。区别是:元空间不在虚拟机设置的内存中,而是使用本地内存。

5.2 组成

在这里插入图片描述
jdk1.8以前,一般将方法区成为永久代,jdk1.8以后使用元空间替代方法区。不止是名字变了,其内存结构也发生了很大的变化,永久代是在虚拟机设置的内存空间中,而元空间依赖于系统的本地内存。

5.3 方法区内存溢出

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

5.4 运行时常量池

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

5.5 StringTable

5.5.1 定义

  • StringTable数据结构是哈希表,不能扩容,不会存在重复的值。
  • 常量池中的字符串仅是符号,只有在被用到时才会转化为字符串对象,并放入串池中。
  • 利用串池的机制,来避免重复创建字符串对象。
  • 字符串变量拼接的原理是new StringBuilder()对象,调用其append()方法,然后再调用toString()。结果存在堆中
  • 字符串常量拼接的原理是编译期间的优化。因为常量的值是确定的,所以在编译期间就能确定结果(比如"a"+“b”,会直接去串池中寻找"ab",有就用,没有就放进去)。结果存在串池中
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中。
    • JDK1.8 尝试将这个字符串对象放入串池,如果有则不会放入,如果没有则会放入串池。然后返回串池中的对象
    • JDK1.6 尝试将这个字符串对象放入串池,如果有则不会放入,如果没有会把这个对象复制一份放入串池,然后返回串池中的对象。

5.5.2 StringTable 的位置

JDK1.6 存放在常量池中,而常量池在永久代,所以JDK1.6 StringTable存放在永久代,而从JDK1.7开始就移到了堆空间中。

为什么要放到堆中?
因为永久代的回收频率比较低,只在FullGC的时候才会被回收,FullGC只会在老年代或者永久代空间不足时才会触发。如果有大量的字符串被创建,放在永久代,由于永久代的回收频率低,会导致永久代空间不足。如果放到堆里,能够及时回收内存。
在这里插入图片描述
看代码,猜结果1
在这里插入图片描述
解析:

  • s2==“ab”:是因为String s2 = s.intern()这行代码返回的结果就是串池中的对象,无论能不能将s放到串池,返回的都是串池中的结果,所以返回true。
  • s==“ab”:是因为String s2 = s.intern()这行代码已经将s放入到串池中了,而放入之前是没有的所以放入成功了,所以s这时也是串池中的对象,结果为true。

看代码,猜结果2
在这里插入图片描述
解析:

  • s2==x:是因为String s2 = s.intern()这行代码返回的结果就是串池中的对象,无论能不能将s放到串池,返回的都是串池中的结果,所以返回true。
  • s==x:因为串池中已经提前有了"ab",所以String s2 = s.intern()这行代码未能将s放到串池中去,这时s存放在堆中,所以返回false。

看代码,猜结果3
在这里插入图片描述
要明白上面代码的逻辑,学会自己去分析。

5.5.3 StringTable 垃圾回收

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

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Code_05_StringTableTest {
    
    

    public static void main(String[] args) {
    
    
        int i = 0;
        try {
    
    
            for(int j = 0; j < 10000; j++) {
    
     // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            System.out.println(i);
        }
    }

}

在这里插入图片描述

5.5.4 StringTable性能调优

因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少哈希冲突。如果系统里字符串常量特别多的话并且可能有大量重复的话,可以调大桶的个数,然后让字符串入池,来减少堆内存的使用。

-XX:StringTableSize=桶个数(最少设置为 1009 以上)

猜你喜欢

转载自blog.csdn.net/hpp3501/article/details/120881271