→ JVM 内存结构
运行时数据区:堆、方法区、运行时常量池、直接内存、
一、堆 (java Heap)
Java 堆(Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但随着 JIT 编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些变化发生,所有的对象并不都是一定分配在堆上。
为了更好的回收内存和分配内存,从内存回收的角度来看,Java 堆还可以细分新生代和老年代,对于新生代和老年代虚拟机一般都采用不同的垃圾回收算法;从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样。在实现时 ,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 –Xmx 和 –Xms 控制)。
如果在堆中没有完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
二、方法区 (Method Area)
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存 区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆)。
方法区与永久代:对于习惯在 HotSpot 虚拟机上开发、部署程序的人来说,一般称方法区为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样 HotSpot 的垃圾收集器可以像管理 Java 堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如 BEA JRockit、IBM J9等)来说是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但是使用永久代来实现方法区,这样更容易遇到内存溢出问题(永久代有 -XX:MaxPermSize 的上限,J9 和 JRockit 只要没有触碰到进程可用内存的上限,例如 32 位系统中的 4GB,就不会出现问题),而且有极少数方法(例如 String.intern())会因为这个原因导致不同虚拟机下有不同的表现。因此,在 JDK 1.8 中,HotSpot 虚拟机放弃永久代,采用元空间来实现方法区了。
异常状况:根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
三、运行时常量池 (RuntimelyConstant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常量池的动态性:运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产出,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被利用的比较多的就是 String 类的 intern() 方法。
异常状况:当常量池中无法再申请到内存时会抛出 OutOfMemoryError 异常。
四、直接内存 (DirectMemory)
直接内存不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO(New Input / Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存分配不会受到 Java 堆大小的限制,但是,会受到本机总内存大小及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 –Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
五、Java虚拟机栈 (java Cirtual Machine Stacks)
java虚拟机栈也是线程私有的,生命周期与线程相同,主要存储基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身。可能是一个对象起始地址的引用指针,也可能指向代表一个对象的句柄或者此对象的相关位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中double、long类型数据会占用2个局部变量空间(slot),其余的数据类型只占有1个。局部变量表所需要的内存空间在编译器完成分配,运行期间不会改变其大小。
目前大多数虚拟机可动态扩展,如果扩展时无法申请到足够的内存即会抛出OutOffMemoryError异常。
六、本地方法栈 (Native Method Stack)
与java虚拟机栈作用是相似的,他们之间的区别是java虚拟机栈用来执行java方法(也就是字节码),而本地方法栈则为虚拟使用到的native(用来修饰可供其他语言调用的方法,如操作操作系统底层服务的方法)服务,部分虚拟机(譬如 sun HotSpot)直接将虚拟栈和本地方法栈合二为一,与java虚拟机栈相同,也会抛出OutOffMemoryError异常。
七、程序计数器 (Program Counter Register)
他记录了程序执行的字节码的行号和指令,字节码解释器工作时就是通过改变计数器值来选择下一个要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等。
由于java虚拟机多线程是通过线程轮流切换CPU时间片的方式来实现的,在任何确定的时刻,一个处理器(对于多核处理来说是一个内核)都置灰执行一条线程中的指令。因此,为了线程每次切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这块内存也叫“线程私有内存”,其不会出现OutOfMemoryError异常。
【原文链接】堆、方法区、运行时常量池和直接内存 - 辰凩風
JVM内存模型程序计数器、虚拟机栈、本地方法栈、堆、方法区、运行时常量池、直接内存_wangaz521的博客
JVM 运行时数据区——方法区、堆、栈_休息的风的专栏
堆和栈区别
栈内存
栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
堆内存
存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
堆与栈的区别
1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
4.栈空间比较小,堆空间比较大
5.栈空间的内存是系统自动分配的,不需要程序员进行管理,堆空间是动态分配的,一般存放对象,并需要手动释放内存
Java 中的对象一定在堆上分配吗?
一般认为,Java对象都是在堆上分配的,但也有一些特殊情况,Java对象内存分配策略:
在Java中,典型的不在堆上分配的情况有两种:TLAB(Thread Local Allocation Buffer)和栈上分配(严格来说TLAB也是属于堆,只是在TLAB比较特殊)。
一 、栈上分配
JVM在server模式下的逃逸分析可以分析出某个对象是否永远只在某个方法,线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即时编译的收益)有所下降,所以在很长的一段时间里,即使是Server Compiler,也默认不开启逃逸分析,甚至在某些版本(如JDK 1.6 Update18)中还曾经短暂地完全禁止了这项优化。
二、TLAB分配
对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS和失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
JVM在内存新生代Eden Space中开辟了一小块区域,由线程私有,称作TLAB(Thread-local allocation buffer),默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。通常默认的TLAB区域大小是Eden区域的1%,当然也可以手工进行调整,对应的JVM参数是-XX:TLABWasteTargetPercent。
三、为什么不直接在堆上分配
因为堆是所有线程共享的,所以就是竞争资源,对于竞争资源必须采取必要的同步,所以当使用new关键字在堆上分配对象时是需要锁的,既然有锁就存在锁带来的开销,而且由于是对整个堆加锁,相对而言锁的粒度还是比较大的,影响效率。而无论是TLAB还是栈都是线程私有的,避免了竞争。
所以对于某些特殊情况,可以采取避免在堆上分配对象的办法,以提高对象创建和销毁的效率。
四、对象内存分配的两种方法
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
1) 指针碰撞(Serial、ParNew等带Compact过程的收集器)
假设Java堆中内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器,那所分内存就仅仅是把那个指针向空闲那一边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞(Bump the Pointer)”。
2)空闲列表(CMS这种基于Mark-Sweep算法的收集器)
如果Java堆中的内存不是规整的,已使用的内存和空闲的内存交错, 那就没有办法进行简单的指针碰撞了,虚拟机必须维护一个列表,记录上哪些内存是可用的。在分配是时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表(Free List)”。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
【原文链接】https://blog.csdn.net/zhaohong_bo/article/details/89419480
class文件格式
class文件是一组以8字节为基础单位的二进制流,各个数据项严格按照顺序紧凑排列在class文件中,中间没有任何分隔符。每个class文件都是由8字节为单位的字节流组成,所有的16位、32位和64位的数据都被构造成2个、4个和8个8字节单位来表示。
每一个Class文件对应于一个如下所示的ClassFile结构体:
ClassFile{
u4 magic; 魔数
u2 minor_version; 副版本号
u2 major_version; 主版本号
u2 constant_pool_count; 常量池计数器
cp_info constant_pool[constant_poll_count-1]; 常量池
u2 access_flags; 访问标志
u2 this_class; 类索引
u2 super_class; 父类索引
u2 interfaces_count; 接口计数器
u2 interfaces_count[interfaces_count]; 接口表
u2 fields_count; 字段计数器
field_info fields[fields_count]; 字段表
u2 methods_count; 方法计数器
methods_info methods[methods_count]; 方法表
u2 attributes_count; 属性计数器
attribute_info attributes[attributes_count]; 属性表
}
涉及内容包括:
- magic:魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
- minor_version、major_version:副版本号和主版本号,minor_version和major_version的值分别表示Class文件的副、主版本。一个Java虚拟机实例只能支持特定范围内的主版本号(Mi至Mj)和0至特定范围内(0至m)的副版本号。
- constant_pool_count:常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
- constant_pool[]:常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。
- access_flags:访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。
- this_class:类索引
- super_class:父类索引
- interfaces_count:接口计数器,interfaces_count的值表示当前类或接口的直接父接口数量
- interfaces[]:接口表,在interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。
- fields_count:字段计数器,fields_count的值表示当前Class文件fields[]数组的成员个数。
- fields[]:字段表,fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
- methods_count:方法计数器,methods_count的值表示当前Class文件methods[]数组的成员个数。
- methods[]:方法表,methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
- attributes_count:属性计数器,attributes_count的值表示当前Class文件attributes表的成员个数。
- attributes[]:属性表