【Java虚拟机】——Java内存区域
运行时数据区
一、Java内存区域图
由图可知,Java内存区域大致被分为两个,一个是线程共享内存区,一个是线程私有内存区。
线程私有内存区包括:程序计数器,本地方法栈,虚拟机栈
线程共享内存区包括:Java堆和方法区。
那么,为什么要区分线程私有和线程共享呢?因为生命周期不同首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。
二、运行时数据区图
由图可知各个数据区的功能,
程序计数器:通过改变程序计数器的值来选取下一条需要执行的字节码命令。
Java虚拟机栈:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表(是一组变量值的存储空间,用于存放 方法参数 和 局部变量)、操作数栈(方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的)、动态链接、方法出口等
本地方法栈:用来执行本地方法,所谓本地方法,就是不是用Java语言编写的方法,编译成和处理器相关的机器代码,保存在动态链接库中
Java堆:用来分配所有对象的实例,同时也是Java虚拟机垃圾回收机制的主要工作场所。
方法区:用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。同时还存放运行时常量池(Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中)
HotSpot虚拟机对象的创建
一、new和newInstance的区别
1.类的加载方式不同
在使用newInstance()方法的时候,必须保证这个类已经加载并且已经连接了,而这可以通过Class的静态方法forName()来完成的。
2.调用的构造方法不同
- new关键字能调用任何构造方法。
- newInstance()只能调用无参构造方法。
3.执行效率不同
- new关键字是强类型的,效率相对较高。
- newInstance()是弱类型的,效率相对较低
既然使用newInstance()构造对象的地方通过new关键字也可以创建对象,为什么又会使用newInstance()来创建对象呢?
假设定义了一个接口Door,开始的时候是用木门的,定义为一个类WoodenDoor,在程序里就要这样写 Door door = new WoodenDoor() 。假设后来生活条件提高,换为自动门了,定义一个类AutoDoor,这时程序就要改写为 Door door = new AutoDoor() 。虽然只是改个标识符,如果这样的语句特别多,改动还是挺大的。于是出现了工厂模式,所有Door的实例都由DoorFactory提供,这时换一种门的时候,只需要把工厂的生产模式改一下,还是要改一点代码。
而如果使用newInstance(),则可以在不改变代码的情况下,换为另外一种Door。具体方法是把Door的具体实现类的类名放到配置文件中,通过newInstance()生成实例。这样,改变另外一种Door的时候,只改配置文件就可以了。示例代码如下:
String className = 从配置文件读取Door的具体实现类的类名;
Door door = (Door) Class.forName(className).newInstance();
再配合依赖注入的方法,就提高了软件的可伸缩性、可扩展性。
二、对象创建过程
1.分配内存
(1)为了安全,在创建对象分配内存空间时进行同步处理,把内存分配的动作按照线程划分到不同的空间进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),那个线程要分配内存,就在那个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
(2)空闲列表:记录那块内存区域可用。
(3)指针碰撞:把只是分界点的指针向着空闲区域的移动一段与对象大小相同距离。
2.初始化
所用内存空间都初始化为零值(保证对象的实例化字段在Java代码中可以不赋初始值就直接使用)
三、对象的内存布局
1.对象头
对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
2.示例数据
对象真正存储的有效信息
3.对齐填充
HotSpot VM的自动内存管理系统要求对象起始地址必须是8bytes的整数倍,就是对象的大小必须是8字节的整数倍。对象头是8字节的整数倍,所以当实例数据部分没有对齐时,需通过对齐填充来补全。
四、对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象
1.句柄
存在指向实例池内对象实例化数据的指针和指向方法区中对象类型数据的指针
2.直接指针
Java堆中对象实例化数据中包含指向方法区中对象类型数据的指针
五、OutOfMemoryError异常
1.Java堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,对象数量达到最大堆容量限制,则发生溢出。
限制Java堆的大小为20MB,最小值和最大值相同(不可扩展)。当虚拟机出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
运行结果:
2.虚拟机栈和本地方法栈溢出
HotSpot不区分虚拟机栈和本地方法栈,栈容量只能由-Xss参数设定。
- StackOverFlow:线程申请的栈深度超过允许的最大深度
- OutOfMemoryError: 虚拟机扩展时无法申请到足够的内存空间
3.方法区和运行常量池溢出