QQ图片20200224205408.png

1.程序计数器

全称:Program Counter Register(物理上是寄存器实现的,读取速度非常快)

作用

QQ图片20200224205642.png
要了解程序计数器,可以看上图的代码。

右侧就是普通的java源代码,左侧的就是编译后的JVM指令(二进制字节码)。可以看到JVM每条指令左边都有一个数字,你可以理解为每条JVM指令对应的内存地址,根据内存地址就可以找到对应的指令去执行它。

执行每条JVM指令的同时,它也会把下一条JVM指令的内存地址存入程序计数器。当指令执行完后,解释器就会到程序计数器找到下一条指令的内存地址,去执行它。
综上,作用就是记住下一条指令的执行地址

特点

  • 是线程私有的

每一个线程都有自己的程序计数器

  • 不会存在内存溢出

**

2.虚拟机栈

全称:JVM Stacks

定义

QQ图片20200225105220.png

从图中可以看出:
虚拟机栈就是线程运行时需要的内存空间。如果有多个线程的话,就会有多个虚拟机栈。

一个栈内又由多个栈帧(frame)组成。这里的栈帧是什么意思呢,其实,一个栈帧就对应着一次方法的调用。也就是说,栈帧就是每个方法运行时需要的内存

每个线程只能有一个活动栈帧。它对应着当前正在执行的那个方法。

思考

1.垃圾回收是否涉及栈内存呢?
答:不涉及,因为虚拟机栈就是一次次的方法调用组成的,当chuchan方法执行结束后,栈帧内存 会随着出栈自动释放掉。

2.栈内存是否分配的越大越好?
答:不是的。栈内存分配的越大,反而会让线程的执行数变少。因为物理内存的大小是一定的。

3.方法内的局部变量是否是线程安全的?
答:先看下面的代码:

  1. public class Demo01 {
  2. static void m1() {
  3. int x =0;
  4. for(int i = 0; i < 5000; i++){
  5. x++;
  6. }
  7. System.out.println(x);
  8. }
  9. }

有一个局部变量x,方法m1对x变量自增5000次。
如果这时有多个线程执行此方法。会产生线程安全问题吗?

