JVM运行时数据区域之最终章
JVM运行时数据区域之java虚拟机栈
堆
什么是堆
堆是java运行时数据区域中最大的一块内存,与栈不同,堆是被所有线程所共享的数据区域;在java中几乎所有的对象都在堆上面分配内存,即大多数对象都存放在堆中,注意这里说的“大多数”,下面会提及为何是大多数。
GC堆?
堆也被称之为是GC堆,因为堆是java垃圾回收器管理的内存区域,因为有垃圾回收器所以java程序员才不用向C和C++程序员那样管理释放内存。也正是因为垃圾回收,所以堆内部又也被划分为不同的区域,例如新生代,老年代,eden,survivor区域,根据对象的分代年龄,对象大小将其放置在不同的区域。
下面先简单看一下堆的内存区域划分,在讲垃圾回收的时候再详细说明
在GMS收集器下堆的内存区域的划分
G1收集器下的堆内存区域
堆的内存区域连续吗?
根据《java虚拟机规范》的规定,java堆可以是物理上面的不连续空间,但是逻辑上面它应该是被视为是连续的。而且java堆既可以是固定大小,也可以是可扩展的。当堆没有内存为对象进行实例分配,而且也无法扩展的时候,JVM会抛出OOM异常
堆中只有对象吗?
看完上面的内容容易让大家产生一种错觉就是堆中只有对象,其实不然,堆中还有字符串常量池和Class类对象,类变量等;在不同的JDK下,堆的内存区域也略有有不同,例如JDK1.6的堆中还有永久代。但是无论如何,堆的最重要的任务就是存放对象实例。
对象都在堆中吗?
在Java发展的早期,对象确实都在堆中分配内存,但是随着java语言的发展以及逃逸分析的日益强大,栈上分配,标量替换等优化手段使得对象不全在堆分配内存。下面就逃逸分析和栈上分配做简单介绍
逃逸分析
逃逸分析是现在JVM比较前沿的优化技术,它的基本原理就是分析对象的动态作用域,当一个对象被创建后,它有可能被外部的方法所引用例如参数的传递,这种称之为是方法逃逸;甚至还有可能被外部的线程所访问,例如全局变量,这种是线程逃逸;如果这个对象只是在这个方法内使用,则称之为从不逃逸
栈上分配
对象在堆上分配内存之后就可以被所有的线程访问,但是试想,如果一个对象它从不发生逃逸,那么将它保存在堆上不仅没有必要,还会消耗堆内存,加重GC的压力,所以这时就可以将它分配到java虚拟机栈上的栈帧中,这样不仅保障了栈帧对应的方法可以使用它,而且在方法执行结束后,对象会随着栈帧的出栈而销毁。
方法区
java的一个类源程序被javac编译成class文件后再被java的类加载器载入内存后,是怎么样存放的?
方法区和堆一样,也是线程共享的数据区域,它就是用来存储被JVM加载的类型(类和接口)信息,常量,静态变量,即时编译器编译后的代码缓存等数据;说简单点就是class文件被加载进内存后,class文件的静态结结构会转换为方法区的动态结构存储在方法区中,而由字节码编译而成的机器码也会存储在方法区。
当然上面的说法在逻辑上就是那样,但是实际上像静态变量,常量在不同的JVM下存储的位置是不同的。我们可以把《java虚拟机规范》定义的方法区理解成是一个接口,对于不同的JVM,对于这个”接口“的实现也不同;下面就以HotSpot虚拟机为例,就不同JDK下的方法区的实现做一下简单的介绍。
JDK1.7及以前的永久代
在JDK8以前,HotSpot JVM以永久代实现方法区,永久代和老年代,新生代一样在堆上实现,最初设计的初衷就是让垃圾收集器可以像管理堆一样管理这部分内存,这样就可以省去专门为方法区编写代码的工作。但是后来这种实现方法区的方式弊端逐渐显示出来
- 永久代在堆中容易造成OOM,如果它的内存设置过大,那么就会使得老年代和新生代的内存不足,导致频繁GC;如果它的内存设置过小,因为在程序运行过程中,加载多少类是不可知的,所以在类很多的情况下,它本身由容易出现OOM
- 永久代的设计加重GC的压力,影响性能;因为不论是老年代满了还是永久代满了都会触发Full GC,造成STW(stop the world)。
- 字符串常量池存放在永久代中,容易造成永久代内存溢出
JDK8的元空间
既然永久代又这么多弊端,那么HotSpot团队肯定得即使止损呀。其实从JDK1.7开始永久代就开始逐渐被移除,例如将字符串常量池从永久代中移出,单独存放在堆中,在JDK8中则完全移除了永久代,改用在元空间中实现方法区,而类变量则随着Class类对象存放在堆中。
元空间
元空间不在jvm运行时数据区域中,它位于本地内存,所以它的大小受制于本地内存的大小,理论上元空间不会出现OOM,它可以无限使用本地内存,但为了不让它如此膨胀,JVM提供了参数来限制它对本地内存的使用。
- -XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
- -XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
- XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。
参考 isysc1 的一文读懂元空间和永久代
运行时常量池
运行时常量池也是方法区的一部分,在class文件中有一项信息就是常量池表,这部分内容在class文件被加载入内存后,存储在运行时常量池中;除此之外,常量池表中的符号引用被翻译成直接引用后也会存储于此,因为每一个类都有常量池表,所以每一个类型被加载入内存后,都有自己对应的运行时常量池
下面看一下class文件中的常量池表
源程序
/**
* @author sheledon
*/
public class Main {
private int a;
private int b;
private ThreadLocal threadLocal;
public void test(){
System.out.println("欢迎来到小白的知识空间");
}
public static void main(String[] args) {
}
}
反编译后常量池表
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #29 // 欢迎来到小白的知识空间
#4 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #32 // test/Main
#6 = Class #33 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 b
#10 = Utf8 threadLocal
#11 = Utf8 Ljava/lang/ThreadLocal;
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Ltest/Main;
#19 = Utf8 test
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 SourceFile
#25 = Utf8 Main.java
#26 = NameAndType #12:#13 // "<init>":()V
#27 = Class #34 // java/lang/System
#28 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#29 = Utf8 欢迎来到小白的知识空间
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#32 = Utf8 test/Main
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
}
字符串常量池
关于字符串常量池在不同JDK存储位置的变化和其常量池中存放内容由对象到引用而引出的String.intern()方法和new String("abc")到底创建了几个对象的讨论也非常有意思,后面我会写一篇文章来说明。
直接内存
直接内存也不是JVM运行时数据区域的一部分,它也位于本地内存,并且还不是《java虚拟机规范》定义的内存,但是这部分内存被频繁使用,也会导致OOM。
在JDK1.4中新加入的NIO类,通过通道和缓存区的IO方式可以使用这部分内存。
最后
我是不想当码农的小白,平时会写写一些技术博客,推荐优秀的程序员博主给大家还有自己遇到的优秀的java学习资源,希望和大家一起进步,共同成长。
以上内容如有错误,还望指出,感谢
公众号点击交流,添加我的微信,一起交流编程呗!
公众号: 小白不想当码农
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。