JVM虚拟机
关于Java内存区域(运行时数据区)
Java 虚拟机在执⾏ Java 程序的过程中会把它管理的内存划分成若⼲个不同的数据区域。JDK.1.8 和之前的版本略有不同。


-
线程私有的
- 程序计数器
- 虚拟机栈
- 本地⽅法栈
-
线程共享的
- 堆
- ⽅法区
- 直接内存 (⾮运⾏时数据区的⼀部分)
程序计数器
程序计数器是⼀块较⼩的内存空间,可以看作是当前线程所执⾏的字节码的⾏号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之间计数器互不影响,独⽴存储,我们称这类内存区域为“线程私有”的内存。
- 总结:
- 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
- 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。
注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建⽽创建,随着线程的结束⽽死亡。
Java 虚拟机栈
-
与程序计数器⼀样,Java 虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是Java⽅法执⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。
-
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java 虚拟机栈是由⼀个个栈帧组成,⽽每个栈帧中都拥有:局部变量表、操作数栈、动态链接、⽅法出⼝信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤(reference 类型,它不同于对象本身,可能是⼀个指向对象起始地址的引⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈也是线程私有的,每个线程都有各⾃的 Java 虚拟机栈,⽽且随着线程的创建⽽创建,随着线程的死亡⽽死亡。
本地⽅法栈
- 和虚拟机栈所发挥的作⽤⾮常相似,区别是:虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的Native⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
- 本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数栈、动态链接、出⼝信息。
⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
堆
- Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
- Java世界中“⼏乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
- Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代:再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。
方法区
- ⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。
*** 运行时常量池
-
运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池表(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)
-
既然运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 OutOfMemoryError 错误。
直接内存
- 直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 错误出现。
Java对象的创建过程
Step1:类加载检查
- 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
Step2:分配内存
-
在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java 堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
-
内存分配的两种⽅式:

-
内存分配并发问题
在创建对象的时候有⼀个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采⽤两种⽅式来保证线程安全:- CAS+失败重试:CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。
- TLAB:为每⼀个线程预先在 Eden 区分配⼀块⼉内存,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤上述的 CAS 进⾏内存分配
Step3:初始化零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
- 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
Step5:执⾏ init ⽅法
- 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始,
⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
对象的访问定位有哪两种⽅式?
-
建⽴对象就是为了使⽤对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
-
句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

-
直接指针:如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。

这两种对象访问⽅式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。
-

浙公网安备 33010602011771号