Java虚拟机--JVM基础结构、堆、栈、方法区详解

初识Java虚拟机–基本结构

1、java虚拟机的基本结构图:

java虚拟机结构图
类加载子系统: 负责从文件系统或网络中加载Class信息,加载的了信息存放于方法区的内存空间中。除了类的信息,方法区中还会存放运行时常量池的信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池补发的内存映射)。
Java堆: 在虚拟机启动时建立,是java程序最主要的内存工作区域,几乎所有的java对象实例都存放在java堆中,堆空间是所有线程共享的,这是一块玉java应用密切相关的内存区域。
直接内存: Java的NIO库容许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区域,java堆外内存的大小不会直接受限于Xmx指定的最大堆大小。
垃圾回收系统: 是java虚拟机的重要组成部分,垃圾回收容器可以对方法区、java堆和直接内存进行回收,
Java栈: 每一个Java虚拟机线程都有一个私有的Java栈。一个线程的Java栈在线程创建的时候被创建。Java栈中保存这帧信息,Java栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关。
本地方法栈: 和Java栈非常类似,最大的不同在于Java栈用于Java方法的调用,而本地方法栈则用于本地方法的调用。(作为对java的扩展,java虚拟机允许Java直接调用本地方法(通常使用C语言编写))
PC(Program Counter)寄存器: 也是每个线程私有的空间,Java虚拟机会为每一个Java线程创建PC寄存器
执行引擎: Java虚拟机的最核心组件之一,它负责执行虚拟机的字节码。现在虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。

2、对象去哪了:辩清Java堆

几乎所有的对象都存放在堆中,并且java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,而不需要显式地释放。
根据垃圾回收机制的不同,Java堆有可能拥有不通的结构。最为常见的一种结构将整个Java堆分为新生代和老年代。
堆空间的一般结构
如图:新生代可能分为eden、s0、s1,其中s0和s1也被称为from和to区域,他们是两个大小相等、可以互换角色的内存空间。绝大多数情况下,对象首先在eden区分配,在一次新生代回收后,如果对象还存活,则会进入s0或s1,之后没经历一次新生代回收,对象如果还存活,它的年龄就会加1.当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。
下面实例来讲述Java堆、方法区和java栈之间的关系。

public class SimpleHeap {
	private int id;
	public SimpleHeap(int id){
		this.id = id;
	}
	public void show(){
		System.out.println("My ID is "+id);
	}
	public static void main(String[] args){
		SimpletHeap s1 = new SimpletHeap(1);
		SimpletHeap s2 = new SimpletHeap(2);
		s2.show();
		s2.show();
	}
}

上述代码声明了一个SimpleHeap类,并在main()函数中创建了两个SimpleHeap实例,此时,各对象和局部变量的存放如下图所示。SimpleHeap实例本身在堆中分配,描述SimpleHeap类的信息存放在方法区,main()函数中的s1和s2局部变量存放在Java栈中,并指向堆中的两个实例。
堆、方法区、栈的关系
3、函数如何调用:出入Java栈
Java栈是一块线程私有的内存空间。如果说java堆和程序数据密切相关,那么Java栈就是和线程执行密切相关的。线程执行的基本行为是函数的调用,每次函数调用的数据都是通过Java栈的传递。
Java栈与数据结构中的栈有着类似的含义,它是一块先进先出的数据结构,只支持出栈和入栈两种操作。在java栈中保存的主要内容为栈祯。每一次函数调用,都会有一个对应的栈祯被压入Java栈,每一次函数调用的结束,都会有一个栈祯被弹出Java栈

如下图所示:函数1对应的栈祯1,函数2对应栈祯2,以此类推。函数1中调用函数2,函数2中调用函数3,函数3中调用函数4。当函数1被调用时,栈祯1入栈;当函数2被调用时栈祯2入栈;当函数3被调用时栈祯3入栈;当函数4被调用时栈祯4入栈。当前正在执行的函数所对应的祯就是当前的祯(位于栈顶),它保存着当前函数的局部变量、中间运算结果等数据。
当函数返回时,栈祯从jaa栈中被弹出。Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常。不管使用哪一种方式都会导致栈祯被弹出。
在一个栈祯中,至少要包含局部变量表、操作数栈和帧数据区几部分;

