大三下学期,距离放假还有一个星期,打算重温一下《深入理解jvm虚拟机》这本书
JVM运行时数据区
我们知道,每个线程都是一个顺序执行的单元
所谓的线程独占区,就是每开辟一个线程,线程内都会包含一个相互独立的(虚拟机栈、本地方法栈、程序计数器),
而所谓的线程共享区,就是多个线程同时共享同一个(方法区、Java堆)。
下面就一起来看看上图所述的JVM运行时的数据区结构
先来说说最简单的部分——程序计数器:
1、程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
2、程序计数器存在于线程独占区,JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器。
3、如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个程序计数器的值为undefined。
4、此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域(因为程序计数器是jvm负责维护的,开发人员不进行操作,所以也不存在OutOfMemoryError情况)。
这里多说一句:goto是Java中保留的关键字(就是目前Java不用,也不让你用,为什么要保留呢?可能以后的版本中,goto会被Java使用,若在此之前,Java没用限制你用goto关键字,在日后goto被Java使用时,那么你原先的代码改动就可能比较大了,所以目前来说goto是Java中保留的关键字),若日后可以使用"goto"关键字,那么可能就意味着可以对程序计数器进行操作了。。。
Java虚拟机栈:
虚拟机栈描述的是Java方法执行的动态内存模型
栈帧:
每个方法执行,都会开辟一段内存区域用于存放方法执行所需数据——创建一个栈帧,它会伴随着方法从创建到执行完成。栈帧存储着局部变量表、操作数栈、动态链接、方法出口等。 JVM虚拟机它是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
局部变量表: 编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型。
注意!!!局部变量表所需的内存空间在编译期间完成分配。意味着当进入一个方法时,这个方法需要在栈帧中分配多少内存是固定的,在方法运行的期间局部变量表的大小不会发生改变。
思考:根据上面所说的,局部变量表包括了引用类型,如果是一个User对象,user对象中有一个String类型的name属性,那么我们在运行期间会给他设定值,他的值的长度是不固定的,那么他的内存大小会发生改变吗?之前不是说:"在方法运行的期间局部变量表的大小不会发生改变的吗?"
答:其实,在局部变量表中所存放的其实只是对象的引用,对象的创建实际是创建在了堆内存中,栈中的局部变量表只是存储了user对象的引用(关于对象的创建过程,后面会提到),这个对象的引用,大小是不会改变的。
栈帧是有大小的,如果栈中所含的栈帧过多,就会出现熟知的StackOverFlowError(可以通过递归进行模拟)
package cn.itcats.jvm.test1;
/**
* 通过递归循环调用,模拟栈溢出
* @author itcats
*/
public class StackOverFlowErrorTest {
public void test() {
System.out.println("方法执行了");
test();
}
public static void main(String[] args) {
new StackOverFlowErrorTest().test();
}
}
抛出大量异常信息
at cn.itcats.jvm.test1.StackOverFlowErrorTest.test(StackOverFlowErrorTest.java:9)
at cn.itcats.jvm.test1.StackOverFlowErrorTest.test(StackOverFlowErrorTest.java:9)
at cn.itcats.jvm.test1.StackOverFlowErrorTest.test(StackOverFlowErrorTest.java:9)
at cn.itcats.jvm.test1.StackOverFlowErrorTest.test(StackOverFlowErrorTest.java:9)
at cn.itcats.jvm.test1.StackOverFlowErrorTest.test(StackOverFlowErrorTest.java:9)......忽略
总结:
1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2、如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈:
(其实HotspotVM不区分Java虚拟机栈和本地方法栈,两者非常相似,但学习不局限与HotspotVM,而是遵循jvm的一个规范,还是有必要学习本地方法栈的)
虚拟机栈:为虚拟机执行Java方法服务。
本地方法栈:为JVM所调用到的Nativa即本地方法服务。
异常方面: 和虚拟机栈出现的异常很相似。
堆内存(分为新生代和老年代)
新生代分为:
Eden 伊甸园 (被new完后存在于Eden,被新建的对象,垃圾回收常光顾的地方)
Servivor 存活区 (在Eden未被垃圾回收所收集的对象) FROM区和TO区
老年代:
Tenured Gen (存活区中未被垃圾回收所收集的对象,地位较高的对象)
特点:所有线程共享一块内存区域,在虚拟机开启的时候创建。
1、存储对象实例,更好地分配内存。
2、堆内存是jvm所管理的最大的一块内存区域,意味着堆内存也是垃圾收集器(GC)常光顾的一块区域。
3、划分为新生代,老年代,Eden空间,都是为了更好的执行垃圾回收。
4、存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。
5、在JIT编译器等技术的发展下,所有对象不都在堆上进行分配。有些对象实例也可以分配在栈中。
6、实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。 如常见的处理 -Xmx -Xms
7、如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。(可以通过死循环内部不断对对象进行创建)
8、类中的成员变量存储在堆区
方法区
特点:所有线程共享一块内存区域。
1、存储虚拟机加载后的类信息,常量,静态变量,即时编译器编译后的代码等数据
(类信息包括类的版本、字段、方法、接口)。
2、HotSpot VM使用永久代来表示方法区,目的就是为了垃圾收集器可以像管理Java堆一样管理这部分内存 ,能够省去了专门为方法区编写内存管理代码的工作,其实这两者并不等价,上述说法仅对于HotSpot。
3、当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
4、运行时常量池存在于方法区中(StringTable 实际是HashSet)
5、方法区又叫静态存储区,存放class文件和静态变量
6、方法区有个别名,叫非堆,有些人喜欢叫他永久代,因为方法区是永久代实现的,但是两者并不等价,仅仅是因为SpotHot使用永久代实现方法区。实现的目的:HotSpot的垃圾收集器可以像管理java堆一样管理这部分内存。圾收集行为在这个区域是比较少出现的,但并非数据进入方法区就如永久代的名字一样永久的存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
具体可以了解《JVM中常量池存放在哪里》
package cn.itcats.jvm.test1;
public class StringTableTest {
/*
* main方法执行
* 1、Java虚拟机栈创建一个栈帧,栈帧中存储着局部变量表等。
* 2、局部变量表: 编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型。
* 3、因为局部变量表所需的内存空间在编译期间完成分配,字符串长度不定,所以栈帧中实际存放的是引用a与b
* 4、若在Java堆中创建了两个对象"abc",则结果应该为false(地址不同)
* 5、事实上,abc存放在方法区中的常量池(StringTable)中,实际上是HashSet,引用a的"abc"先存放入StringTable中,
* 因为Set是无序不重复的,可知StringTable中目前只存在一个abc,
* 而Java虚拟机栈中栈帧中的局部变量表的引用a和b同时指向方法区中的常量池的同一"abc"
*/
public static void main(String[] args) {
String s1 = "abc"; //存在于常量池,字节码常量
String s2 = "abc"; //存在于常量池,字节码常量
System.out.println(s1 == s2); //true
String s3 = new String("abc");
System.out.println(s1 == s3); //false 通过new创建对象一定存在于堆内存
System.out.println(s1 == s3.intern()); //true 运行时常量池,将堆内存中的s3迁移到方法区的常量池中,比较则是用常量池的"abc"相互比较
}
}