(本文参考深刻理解JAVA虚拟机第二版第2章)
复制代码
在讲 JVM 以前,先讲讲 JDK、JRE和 JVM 的关系,以下面这张图(图片来自百度图片): html
JVM:熟称Java虚拟机,也叫运行时数据区域,是保证跨平台的基本,由于 jvm 只认识字节码,只要linux、window、mac 有jvm 都是能够编译执行的;固然它还有一个java
而这里,咱们就须要讲解 JVM 这个 运行时数据区域的分布了,以下图(图片来自百度图片,稍微修改了一点):linux
上面解释了一个java程序是怎么运行的,其中 内存空间这里,就是 JVM 了;web
为了方便解释,这里的顺序不会像上图那里的顺序来;算法
首先先了解程序计算器,线程(UI线程)中程序语句的执行都离不开它,对它的解释以下:安全
结合方法去中的一些变量和常量去理解会比较好
复制代码
虚拟机栈也是线程私有的,与线程的生命周期相同;它对应着线程的内存模式,每一个方法在执行的时候,都有一个栈帧用于存储局部表,操做数栈、动态连接、方法出口等信息;每一个方法的执行,都对应着一个栈帧在虚拟机栈中的入栈和出栈,以下图(网上找的,当时保留的,具体哪位的有点忘了,看到能够联系我) websocket
当进入一个方法时,这些变量在帧中分配的内存大小时固定的,在运行时不会改变局部变量表的大小。针对这个区域,规定了两种异常状况多线程
操做数栈:并发
操做数栈,也能够称作操做栈,它能够是 Java 的任意类型,在数据提取时入栈和出栈,好比 int a = 1 + 2;在把1,2入到这个操做的栈的时候,也会把1,2提取出来,再分配给 a; 动态链接:jvm
能够这样理解,好比线程中的一个A方法,在类加载的时候,它只是一个符号引用,在运行期间,转换为直接引用,这种称为动态链接,关于符号引用,后面会说道。 方法出口: 其实就是返回地址,当方法执行完毕或者手动退出时,就出栈了,用来记录一些信息,好比恢复局部变量等信息
本地方法栈与虚拟机栈的做用很是类似;只不过虚拟机栈执行的是 java 的字节码服务,而本地方法栈执行的是 Native 方法服务; 本地方法栈一样会穿件栈帧,如局部变量表、操做栈等信息,同时也有 StackOverflowError 和 OutOfMemoryError 异常
是Java虚拟机锁管理的内存中最大的一块,在虚拟机启动建立时,此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都是在这分配内存的; Java 堆是内存回收的主要区域,也叫 GC 堆;根据规定,Java堆的物理地址能够是不连续的,只要保证逻辑上是连续的便可。因为Java 堆基本采用分代手机算法,因此也能够分为:新生代和老年代;再细致分,也能够分为 Eden空间,From Survivor 空间、To Surivivor 空间等涉及到的GC回收算法,后面再开章节介绍。
方法堆也是线程共享的一个区域块,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区规定为 Java 堆的一个逻辑模块,但它还有一个方法,叫 Non-Heap (非堆) ,目的就是为了和 Java堆区分开来
运行时常量池,其实算方法区的一部分。Class文件中除了有 类的版本、字段、方法、接口等信息外;还有一项信息就是常量池,用于存放编译期生成的字面量和字符引用,以下图: (图片来源 blog.csdn.net/wangbiao007…)
在JDK1.4中,新增长了一个 NIO(New Inout/Outinput)类,引入了一种基于通道(channel)与缓冲区(buffer)的I/O方式,它可使用 Native 函数库直接分配堆外内存,而后经过一个存储在Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样在一些场景中可以显著提高技能,避免了数据再 Java 堆和 Native 堆中来回复制数据,常见的通道类型有:
具体案例能够查找NIO的具体案例 直接内存,不是虚拟机运行时内存区的一部分,也不是Java规范中定义的内存区域。但既然是内存,若是 超过了 RAM 和 SWAP 寻址空间限制,仍是会报OutOfMemoryError的。
上面了解了 JVM 的一些知识以后,那么一个对象的建立是怎么样的呢?对象的建立,能够分为如下几个步骤
当虚拟机遇到一个 new 指令的时候,会先去检测这个指令的参数是否能定位到这个类的符号引用,并检查这个类是否被加载、解释或初始化过。若是没有,则执行类加载 (后面新开一章解释)
内存分配
在类加载经过以后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后即可彻底肯定,至关于从Java堆中抽取一块内存出来;而根据内存的是否绝对规整,分为 指针碰撞 和 空闲列表 两种分配方式:
从上面的解释看,用哪一种分配方式,是经过Java堆的内存块是否绝对规整决定的。
内存分配
但对象的建立是频繁的,在并发的状况,多线程不必定是安全的,即存在A对象在分配内存,指针还将来得及修改,B对象也同时使用了原来的指针来分配对象。因此又衍生了两种解决办法,CAS+失败重试 和 TLAB两种方式
初始值为零
在内存分配完成以后,虚拟机须要将分配到的内存空间初始化为零值 (除对象头外),这一步操做也保证了对象的实例字段在java代码中能够不赋初始值就可使用,由于程序能访问这些字段的数据类型所对应的零值。
设置对象头
初始值设置以后,怎么知道对象是哪一个类的实例,如何才能找到类的元数据信息、哈希码、GC分代年龄等信息呢?这就须要对对象头进行一些必要的设置,才能定位到,详细在5.2节介绍。
入栈,执行init指令:
从虚拟机来看,对象已经分配产生完成了,且入栈了;但 Java 程序来看,这才刚开始,因此,new 以后,则执行 init 方法,进行初始化。
上面讲解了对象在 虚拟机的分配以后,再扩展一下,对象在内存中是怎么分配的呢,对象在内存中的存储布局可分为 3个部分:
其中,对象头能够再细分为两部分:
实例数据
是对象真正储存的有效信息,好比程序中定义的各类类型的字段内容,不管父类和子类都会记录下来;在分配时,相同宽度的字段会被分配到一块儿,这也是父类定义的变量会出如今子类以前的缘由。
对齐填充
没啥实际意义,只是为了保证对象是8字节的整数倍,没对齐时,用来补全而已。
创建对象是为了使用对象,Java 程序须要经过栈上的 reference 数据来操做堆上的具体对象;但这些访问方式取决于虚拟机实现而定,目前主流有句柄和直接指针两种:
优势介绍: 句柄:使用句柄好处是,reference中存放的是文档的句柄地址,对象被移动时,只改变句柄的实例数据指针,而reference 自己不须要修改 直接指针:使用直接指针的最大好处就是速度更快,节省了指针定位的开销;
为何字符串拼接的时候,不适合用 String ,而应该使用 StringBuilder 或者 StringBuffer ? 好比 String = "abc"; (可参考常量池来解释哟)