JVM内存结构概览

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

程序计数器

作用:

  • 用于记录本线程的下一条 jvm 指令的执行地址行号;
  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样解释器会知道下次该从哪里拿到指令并进行解释执行;
  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录当前线程下一行指令的地址行号,以便于之后切换回来之后接着往下执行。

特点:

  • 是线程私有的;
  • 不会存在内存溢出。

虚拟机栈

定义:

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

常见问题:

  • 垃圾回收是否涉及栈内存?
    不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。
  • 栈内存分配越大越好吗?
    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  • 方法呢的局部变量是否线程安全?
    如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出( java.lang.stackOverflowError ),使用-Xss 指定栈内存大小!

例如:

1
2
-Xss256k
-Xss1m

栈内存诊断

  • top命令,查看是哪个进程占用 CPU 过高;
  • ps H -eo pid, tid, %cpu | grep pid 命令,查看是哪个线程占用 CPU 过高;
  • jstack tid 查看该线程的栈的使用情况。

本地方法栈

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

堆(Heap)

  • 通过new关键字创建的对象都会被放在堆内存

特点

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

堆内存溢出

堆内存溢出时会报错:java.lang.OutofMemoryError :java heap space.

可以使用 -Xmx 来指定堆内存大小。

例如:

1
-Xmx8m

堆内存诊断

  • jps 工具
    查看当前系统中有哪些 java 进程
  • jmap 工具
    查看堆内存占用情况 jmap - heap pid
  • jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测
  • jvisualvm 工具

方法区

定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

方法区内存溢出

  • jdk1.8 之前会导致永久代内存溢出
    使用 -XX:MaxPermSize=8m 指定永久代内存大小,这里指定为8m;
  • jdk1.8 之后会导致元空间内存溢出
    使用 -XX:MaxMetaspaceSize=8m 指定元空间大小,这里指定为8m。

运行时常量池

二进制字节码包含类的基本信息,常量池,类方法定义。

其中常量池是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息。

当某个类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

StringTable

作用

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象;
  • 利用串池的机制,来避免重复创建字符串对象。

拓展:字符串变量拼接的原理是StringBuilder字符串常量拼接的原理是编译器优化。

主动入池

可以使用intern方法,主动将串池中还没有的字符串对象放入串池中。

在jdk1.8中,调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中,如果串池中没有该字符串对象,则放入成功,如果有该字符串对象,则放入失败。无论放入是否成功,都会返回串池中的字符串对象。

而在jdk1.6中,只是尝试将字符串对象的拷贝放入。

存放位置

jdk1.6 StringTable 放在永久代中,jdk1.8 StringTable放在堆中。

垃圾回收

  • -Xmx10m 指定堆内存大小;
  • -XX:+PrintStringTableStatistics 打印字符串常量池信息;
  • -XX:+PrintGCDetails打印垃圾回收详情。

性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间。
    1
    -XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池
  • 可以通过 intern 方法减少重复入池