java内存模型
- 程序计数器
在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行本地方法,则是未指定值(undefined) - Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个一个的栈帧,对应着一次次的java方法调用。栈帧中存储着局部变量表、操作数栈、动态链接、方法正常退出或者异常退出的定义等。
3.堆,它是Java内存管理的核心区域,用来放置Java对象实例,几乎所有创建的Java对象实例都是被直接分配在堆上。堆被所有的线程共享,理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。 - 方法区,这也是所有线程共享的一块内存区域,用于存储所谓的元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
由于早期的HotSpot JVM实现,很多人习惯将方法区成为永久代(Permanent Generation)。Oracle JDK 8中将永久代移除,同事增加了元数据区(Metaspace) - 运行时常量池,这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java常量池可以存放各种常量信息,不管是编译器生成的各种字面量,还是需要在运行时决定的引用符号,所以它比一般语言的符号表存储的信息更加宽泛。
- 本地方法栈,它和java 虚拟站是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在Oracle HotSpot JVM中,本地方法栈和Java虚拟机栈是在同一块区域,这完全取决于技术实现的决定,并未在规范中强制。
堆内存的年代视角
按照通常的GC年代方式划分,Java堆内存分为:
1.新生代
新生代是大部分对象创建和销毁的区域,在通常的Java应用中,绝大部分对象生命周期都是很短暂的。其内部又分为Eden区域,作为对象初始分配的区域;两个Survivor,有时候也叫from、to区域,被用来放置从Minor GC中保留下来的对象。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,HotSport JVM还有一个概念叫做Thread Local Allocation Buffer(TLAB)。这是JVM为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度,可以参考下面的示意图。从图中可以看出,TLAB仍然在堆上,它是分配在Eden区域内的。其内部结构比较直观易懂,start、end就是起始地址,top(指针)则表示已经分配到哪里了,所以我们分配新对象,JVM就会移动top,当top和end相遇时,即表示该缓存已满,JVM会视图再从Eden分配一块儿。
2.老年代
放置长生命周期的对象,通常都是从Survivor区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配在老年代。
3.永久代
这部分就是早期HotSpot JVM的方法区实现方式了,储存Java类元数据、常量池、Intern字符串缓存,在JDK 8之后就不存在永久代这块儿了。
接下来我会依赖 NMT 特性对 JVM 进行分析,它所提供的详细分类信息,非常有助于理解 JVM 内部实现。
首先来做些准备工作,开启 NMT 并选择 summary 模式
-XX:NativeMemoryTracking=summary
为了方便获取和对比 NMT 输出,选择在应用退出时打印 NMT 统计信息
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
配置截图如下
接下来执行一个简单的HelloWorld程序
public class Test {
public static void main(String[] args) {
System.out.println();
}
}
输出如下,接下来对输出进行详细的介绍
- 第一部分是 Java 堆
- 第二部分是Class内存占用,它所统计的就是Java类元数据所占用的空间
- 第三部分是Thread,这里既包括Java线程,如程序主线程、Cleaner线程等,也包括GC等本地线程。
- 第五部分是GC部分,我们可以使用以下命令替换默认GC方式为SerialGC
-XX:+UseSerialGC
从下图的结果可以看出,线程数降低为11,而且GC设施本身的内存开销就少了非常多。
- 第六部分是Compiler部分,就是JIT的开销,可以通过以下命令关闭TieredCompilation从而降低内存使用
-XX:-TieredCompilation