堆空间
《java虚拟机规范》对java堆的描述是 几乎所有的对象实例及数组都应当在运行时分配在堆上,同时堆也是垃圾回收的重点区域。
堆空间细分为:
- jdk1.7分为三个部分 新生代+老年代+永久代
- jdk1.8 也分为三个部分 新生代+老年代+元空间(不属于堆空间)
存储在JVM中的java对象可以分为两类
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
- 一类是生命周期较长的对象。
jvm堆的新生代进一步细分为Eden区、Survivor0区和Survivor1区(也称from去和to区)
各个分区默认所占比例为 老年代和新生代为2:1,新生代分为一个Eden和两个Survivor区域,比例为8:1:1 如下图所示。
几乎所有的对象都是在Eden区被创建,同时绝大部分对象的销毁也是在新生代。
对象的分配过程
- new对象逃逸分析若没有逃逸出方法则使用标量替换放在栈上否则放在Eden区
- 当Eden区空间放满时,JVM的垃圾收集器将对Eden区进行垃圾回收(Minor GC),将Eden区中不再被其他对象所有的对象进行销毁。
- 然后将Eden区中存活的对象放在Survivor0区。
- 当再次触发垃圾回收时,销毁 Eden+Survivor0 不再被其他对象所有的对象 并将存活下来的对象放在Survivor1区中。
- 如果再次触发垃圾收集 销毁 Eden+Survivor1 不再被其他对象所有的对象 并将存活下来的对象放在Survivor0区中。
- 经过多次(默认15次)垃圾收集后依然存活的对象 就放入老年代。
TLAB
为什么有TLAB?堆区是线程共享的区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,为避免多个线程操作同一地址需要加锁进而影响分配效率,故使用TLAB。TLAB就是对Eden区进一步划分,JVM为每一个线程分配一个私有的缓冲区 它包含在Eden区内,对线程分配时,使用TLAB可以避免线程安全问题 同时还能提升分配效率。但不是所有对象实例都能在TLAB中成功分配,但JVM确实是将TLAB作为内存分配的首选。默认情况下TLAB的内存空间非常小仅占有整个Eden区空间的1%。一旦对象在TLAB空间放分配失败,JVM就会在Eden区使用加锁机制分配内存以确保数据操作的原子性。
堆是对象存储的唯一选择么
随着JIT编译器的发展与逃逸分析的逐渐成熟,栈上分配、标量替换优化技术 使得所有的对象都分配在堆上也变得不那么绝对了。有一种特殊情况,如果经过逃逸分析发现一个对象并没有逃逸出方法,那么就有可能被优化在栈上分配,也就无需进行垃圾回收。
逃逸分析就是分析对象的作用域:
1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
其实逃逸分析技术的到如今也并不是十分成熟,虽然逃逸分析可以做标量替换、栈上分配和锁消除,但是逃逸分析自身也是需要进行一系列复杂的分析也是一个相对耗时的过程。(比如经过逃逸分析后发现没有一个对象是不逃逸的那这个逃逸分析过程就浪费掉了)虽然这项技术并不是十分成熟,但是它也是及时编译器优化技术中一个十分重要的手段。
方法区
堆、栈、方法区的关系
- 方法区和堆空间一样是各个线程共享的区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和java堆一样可以是不连续的。
- 方法区的大小决定了系统可以把创建多少个class对象,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace。
jdk1.7及以前,习惯把方法去称为永久代,jdk1.8开始使用元空间取代了永久代。元空间的本质上和永久代类似都是的对JVM规范中方法区的实现,元空间和永久代最大的区别是元空间不在虚拟机设置的内存中而是使用的本地内存,内部结构也做了相应调整。
方法区中存储的是什么
存在已被虚拟机加载的类信息、运行是常量池、常量、静态变量、即使编译器编译后的代码缓存等。
方法区中包含了运行时常量池 而 字节码中包含了常量池。
一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口等信息外,还包含了一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
常量池的作用:字节码文件需要数据支持,通常这中数据会很大以至于不能直接存在字节码里,而存在常量池中 这个字节码包含了指向常量池的引用。在动态链接时会用到运行时常量池。
常量池中有什么?
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
运行时常量池
- 运行时常量池是方法区的一部分。
- 常量池表是Class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后存放在方法去的运行时常量池中。
- 运行是常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
- JVM为每个加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样是通过引用访问的。
- 运行是常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量中的符号地址了,这里换为真实地址。
- 运行时常量池类似传统编程语言中的符号表。
以下是运行时常量池和常量池的关系图