1.JVM学习结构导图

image.png

2.内存结构

2.1 程序计数器

  1. 线程私有
  2. 不会有内存溢出的情况
  3. 线程切换时会保存当前线程执行的地址

    2.2 虚拟机栈

  4. 每个线程需要运行的内存空间,即虚拟机栈

  5. 虚拟机栈中存入的是栈帧,可以理解为运行的方法,一个虚拟机栈中可以有多个栈帧
  6. 每个线程(每个时刻)只能有一个活动栈帧 ,对应着当前正在执行的方法,方法执行时入栈,执行完毕时弹出栈
  7. 垃圾回收不涉及虚拟机栈,因为虚拟机栈是由一个个栈帧组成的,方法执行完毕栈帧出栈,不需要通过垃圾回收去回收内存
  8. 栈内存不是分配越大越好,设置的命令为 -Xss size ;物理内存是一定的,栈内存越大,即每个线程需要占用的内存空间就会越大,导致可分配的线程数就会越少。

    1. linux 1024KB
    2. macOS 1024KB
    3. Solaris 1024KB
    4. Windows 取决于虚拟内存大小
  9. 方法内的局部变量不会存在线程安全问题,若该变量属于静态变量,则会出现线程安全问题

  10. 栈内存溢出 Java.lang.stackOverflowError
    1. 无限递归(栈帧过多)
    2. 每个栈帧所占用的内存过大直接超过虚拟机栈的最大内存
  11. 线程运行诊断:

    1. top 命令,查看哪个进程占用过高的CPU
    2. ps H -eo pid,tid,%CPU | grep 进程id ,通过ps命令查看过高的进程id对应哪个线程占用过高的CPU
    3. jstack 进程id ,将ps中的线程id转为16进制后,然后在jstack命令下查看到对应的哪个线程有问题,进一步定位源码

      2.3 本地方法栈

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

  13. Object类中的clone wait notify hashCode 等 Unsafe类都是native方法

2.4 堆

2.4.1 定义

  1. 通过new关键字创建的对象都会被放在堆内存,jvm运行时数据区,占用内存最大的区域
  2. 被所有的线程共享
  3. 堆可以分为新生代和老年代,新生代由 To 、From 、Eden 组成
  4. 设置的命令 -Xmx -Xms :JVM初始分配的堆内存大小是由-Xms设置的,默认是物理内存的1/64

    2.4.2 堆内存检测工具

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

  5. jps工具 :查看当前系统中有哪些Java进程

  6. jmap :查看堆内存的占用情况
  7. jconsole :图形界面工具,可以连续检测
  8. jvisualvm

    2.5 方法区

    2.5.1 定义

  9. 方法区在JVM启动时创建,实际的物理内存空间可以不连续,关闭JVM就会释放这个区域

  10. 方法区存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
  11. jdk1.6 版本是由PermGen永久代实现的,且由JVM管理,【java.lang.OutOfMemoryError:PermGen space】而1.8版本之后是由本地内存进行管理,脱离了JVM,由元空间实现(元空间不再使用堆内存,使用的是操作系统内存)【java.lang.OutOfMemoryError:Metaspace】
  12. 内存中存放类信息、静态变量、常量等数据,属于各个线程共享的区域

    2.5.2 方法区的演进

  • Jdk 1.6 及之前:有永久代(静态变量存放在永久代上)、字符串常量池(1.6在方法区)
  • Jdk 1.7 :有永久代,但已经逐步 “ 去永久代 “,字符串常量池、静态变量移除,保存在堆中
  • dk 1.8 及之后: 无永久代,常量池1.8在元空间。但静态变量、字符串常量池仍在堆中
  • 为什么元空间要取代永久代:
    • 设置空间大小很难确定
      • 永久代参数设置过小,在某些场景下,如果动态加载的类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误
      • 永久代参数设置过大,导致空间浪费
      • 默认情况下,元空间的大小受本地内存限制
    • 永久代进行调优很困难:(方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,而不再使用的类或类的加载器回收比较复杂,full gc 的时间长

2.5.3 方法区的设置

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

2.5.4 常量池、运行时常量池、StringTable(串池)

2.5.4.1 常量池

  1. 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息
  2. 对于class 文件用javap -v *.class 后得到的信息中,Constant pool即常量池

    2.5.4.2 运行时常量池

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

  4. 方法区内常量池之中主要存放的两大类常量:字面量和符号引用,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

2.5.4.3 StringTable(串池)

  1. 常量池是.class文件,存放堆中数据的引用地址,而不是真实的对象,运行时常量池是jvm运行时将常量池中数据放入池中,此时引用地址真正的指向对象而不是.class文件;Stringtable是哈希表(不能扩容),它也叫做串池,用来存储字符串,这3个不是同一个东西,我们需要进行区分
  2. StringTable中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中
  3. jdk1.6中,StringTable是放在永久代(方法区)中,jvm进行FullGC才会对常量池进行垃圾回收,影响效率,因此在jdk1.8中将StringTable放在堆中,jvm内存紧张时就会对StringTable进行垃圾回收
  4. StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间: -XX:StringTableSize=桶个数(最少设置为1009以上)

    2.5.4.4 其他

  5. StringTable为什么要调整?

    1. jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才能触发。而full gc是老年代的空间不足、永久代不足才会触发。
    2. 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存。
  6. 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  7. 字符串变量拼接的原理是StringBuilder
  8. 字符串常量拼接的原理是编译器优化
  9. 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
  10. 无论是串池还是堆里面的字符串,都是对象
  11. jdk1.8 调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中,如果串池中没有该字符串对象,则放入成功.如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象,StringTable是放在堆中的
  12. jdk1.6 调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中,如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中,如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象,此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象,StringTable是属于常量池的一部分

    2.6 直接内存

    文件读写的流程:先将文件从磁盘文件读到系统内存的缓冲区,然后再从系统内存缓冲区拷贝到Java缓冲区,该区域才是JVM可以回收的。
    image.png

直接内存,是在系统内存和java内存直接有个类似通道的区域,都是可以进行访问的地方,不需要从系统内存复制到java堆内存,提高了效率
image.png
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放
ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存