Java

xmy...大约 10 分钟

内存区域

image-20210909114511412
image-20210909114511412

HotSpot在JDK1.8之前方法区就是永久代,永久代就是方法区。

JDK1.8后删除了永久代,改为元空间,元空间在直接内存中。方法区就是元空间,元空间就是方法区。

创建一个线程,JVM就会为其分配一个私有内存空间,其中包括PC、虚拟机栈和本地方法栈

PC

用来指示下一个执行的字节码指令,基于这一点就能实现代码的控制流程

为了确保每个线程切换回来都能从上次的位置继续运行,PC必须是线程私有的,切换出去时需保存各自的PC

虚拟机栈

虚拟机栈中一个栈帧压入就对应一个方法的调用,栈帧弹出就对应方法返回。栈帧中包含:局部变量表、操作数栈、动态链接、方法出口信息。

局部变量表也就是常说的栈内存,用来存储基本类型和引用

HotSpot不支持动态扩展虚拟机栈,在创建线程时就确定了虚拟机栈的最大深度,如果申请不了这么多内存,就会抛出OOM错误,如果线程在运行时调用了很多方法,到达了栈的最大深度,就会抛出SOF错误

本地方法栈

和虚拟机栈相同,区别仅在于虚拟机栈中是java方法,本地方法栈中是native方法,但HotSpot中已经将二者合而为一了。

JVM中最大的一块内存空间,所有对象实例和数组都在这里分配内存,所有线程共享堆内存。

JDK1.7开始默认开启了逃逸分析,如果一个对象只在一个线程中被引用了,则该对象可以直接在栈上分配内存空间。

堆也叫GC堆,是垃圾回收的主要区域。为了便于垃圾回收,JDK1.8之前将堆分为三个部分:

  1. 新生代
  2. 老年代
  3. 永久代

而1.8之后将永久代删除了,取而代之的是元空间,元空间则在直接内存中。

此外,新生代还细分为eden、from survivor(s0)和to survivor(s1)

当新对象实例产生时,年龄为0,首先被分配在eden里,在一次gc后,如果还存活,就被扔到survivor里,并且年龄+1,当年龄增加到一定程度后就被扔到老年代里。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

方法区

存放被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1.8之前是永久代,属于堆内存。1.8之后是元空间,属于直接内存。

永久代受JVM创建时分配的最大堆内存限制,而元空间则受系统内存限制,可以存储更多。

常量池

分为字符串常量池和运行时常量池

1.8之后字符串常量池在堆中,而运行时常量池在元空间

直接内存

在JVM进程的内存空间之外,属于系统内存。

JVM可通过native方法对其进行直接操作,而无需在使用时将其拷贝到JVM内存区。

对象创建过程

Java创建对象的过程
Java创建对象的过程
  1. 类加载检查:

    JVM遇到一条new指令时,先检查能不能在常量池中定位到该类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有就要先进行类加载。类已被加载就通过检查。

  2. 分配内存:

    类加载检查通过后JVM为新对象分配内存,对象所需的内存大小在类加载完成后就确定了,JVM会在堆中按照指针碰撞空闲列表的方式为对象划分出一块空间,选择哪种方式会根据垃圾收集器的算法而定。此外,内存分配还要保证线程安全,JVM采用CAS+失败重试TLAB的方式保证线程安全。

    CAS+失败重试:乐观锁的一种实现,每次占用资源不加锁,而是不断尝试占用。

    TLAB:线程创建时预先在堆中给线程分配一块内存,称为TLAB,专门用来存放该线程运行过程中创建的对象,而TLAB满了时,采用上述CAS在堆的其它内存中分配

  3. 初始化零值:

    将对象的字段设为默认零值,不包括对象头

  4. 设置对象头:

    在对象头中设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄、是否启用偏向锁等信息

  5. 执行init方法:

    初始化对象,即按照程序员写的构造方法给对象进行初始化。

GC

内存分配与回收

新对象优先被分配在eden里,年龄设置为0,经历第一次gc后如果还存活,就被扔到survivor中,并且年龄+1,随后每经历一次gc如果还存活就年龄+1,如果年龄超过了上限(默认15),就被扔到老年代中。通过-XX:MaxTenuringThreshold设置上限。

此外,JVM还有动态年龄判定机制:如果在survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到年龄超过上线。

如果是过大的对象,为了避免其来回复制,可以在创建时直接扔到老年代里。通过-XX:PretenureSizeThreshold设置,只支持Serial和ParNew。

image-20210911183213177
image-20210911183213177

