java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域各自有各自的用途,以及创建销毁的时间,有的区域随着虚拟机进程的启动存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
JVM最重要的特性,平台无关性。“书写一次,到处运行”。
“Java是解释执行”:Java的源代码,首先通过Javac编译成为字节码,然后,在运行时,通过java虚拟机内嵌的解释器将字节码转换成为最终的机器码。我们常用的Hotspot JVM,都提供了JIT编译器,也就是动态编译器,JIT能够在运行时将热点代码编译成为机器码,这种情况下热点代码就属于编译执行,而不是解释执行
JVM内存模型
JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)
程序计数器
是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的命令。因此每条线程都有独立的计数器。
如果线程正在执行Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行native方法,则计数器为空。不会出现OutOfMemoryError。
Java虚拟机栈
线程私有的,生命周期与线程相同。
每个方法在执行的同时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法从调用执行直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出站的过程。
虚拟机栈中存放局部变量表,包括基本数据类型、对象的引用(reference类型,可能是指向对象其实地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置),returnAddress类型(指向一条字节码指令的地址)。
long和double类型占用2个局部变量空间(Slot),其余占用一个。局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量的大小。
如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置,每个线程的内存大小),将抛出StackOverflowError异常。如果虚拟机可以动态扩展,如果扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈
与虚拟机栈相似,但是是为虚拟机栈使用到的Native方法服务。
虚拟机规范对本地方法栈中方法使用的语言、方式及数据结构没有强制规定。虚拟机可以自由实现它,有的就将本地方法栈和虚拟机栈合二为一了。也会抛出StackOverflowError和OutOfMemoryError。
Java堆
Java堆是被所有线程共享的一块内存,虚拟机启动时创建。存放对象实例。几乎所有的对象实例以及数组都要在对上分配。JIT编译器的发展和逃逸分析的成熟,栈上分配、标量替换优化出现。
Java堆GC收集器基本采用分代收集算法,Java堆分为:新生代和老年代,Eden空间、From Survivor空间、To Survivor空间等。
线程共享的Java堆总可能划分出多个线程私有的分配缓冲区。
Java堆是可扩展的(通过-Xmx和-Xms控制,最大最小内存),如果对着没有内存完成实例的分配,而且堆也无法扩展时,将会抛出OutOfMemoryError异常
方法区
线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。属于堆的一个逻辑部分,别名:Non-Heap。
方法去用永久代来实现,可以与堆一样进行GC。字符串常量池已经被移除永久代,字符串常量被移到了堆中。
内存回收的主要目标是针对常量池的回收和对类型的卸载。
注:类型卸载
只有当加载该类型的类加载器实例(非类加载器类型)为unreachable状态时,当前被加载的类型才被卸载.启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载。
-
一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的
-
一个被特定类加载器实例加载的类型运行时可以认为是无法被更新的
-XX:PermSize和-XX:MaxPermSize 控制方法区大小
Metaspace
java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace。
Metaspace与PermGen之间最大的区别在于:Metaspace并不在虚拟机中,而是使用本地内存。
如果类元数据的空间占用达到MaxMetaspaceSize设置的值,将会触发对象和类加载器的垃圾回收。
* -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
* -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
* -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
* -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
运行时常量池
运行时常量池是方法去的一部分。Class文件中处理有类的版本、字段、方法、接口等信息,还有一项就是常量池。用于存放编译期生成的各种字面常量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
并非预置如Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入常量池中,例如Stirng的intern()方法(new String("ddd").intern())。在jdk1.7之前,字符串常量存储在方法区的永久代(PermGen Space),在jdk1.7之后,字符串常量重新被移到了堆中。
直接内存
并不是JVM的一部分。被频繁使用,例如NIO类,引入了CHannel,Native函数会用到堆外内存,避免了在Java堆和Native堆来回赋值数据。可能会导致堆动态扩展时出现OutOfMemoryError异常
-XX:MaxDirectMemorySize 指定容量,不指定默认 -Xmx(Java堆最大值)
对象的创建
new指令,首先检查指令的参数在常量池中能否定位到类的引用符号,并检查是否加载、解析和初始化过,没有则先加载。
接下来为新生对象分配内存,所需内存大小在类加载完成后便完全确定。Java堆中内存时候规整的,使用指针碰撞(指针向空闲空间一段移动对象大小的距离)分配内存,不规整使用空闲列表方式。
指针不是线程安全的,两种方案解决,一分配内存空间进行同步处理,CAS方式保证原子性,二每个线程在Java对中预先分配一小块内存,称为本地线程分配缓冲区。
之后,虚拟机需要将分配到的内存空间都初始化为零值。
虚拟机要对对象进行必要的设置,例如对象是哪个实例,对象的Hash code,GC分代年龄等,都存在对象头。
new指令执行完成后,接着执行<init>方法,把对象初始化(赋值)。
对象头,一是存储对象自身的运行时数据,如hash code、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。32bit或64bit(32位或64位虚拟机中),称为 Mark Word。另外一部分是类型指针,即对象指向它的类元数据的指针。但查找对象的元数据信息并不一定要经过对象本身。第三部分是剩余的占位符,要求对象头是8字节的倍数。
对象的访问使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
欢迎关注公众号,让我们一起学习探讨问题