答案是不会。
QQ图片20200225114230.png
从图中可以看出,当两个线程都执行此方法时,分别会创建两个局部变量的栈帧入栈。相当于每个线程都有自己私有的变量x,互不干扰。(局部变量是线程私有的

再来深入分析一下,看下面这段代码:

  1. public class Demo02{
  2. public static void main(String[] args) {
  3. }
  4. public static void m1() {
  5. StringBuilder sb = new StringBuilder();
  6. sb.append(1);
  7. sb.append(2);
  8. sb.append(3);
  9. System.out.println(sb.toString());
  10. }
  11. public static void m2(StringBuilder sb) {
  12. sb.append(1);
  13. sb.append(2);
  14. sb.append(3);
  15. System.out.println(sb.toString());
  16. }
  17. public static void m3() {
  18. StringBuilder sb = new StringBuilder();
  19. sb.append(1);
  20. sb.append(2);
  21. sb.append(3);
  22. return sb;
  23. }
  24. }

在m1方法里,sb是方法里的局部变量,是线程私有的,所以是线程安全的。

在m2方法里,sb作为了一个参数传递进来,这就意味着有其他的线程能访问到它,就不是线程私有的了,是共享的了,就会存在线程安全问题。可以用StringBuffer解决这个问题。

在m3方法里,sb对象作为一个返回值返回了,这意味着其他线程也能获取到这个对象并并发地修改它。所以也存在线程安全问题。

综上所述,要判断一个局部变量是否是线程安全的,就要看它有没有逃离该方法的作用范围

栈内存溢出

java.lang.StackOverFlowError
什么情况会导致内存溢出呢?

  • 栈帧过多(比如递归调用,如果没有设置正确的结束条件,就会导致内存溢出)
  • 栈帧过大(这种情况极少出现)

    3.本地方法栈

    全称:Native Method Stacks

本地方法就是值那些不是由Java编写的代码。有时候需要借助C语言等和操作系统的底层打交道。

4.堆

全称:Heap

定义

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

特点

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

    堆内存溢出

    java.lang.OutOfMemeoryError 就是指的堆内存溢出。(heap space)

参数:-XMX(最大堆空间内存)

堆内存诊断

1.jps工具

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

2.jmap工具

  • 查看堆内存占用情况(命令行语句:jmap -heap 进程id)

3.jconsole工具

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

4.jvisualvm工具

  • 可视化的虚拟机


5.方法区

全称:Method Area

定义

方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。比如成员变量、方法数据、成员方法以及构造器方法的代码部分。

综上,方法区存储的就是跟类相关的一些信息。

方法区是在java虚拟机启动时创建的。尽管逻辑上方法区是堆的一部分,但在实现上不同的虚拟机有各自的实现方式。

在jdk1.8以前,方法区的实现方式是永久代,在1.8以后,就把永久代移除了,换了个实现方式叫元空间。元空间用的就是本地内存,也就是操作系统的内存。

具体结构看下图:
QQ图片20200226114706.png
这里的方法区只是一个概念,实现的方式是永久代。
QQ图片20200226114711.png
1.8后,方法区的实现方式变成了元空间,元空间是由本地内存管理了,不在堆内存了。StringTable移到了堆内存中了。

方法区内存溢出

方法区如果内存不足了,也会抛出一个OutOfMemory的异常。

1.8以前会导致永久代内存溢出,1.8以后会导致元空间内存溢出

  1. /*
  2. * 演示元空间内存溢出
  3. * -XX:MaxMetaSpaceSize=8m
  4. */
  5. public class Demo03 extends ClassLoader{ //可以用来加载类的二进制字节码
  6. public static void main(String[] args) {
  7. int j = 0;
  8. try {
  9. Demo03 test = new Demo03();
  10. for(int i = 0; i < 10000; i++, j++){
  11. //ClassWriter 作用是生成类的二进制字节码
  12. ClassWriter cw = new ClassWriter(0);
  13. //版本号, 修饰符public, 类名, 包名, 父类, 接口
  14. cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
  15. //返回byte[]
  16. byte[] code = cw.toByteArray();
  17. //执行了类的加载
  18. test.defineClass("Class" + i, code, 0, code.length);
  19. }
  20. } finally{
  21. System.out.println(j);
  22. }
  23. }
  24. }

看上图的代码,如果我们把虚拟机参数云空间最大容量改为8M。就会抛出内存溢出异常。
QQ图片20200320085423.png
1.8以前,会显示永久代内存溢出:java.lang.OutOfMemory: Permgen space

运行时常量池

先来聊聊什么叫常量池?
常量池其实就是一张表,虚拟机指令会根据这张表来找到要执行的类名、方法名、参数类型、字面量(比如字符串之类的)等信息。

我们可以拿经典代码hello world为例子来学习常量池。如果我们要运行这个程序,肯定首先要把它编译成二进制字节码。
二进制字节码由以下几部分组成:

  • 类的基本信息
  • 常量池
  • 类中的方法定义(类的方法中就包含了虚拟机指令)

首先我们要对二进制字节码文件进行反编译:javap -v HelloWorld.class
QQ图片20200320094134.png
这几行就是方法中的虚拟机指令。比如第一行获取静态变量,那么我们要怎么获取呢?这时候我们就要根据后边的#7去常量池查表。
QQ图片20200320095049.png
查到#7是fieldref——引用了成员变量。指向#8和#9,那么就继续找。#8是类名,我们要找哪个类的成员变量呢?继续找#10。接下来都是如此。。。

运行时常量池是方法区的一部分,是java虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中。

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

StringTable

中文名就是我们俗称的串池(字符串常量池),是运行时常量池的重要组成部分。它存在于堆中(JDK1.7之后)。
StringTable存在着hash表的特性,不存在两个相同的字符串,也不能扩容。

先看下面这个例子:

  1. public class Demo04 {
  2. public static void main(String[] args) {
  3. String s1 = "a";
  4. String s2 = "b";
  5. String s3 = "ab";
  6. }
  7. }

常量池中的信息,都会被加载到运行时常量池中。这时a、b和ab都是常量池中的符号,还没有变为java字符串对象。
创建java字符串对象的时候,就会把该对象放入StringTable中。

看下面这道经典的面试题,搞懂了这道面试题,也就搞懂了StringTable。

  1. public static void main(String[] args) {
  2. String str1 = "abc";
  3. String str2 = "ab" + "c";
  4. String str3 = new String("ab") + "c";
  5. String str4 = str3.intern();
  6. System.out.println(str1 == str2);
  7. System.out.println(str1 == str3);
  8. System.out.println(str1 == str4);
  9. }

分别分析3个输出结果:

  1. 第一个输出为true。原因是JVM存在编译器优化的机制。在编译期(javac .java时)会将可以拼接的*字符串常量帮你自动拼接了,此时由于StringTable中已经存在了”abc”这个字符串对象,因此会让str2指向一个与str1相同的那块地址,因此为true。
  2. 第二个输出为false。由于使用了new String( ),运行过程中会在堆中重新开辟一个空间存储,与之前的常量字符串没啥关系,因此为false。
  3. 第三个输出为true,原因调用intern( )方法时会把在StringTable中查找是否存在与其值相等的(并不是地址相等)的字符串,发现里面恰好存在,因此返回该存在的引用,StringTable中的就是str1,因此为true。

    6.直接内存

    全称:Direct Memory

    定义

    直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域
  • 常见于NIO操作时,用作数据缓冲区。
  • 分配回收成本较高,但是读写性能较好。
  • 不受JVM内存回收管理。


在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。

基本使用

QQ图片20200301110420.png
在文件读写过程中,java首先要调用操作系统内部的函数,它自己本身是不具备磁盘读写能力。首先辉仔啊系统内存中划出一块系统缓存区,但是在系统缓存区java代码是不能运行的,所以还要在堆里再划出一块java缓冲区。简洁读写磁盘文件。但是这里有两块缓冲区,从而会造成磁盘读写的效率不是很高。
QQ图片20200301110755.png
在这张图里,也会在系统内存划出一块缓冲区,也就是我们讲的直接内存。但与上图不同的是,java代码也可以直接访问这块内存。所以直接内存是系统和java可以共享的一块区域。

内存溢出

直接内存也会存在内存溢出。异常是 java.lang.OutOfMemory : Direct buffer memory