JVM内存区域划分及其作用

一、运行时数据区
1 、程序计数器 ( 寄存器 )
当前线程所执行的字节码行号指示器
字节码解释器工作依赖计数器控制完成
通过执行线程行号记录,让线程轮流切换各条线程之间计数器互不影响
线程私有,生命周期与线程相同,随 JVM 启动而生, JVM 关闭而死
线程执行 Java 方法时,记录其正在执行的虚拟机字节码指令地址
线程执行 Nativan 方法时,计数器记录为空( Undefined
唯一在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况区域
在这其中,很多不理解的没关系,我们学过多线程,有两个线程,其中一个线程可以暂停使用,让其他线程运行,然后等自己获得 cpu 资源时,又能从暂停的地方开始运行,那么为什么能够记住暂停的位置的,这就依靠了程序计数器,通过这个例子,大概了解一下程序计数器的功能。
 
2 、本地方法栈
不知道大家看过源码没有,看过的都应该知道,很多的算法或者一个功能的实现,都被 java 封装到了本地方法中,程序直接通过调用本地的方法就行了,本地方法栈就是用来存放这种方法的,实现该功能的代码可能是 C 也可能是 C++, 反正不一定就是 java 实现的。
上面两个不是我们所要学习的重点,接下来三个才是重点。
3 、虚拟机栈
这个大家都应该有所了解,现在来细讲它,虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用来存放存储局部变量表、操作数表、动态连接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这个话怎么理解呢?比如执行一个类 ( 类中有 main 方法 ) 时,执行到 main 方法,就会把为 main 方法创建一个栈帧,然后在加到虚拟机栈中,栈帧中会存放这 main 方法中的各种局部变量,对象引用等东西。如图
当在 main 方法中调用别的方法时,就会有另一个方法的栈帧入虚拟机栈,当该方法调用完了之后,弹栈,然后 main 方法处于栈顶,就继续执行,直到结束,然后 main 方法栈帧也弹栈,程序就结束了。总之虚拟机栈中就是有很多个栈帧的入栈出栈,栈帧中存放的都市一些变量名等东西,所以我们平常说栈中存放的是一些局部变量,因为局部变量就是在方法中。也就是在栈帧中,就是这样说过来的。
以上说的三个都是线程不共享的,也就是这部分内存,每个线程独有,不会让别的线程访问到,接下来的两个就是线程共享了,也就会出现线程安全问题。
4 、堆
所有线程共享的一块内存区域。 Java 虚拟机所管理的内存中最大的一块,因为该内存区域的唯一目的就是存放对象实例。几乎所有的对象实例度在这里分配内存,也就是通常我们说的 new 对象,该对象就会在堆中开辟一块内存来存放对象中的一些信息,比如属性呀什么的。同时堆也是垃圾收集器管理的主要区域。因此很多时候被称为 "GC " ,虚拟机的垃圾回收机制等下一篇文章来讲解。在上一点讲的栈中存放的局部引用变量所指向的大多数度会在堆中存放。
5 、方法区和其中的运行时常量池
和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码 ( 也就是存储字节码文件。 .class) 等数据,这里可以看到常量也会在方法区中,是因为方法区中有一个运行时常量池,为什么叫运行时常量池,因为在编译后期生成的是各种字面量 ( 字面量的意思就是值,比如 inti=3 ,这个 3 就是字面量的意思 ) 和符号引用,这些是存放在一个叫做常量池 ( 这个常量池是在字节码文件中 ) 的地方,当类加载进入方法区时,就会把该常量池中的内容放入运行时常量池中。这里要注意,运行时常量池和常量池,不要搞混淆了,字节码文件中也有常量池,在后面的章节会详细讲解这个东西。现在只需要知道方法区中有一个运行时常量池,就是用来存放常量的。还有一点,运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如 String.intern() 方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。
二、练习画内存图
平常分析中用到的最多还是堆、虚拟机栈和方法区。
例如:看下面这段程序,然后画出内存分析图
 
1 、首先运行程序, Demo1_car.java 就会变为 Demo1_car.class ,将 Demo1_car.class 加入方法区,检查是否字节码文件常量池中是否有常量值,如果有,那么就加入运行时常量池
2 、遇到 main 方法,创建一个栈帧,入虚拟机栈,然后开始运行 main 方法中的程序
3 Carc1=newCar(); 第一次遇到 Car 这个类,所以将 Car.java 编译为 Car.class 文件,然后加入方法区,跟第一步一样。然后 newCar() 。就在堆中创建一块区域,用于存放创建出来的实例对象,地址为 0X001. 其中有两个属性值 color num 。默认值是 null 0
4 、然后通过 c1 这个引用变量去设置 color num 的值,
5 、调用 run 方法,然后会创建一个栈帧,用来装 run 方法中的局部变量的,入虚拟机栈, run 方法中就打印了一句话,结束之后,该栈帧出虚拟机栈。又只剩下 main 方法这个栈帧了
6 、接着又创建了一个 Car 对象,所以又在堆中开辟了一块内存,之后就是跟之前的步骤一样了。
这样就分析结束了,在脑袋中就应该有一个大概的认识对堆、虚拟机栈、和方法区。注意这个方法区的名字,并不是就单单装方法的,能装很多东西。
这个只是一个简单的分析,可以再讲具体一点, 1 、创建对象,在堆中开辟内存时是如何分配内存的? 2 、对象引用是如何找到我们在堆中的对象实例的?通过这两个问题来加深我们的理解。
1、 创建对象的内存分配
在堆中开辟内存时是如何分配内存的?
两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么了。
指针碰撞 (bumpthepointer) :假设 Java 堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
空闲列表 (freelist) :有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录。这就叫做空闲列表。
选择哪种方式由 java 堆是否规整决定,而 java 堆是否规整决定由所采用的垃圾收集器 (GC) 是否带有压缩功能决定。
2 、创建对象时的线程安全问题
还需要考虑的一个问题是,对象创建在 JVM 中是非常频繁的行为,仅仅修改一个指针的位置。在并发的情况下可能也会带来线程安全的隐患。比如:正在给对象 A 分配内存,指针还没来的急修改,对象 B 又使用了原来指针的指针来分配内存。
两种解决方法:
一种是对分配内存空间的动作进行同步处理
另一种的把内存分配的动作按照线程分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲 (threadlocalallocationbuffer,TLAB). 哪个线程要分配内存就到哪个线程的 TLAB 上分配
3 、对象的访问定位
对象引用是如何找到我们在堆中的对象实例的?这个问题也可以称为对象的访问定位问题,也有两种方式。句柄访问和直接指针访问。画两张图就明白了。
句柄访问: Java 堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息

解释图:在栈中有一个引用变量指向句柄池中一个句柄的地址,这个句柄又包含了两个地址,一个对象实例数据,一个是对象类型数据 ( 这个在方法区中,因为类字节码文件就放在方法区中 )
 
直接指针访问:引用变量中存储的就直接是对象地址了,如图所示
 
解释:在堆中就不会分句柄池了,直接指向了对象的地址,对象中包含了对象类型数据的地址。
 
区别:这两种各有各的优势,
使用句柄来访问的最大好处就是引用变量中存储的是稳定的句柄地址,对象被移动 ( 在垃圾收集时移动对象是很普通的行为 ) 时就会改变句柄中实力数据指针,但是引用变量所指向的地址不用改变。
而使用直接指针访问方式最大的好处就是速度更快,节省了一次指针定位的时间开销,但是在对象被移动时,又需要改变引用变量的地址。在我们上面分析的例子中,就是使用的直接指针访问的方式。

猜你喜欢

转载自blog.csdn.net/lm1060891265/article/details/80766493