在这里插入图片描述
注:由于每次函数调用都会生成对应的栈祯,从而占用一定的栈空间,因此,如果栈空间不足,那么函数调用自然无法继续进行。当请求的栈深度大于最大可用栈深度时,系统就会抛出StackOverflowError栈溢出错误。

Java虚拟机提供了参数-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。
下面的代码是一个递归调用,由于递归调用没有出口,这段代码肯能会出现栈溢出错误,在抛出错误后,程序打印了最大的调用深度。

public class TestStackDeep {
	private static int count = 0;
	public static void recursion(){
		count++;
		recursion();
	}
	public staic void main(String args[]){
		try{
			recursion();
		}catch(Thrwable e){
			System.out.println("deep of calling ="+count);
			e.printStackTrace();
		} 
	}
}

使用参数-Xss128K执行上述代码,部分结果如下:

deep of calling = 2505
java.long.StackOverflowError
......

可以看到大约2500次调用后,发生了栈溢出错误,通过增大-Xss的值,可以获得更深的调用层次,尝试使用参数-Xss256K执行上述代码,输出如下

deep of calling = 5809
java.long.StackOverflowError
......

备注:函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持的嵌套调用次数就越多。

3.1、局部变量表
局部变量表是栈祯的重要组成部分之一。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,函数的栈祯销毁,局部变量表也随之销毁。
注:相同的栈容量下,局部变量越少的函数可以支持更深层次的函数调用

在Class文件的局部变量表中,每个局部变量的作用域范围、所有槽位的索引(index列)、变量名(name列)和数据类型(J标识long)。

3.2、操作数栈
操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作;用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间。
3.3、帧数据区
主要存储常量池解析、正常方法返回和异常处理等。
如典型的异常处理表:

Exception table:
from to taget type
 4   16    19   any
19   21    19   any

3.4、栈上分配
栈上分配是Java虚拟机提供的一种优化技术,他的基本思想是:对于那些线程私有的对象,可以将他们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能

判断一个对象是否是一个线程的是有的对象的方法称为:逃逸分析;未发生逃逸的对象,虚拟机就有可能将对象分配在栈上,而不是在堆上;
逃逸实例:

private static User u;
public static void alloc(){
	u=new User();
	u.id=5;
	u.name="逃逸对象";
}

非逃逸实例:

public static void alloc(){
	User u = new User();
	u.id=5;
	u.name="非逃逸对象";
}

运行栈上分配参数设置:

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+printGC -XX:-UserTLAB -XX:+EliminateAllocations

1、-server :只有server模式下,才可以启用逃逸分析;
2、-XX:+DoEscapeAnalysis : 启动逃逸分析
3、-XX:+EliminateAllocations 启动了标量替换(默认打开),允许将对象打散分配在栈上。
4、-XX:-UserTLAB :关闭TLAB ;TLAB是thread local alloction buffer 线程本地分配缓存区;虚拟机的另一种虚拟机优化

栈上分配:依赖逃逸分析和标量替换的实现;
对于大量的零散小对象,栈上分配提供了一种良好的对象分配优化策略,栈上分配的数据快,并且可以有效的避免垃圾回收带来的负面影响,由于和对空间相比栈的空间较小,因此较大的对象不适合在栈上分配。
4、类信息保存区域:方法区
方法区:是一块所有线程共享的内存区域。它用于保存系统的类信息,如:类的字段、方法、常量池等;
JDK1.8之前,方法区可以理解为永久区(Perm);可用有一个使用。参数-XX:PermSize 和 -XX:MaxPermSize指定默认情况下MaxPermSize为64MB;如果永久区内存不足的情况下会报:java.lang.outOfMemoryError:Perm space
JDK1.8之后包括1.8,永久区已经被移除,改为了元数据区,这是一款堆外的直接内存,如果不指定大小可耗尽所有的可用系统内存;可用使用-XX:MaxMetaspaceSize指定大小;如果元数据区内存不足会报:java.lang.OutOfMemoryError: Metaspace

JAVA开发必备书籍推荐:

此文章来源:

实战JAVA虚拟机:JVM故障诊断与性能优化(第2版) 计算机与互联网 葛一鸣 电子工业出版社 9
【京东价】60.25元
【下单地址】https://u.jd.com/AyMl3p
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值