JVM-java内存区域

一、介绍一下Java内存区域(运行时数据区)

 

线程私有:

虚拟机栈、本地方法区、程序计数器PC

线程共享:

堆区、方法区、直接内存。

 

 程序计数器功能:

1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码流程,如:顺序执行、选择、循环异常处理等。

2.在多线程情况下,程序计数器用来记录当前线程执行的位置,从而切换回来的时候知道上一次运行的位置。

程序计数器是唯一一个不会出现OOM的内存区域,它的生命周期随着线程创建而创建,随着线程结束而结束。(因为它只存一条数据)

java虚拟机栈

一个线程会有一个私有的虚拟机栈,线程的每个方法会在虚拟机中开一个栈帧。

java虚拟机栈是由一个一个的栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息

局部变量表包括(boolean、byte、char、short、int、float、long、double)、对象引用

 

java虚拟机栈会出现两种错误:stackoverflowerror和outofmemoryerror

第一个错误:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈深度大于虚拟机栈的最大深度,抛出异常

第二个错误:java虚拟机内存大小可以动态扩展,若虚拟机在动态扩展栈的时候无法申请到足够大的内存就会爆出oom的异常。

 

本地方法栈:和虚拟机栈发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行java方法(也就是字节码服务),而本地方法栈为

虚拟机使用到的Native方法服务。

本地方法被执行的时候,在本地方法栈会创建一个栈帧用于存该本地方法的局部变量表、操作数栈、动态链接、出口信息

方法执行也会出现StackOverflow和OOM

 

java虚拟机管理的内存汇总最大的一块,java对是所有线程共享的一块内存区域,在虚拟机启动的时候创建。

此内存区域唯一的目的就是存放对象实例,几乎所有对象实例以及数组都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也备称为GC堆,从垃圾回收的角度,由于现在收集器基本都采用分代回收,

分为Eden、Survivor、old等空间,进一步划分是为了更好回收内存,或者更快分配内存。

 

大部分情况,对象会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还活着,则会进入S0或者S1

并且对象的年龄会加1,按照年龄从小到大对其所占有的大小进行累积,当累积的某个年龄大小超过了survivor区的一半取和max更小的值

,大于阈值晋升到老年代中,可以通过参数XX:MaxTenuringThreashold来设置。

 

 

方法区

方法区和堆区一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。

虽然java虚拟机把方法区描述为堆的一个逻辑部分,但是名字又称为非堆(no-heap)。

(方法区和永久代的关系)方法区是一种规范,永久代和元空间都是它的实现,jdk7是永久代,jdk8是元空间。

 

运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译器生成的各种字面量和符号引用)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法申请到内存时就抛出异常oom。

jdk8中,hotspot移除了永久代用的元空间,但是字符串常量池还在堆,运行时常量池还在方法区,只不过实现从永久代成了元空间。

 

 

 

 

二、Java对象创建的过程(五步,建议能够默写出来,并且知道每一步虚拟机做了什么)

Java对象创建过程

1、类加载检查

2、分配内存

3、初始化零值

4、设置对象投

5、执行init方法

Step1:类加载检查

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否在常量池中定位到这个类的符号引用,

并且检查这个符号引用代表的类是否被加载过、解析和初始化过。如果没有必须执行相应的类加载过程

step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需要的内存大小在类加载完成后便可确定,

为对象分配空间的任务等同于把一块确定大小的内存从java堆是否规整决定,而java对是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式:

 

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中创建对象是非常频繁的事情。

虚拟机通常用两种方式来保证线程安全:

1、CAS+失败重试:Cas是乐观锁的一种实现方式,所谓乐观锁就是指每次不加锁而是假设没有冲突而去完成某项操作,

如果因为冲突失败了就重试,直到成功为止。虚拟机采用cas配上失败重试的方式保证更新操作的原子性。

2、TLAB:为每一个线程在Eden分配一块内存,JVM在给线程中内存分配对象的时候,首先在TLAB分配

当对象大于TLAB中剩余内存或者TLAB内存用尽。再采用上述CAS进行内存分配

 

Step3:初始化零值

内存分配完后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了

对象的实例字段在java代码中可以不赋初始值直接用,程序访问到这些字段的数据类型所对应的零值。

 

 

step4:设置对象头

初始化零值完成以后,虚拟机需要对对象进行必要的设置,例如对象是哪个类的实例,如何才能找到元数据

对象的哈希码,对象的GC分代年龄等信息,都存放在对象头中。另外根据虚拟机不同的运行状态,是否使用偏向锁

会有不同的设置方式。

 

 step5:执行init方法

在上面工作完成之后,从虚拟机的视角来看,一个对象已经产生了,但从java程序来看对象创建才刚刚开始,<init>方法还没有执行,所有字段都还是0

所以一般执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化。

 

 

问题:对象的内存布局

对象在内存中的布局分为3个区域:对象头、实例数据、对齐填充

hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC、锁状态等)

类型指针,及对象指向它的类元数据的指针,虚拟机通过这个指针这个对象是那个类的实例。

 

实例数据部分是对象真正存储的有效信息,也是程序锁定的各种类型的字段内容

 

对其填充部分不是必然存在的,也没有特别含义,仅仅起一个占位的作用。因为hotspot虚拟机的自动内存管理系统要求

对象其实地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分刚好是8的整数倍

因此实例数据如果没对齐,就需要对齐填充来补全。

 

 

 

 

3、对象的访问定位的两种方式(句柄和直接指针两种方式)

建立对象就是为了使用对象,java程序通过栈上的reference数据来操作堆上的具体对象。

对象的访问由虚拟机实现而定,目前主流的访问方式1、使用句柄和2、直接指针两种

1、句柄:如果使用句柄那么java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址

而句柄中包含了对象实例数据与类型数据各种的具体地址信息

 

 2、直接指针:如果使用直接指针访问,那么java堆对象的布局中必须考虑如何访问类型数据的相关信息,而reference中存的直接就说对象的地址

 

 使用句柄来访问最大好处是refernce中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针而reference本身不需要修改。

 使用直接指针访问访问好处是速度快,节省了一次指针开销。

 

 

扩展问题

1、String类和常量池

2、8种基本类型的包装类和常量池

posted @ 2022-05-05 22:17  雷雷提  阅读(52)  评论(0编辑  收藏  举报