Java内存区域划分以及对象创建回收和类的创建回收过程

本文深入解析Java线程的内存区域,包括线程隔离区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(Java堆、方法区、运行时常量池、直接内存),并详细阐述了对象的创建、内存布局、访问定位和回收过程。

线程的内存区域

1.线程隔离区域:

程序计数器:(唯一一个不会出现OutOfMemoryError的区域)

是一块很小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器就是就是通过改变程序计数器里面的值来选取下一条需要执行的字节码指令,跳转啊,循环啊。同样线程的切换也是需要上一次线程所运行的状态,位置,线程恢复都是通过这个计数器来完成的,

虚拟机栈:

虚拟机栈是由一个个栈帧组成的,每一个方法被调用时后悔产生一个栈帧。每一个栈帧都存储着局部变量表(基本数据类型[boolean,byte,char,short,int,float,long,double],对象引用),操作数栈(在方法的执行过程中,会有各种字节码往操作数栈里面写入和提取内容),等信息,每一个方法从调用到执行完成的过程中,都对应着栈帧从虚拟机栈中入栈到出栈的过程。

    扩展:那么方法/函数如何调用?

   Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。

   Java方法有两种返回方式:

  1. return 语句。
  2. 抛出异常。

   不管哪种返回方式都会导致栈帧被弹出。

 

线程执行过程中,方法递归调用,产生过多栈帧,会抛出StackOverflowError,栈帧的大小超过虚拟机所允许的限制,也会抛出StackOverflowError。

虚拟机栈允许动态扩展,当尝试扩展内存不足或者新的线程初始化新的虚拟机栈申请不到新的空间,就会报OutOfMemoryError。

本地方法栈:

和虚拟机栈几乎类似,虚拟机栈为java方法服务,本地方法栈为Native方法服务。

2.线程共享区域:

java堆:

所有对象的创建都在这个区域进行内存分配,所有的实例对象和数组都在这里分配对象。

垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),之后会在两个survivor之间复制移动,每一次移动都会增加年龄,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

 

方法区:(jdk1.8中称为元空间)

-XX:MaxMetaspaceSize设置最大元空间的大小 。防止Metaspace空间无限扩大不断使用本地的内存。

-XX:MetaspaceSize设置元空间的大小。达到这个大小的时候就会触发Metaspace GC

-XX:MinMetaspaceFreeRatio=N。当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。

-XX:MaxMetasaceFreeRatio=N 。当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(jdk1.7也被称为永久代)

jdk 1.8 的时候,方法区被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。(只受本地内存大小的限制)

元空间何时会发生OOM:

 

运行时常量池:

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

JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

这里关于基本类型的常量池说明一下:

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。

比如Integer i=40;Integer j=40;i==j true(这里其实就是可以当做常量字符串这样来使用)

直接内存:

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。(这里涉及NIO),Java的NIO可以使用Native方法直接在java堆外分配内存,使用DirectByteBuffer对象作为这个堆外内存的引用。

 

对象的创建到回收:

1.对象创建

    1.类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

     2.分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。

     对于内存分配的方式:

Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

      对于内存分配的并发问题:

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

     

     对象会优先在eden区分配:(当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC)

     Minor GC 和 Full GC 有什么不同呢?

  • 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
  • 老年代 GC(Major GC/Full GC):指发生在老年代的 GC(通常是指老年代没有空间存放对象时),出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

     分配担保机制:

虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 对象无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去。

大对象直接进入老年代:大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。---为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

 

3.初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5.执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

 

2.对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

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

 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

 

 

1.句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; 

 

 

 

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

 

 

 

 

总结:

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

 

4.对象的回收:

判断一个对象是否已经无效?

1.引用计数法:

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

 

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

下面这段代码就会出现相互引用内存无法释放的情况:

 

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}

2.可达性分析算法:

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

对堆中的对象进行可达性分析,没有引用变量指向这个对象的时候,就会回收这个对象。

对象在内存中的三种状态:(不可达对象并非"非死不可")

可达状态:对象创建之后,有一个或者多个引用变量指向改对象,可以通过这些引用变量调用该对象的实例变量和实例方法

可恢复状态:程序中某个对象没有引用变量,为可恢复状态,在这个状态下,系统的垃圾回收机制准备回收该对象所占内存,回收之前会调用finalize()方法,会把他放到一个队列里面,如果系统在调用此方法时能够让一个引用变量重新引用该对象,则进入可达状态,否则进入不可达状态。

不可达状态:当对象与其他任何引用变量没有关联,且已经执行了finalize()方法仍然没有进入可达状态,则永久性的失去引用。只有是不可达状态的对象才能被回收。

类的加载过程:(类在实例化对象的时候或者通过类名调用静态方法或者变量的时候会被加载)

详情参见https://www.cnblogs.com/smallJunJun/p/10102289.html

加载:就是查找到到这个类的.class文件,利用.class文件创建class对象

验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:就是给类变量赋初值(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值)。

解析:将常量池中的符号引用替换为直接引用,就是把这些引用指向具体的对象的过程。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

类的回收过程:(主要是对元空间的类进行回收)

1、该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例;

2、加载该类的ClassLoader已经被回收;

3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

转载于:https://www.cnblogs.com/smallJunJun/p/10517159.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值