hotspot中gc分为两类:部分收集partial gc和全收集full gc。

部分收集又分minor gc、major gc和mixed gc

minor gc:只对新生代进行回收

major gc:只对老年代进行回收(有时也指代full gc)

mixed gc:对整个新生代和部分老年代进行回收

全收集full gc:对整个堆和方法区(hotspot中的元空间)进行回收

判别对象、常量、类死亡的方法

  • 对象
  1. 引用计数法:

    对象中设置一个引用计数器,每当该对象被引用时,引用计数器就会+1,失去一个引用时就会-1。引用计数器为0时就代表已经死亡,不会再被引用了。这种判别方式有个缺点,如果两个对象互相引用,但又没有外界对它们的引用,则它们引用计数都为1,会一直存在,但没有意义。

  2. 可达性分析法:

    设置一组对象为gc roots,如果一个对象没有能到达任何一个gc root的引用链,则判别这个对象死亡。一般一个线程启动后并列创建的一组对象会构成gc roots,gc roots内部引用的对象就是非gc root。image-20210911220120397

  • 常量

没有被任何对象引用时就是废弃的

同时满足如下三点就是无用的类

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类 的方法。

引用类型都有哪些?

  1. 强引用

    Object strong = new Object();
    

    一个对象如果被引用,且最高级别是强引用,就不会被回收。

  2. 软引用

    SoftReference<Object> soft = new SoftReference<>(new Object());
    

    一个对象如果被引用,且最高级别是软引用,发生gc时内存足够就不会被回收,内存不够就会被回收。

  3. 弱引用

    WeakReference<Object> weak = new WeakReference<>(new Object());
    

    一个对象如果被引用,且最高级别是弱引用,发生gc时不管内存够不够都会被回收。

  4. 虚引用

    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> phantom = new PhantomReference<Object>(new Object(), queue);
    

    虚引用创建时必须搭配ReferenceQueue。

    一个对象如果被引用,且最高级别是虚引用,就等于没有被引用,发生gc时不管内存够不够都会被回收。

    虚引用看起来和弱引用没啥区别,只是必须搭配ReferenceQueue。

    用虚引用的目的一般是跟踪对象被回收的活动。

  5. ReferenceQueue

    软引用、弱引用和虚引用在创建时都可以关联一个ReferenceQueue,其中虚引用必须关联,其余两个可选关联。

    关联了ReferenceQueue的引用所引用的对象在被回收内存之前,这个引用会被JVM加入到关联的ReferenceQueue中。通过这样的机制,我们就能通过监听该队列,在对象内存被回收前进行一些自定义处理。

在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

GC算法有哪些?

  • 标记-清除 :先标记上所有存活的对象,再一次性回收掉没被标记的,这是最基础的算法,后面的几个都是对其效率和空间碎片问题做了优化。 碎片问题会导致都是小空隙,装不下大对象,而如果将对象整理起来就会空出更大的空隙。

  • 复制 :将内存分为两块,只用其中一块,当用的这一块满了后,还是对其中存活的对象进行标记,然后将这些被标记的逐个复制到另一块内存,最后将剩余的死亡对象一次性回收,说白了就是两块内存来回倒。由于一次gc需要处理的总内存变小了,效率也就提升了。

  • 标记-整理 :和标记-清除一样,先标记存活对象,然后将它们堆到一端,最后回收掉末端以外的对象。

  • 分代收集 :新生代死亡对象比较多,一般用复制算法。老年代死亡对象比较少,一般用标记-清除或标记-整理算法

类加载流程

加载

将字节流读入JVM内存,在方法区存储为一个数据结构,同时创建一个对应的Class对象供程序访问。

验证

一共4步:

  1. 文件格式验证

    验证读进来的字节流是否符合Class标准格式。

  2. 元数据验证

    格式对了之后,验证一下里边的数据合不合理。

    比如:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。

  3. 字节码验证

    分析类的方法体(Class文件中的Code属性),确保方法在运行时不会危害虚拟机。

  4. 符号引用验证(发生在解析阶段)

    检查常量池中引用的外部类是否存在,是否可以正常访问。

准备

类变量(静态变量)分配内存并设置初值

其中如果是final修饰的,意味着在Class文件中,该字段的属性表中存在ConstantValue属性,此时初值设置为代码里写的。

如果不是,就设置为零值,等到初始化阶段再赋值。

解析

把符号引用(地址无关)转化为直接引用(地址相关)

初始化

执行clinit方法,这里要注意不是构造方法,而是执行静态语句,包括静态变量赋值和静态块

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.14.8