JVM
1、JVM 指的是Java虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件,作用是为了支持跨平台特性。
2、JVM的功能有三项:第一是解释执行字节码指令;第二是管理内存中对象的分配,完成自动的垃圾回收;第三是优化热点代码提升执行效率,
3、JVM组成分为类加载子系统、运行时数据区、执行引擎、本地方接口这四部分。
4、常用的JVM是Oracle提供的Hotspot虚拟机,也可以选择GraalVM、龙井、Openj9等虚拟机。
什么是运行时数据区?
运行时数据区指的是JVM所管理的内存区域其中分成两大类
1、线程共享-方法区、堆
方法区:存放每一个加载的类的元信息、运行时常量池、字符串常量池。
堆:存放创建出来的对象。
2、线程不共享-本地方法栈、虚拟机栈、程序计数器
本地方法栈和虚拟机栈都存放了线程中执行方法时需要使用的基础数据。
程序计数器存放了当前线程执行的字节码指令在内存中的地址。
直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存。
哪些区域会出现内存溢出,会有什么现象?
内存溢出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误。
堆:溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。
栈:溢出之后会抛出StackOverflowError。
方法区:溢出之后会抛出OutOfMemoryError,JDK7及之前提示永久代,JDK8及之后提示元空间。
直接内存:溢出之后会抛出OutOfMemoryError。
方法区和永久代 元空间的关系
方法区是JVM规范中定义的一个内存区域,存储与类相关的元数据。
永久代是在JDK 7及之前版本中,用于实现方法区的内存区域。
元空间是JDK 8及之后的版本中,用于替代永久代的内存区域,存储同样类型的元数据,但使用的是本地内存而非堆内存。
关于 堆的基本结构 和动态对象年龄判定的 简单概要的讲一下
JVM中的堆(Heap)是用来存储对象实例和数组的内存区域。它是所有线程共享的内存区域,主要用于分配Java对象。堆的结构可以分为以下几个区域:
-
新生代(Young Generation):
-
新生代又分为三个区域:Eden区、From Survivor区、To Survivor区。
-
Eden区:新创建的对象通常会先分配在Eden区。当Eden区满时,会触发一次Minor GC,将存活的对象移到Survivor区。
-
Survivor区:新生代中两个相同大小的区域,分别称为From Survivor和To Survivor。每次GC后,幸存的对象会在这两个区之间来回移动,直至被晋升到老年代。
-
-
老年代(Old Generation):
-
存放从新生代晋升过来的生命周期较长的对象。当老年代空间不足时,会触发Major GC(Full GC),这通常会伴随较高的暂停时间。
-
-
永久代(Permanent Generation)或元空间(Metaspace):
-
永久代:存储类的元数据(Class Metadata),如类信息、方法信息。JDK 8及之后被元空间(Metaspace)取代。
-
元空间:与堆不同,它使用的是本地内存来存储类的元数据。
-
动态对象年龄判定
在JVM中,垃圾回收器需要决定对象是否应该从新生代晋升到老年代。对象的年龄就是一个重要的判定依据。通常来说,年龄越大的对象越有可能存活下来,因此需要晋升到老年代。
对象年龄的判定规则:
-
每经过一次Minor GC,存活下来的对象年龄+1:
-
当对象在Eden区存活下来,并被移到Survivor区时,年龄被设置为1。
-
如果对象继续在Survivor区存活下来,则年龄每次GC后加1。
-
-
晋升老年代的条件:
-
固定年龄:默认情况下,当对象的年龄达到15岁(该值可以通过
-XX:MaxTenuringThreshold
调整),就会被晋升到老年代。 -
动态年龄判定:如果Survivor区中所有对象的大小总和超过了Survivor区的50%,那么这些对象中年龄较大的那一部分将直接晋升到老年代,而无需等到最大年龄。
-
通过动态对象年龄判定机制,JVM可以更智能地决定哪些对象应该晋升到老年代,从而优化垃圾回收的性能和内存的使用效率。
对象创建
当在 Java 程序中通过 new
关键字创建一个对象时,会发生以下步骤:
-
类加载检查:JVM 首先检查这个类的字节码是否已经被加载、链接和初始化。如果没有,JVM 会通过类加载器加载并初始化这个类。
-
内存分配:JVM 为新对象分配内存。这个内存分配过程可能涉及到“指针碰撞”或“空闲列表”这两种方式,具体取决于 JVM 的实现和内存的规整情况。
-
零初始化:在内存中分配足够的空间后,JVM 会将对象中的所有字节初始化为零(即
0
)。对于对象引用,这意味着它们会被初始化为null
。 -
设置对象头:JVM 设置对象头(Mark Word),包括对象的哈希码、GC 分代年龄、锁状态等信息。
-
调用构造方法:执行对象的构造方法(
<init>
方法),进行进一步的初始化。口语化表达
-
对象创建:
-
当我们用
new
关键字创建一个对象时,JVM 首先得确保这个类的字节码已经加载并且准备好了。 -
然后,JVM 会在堆内存中为新对象分配空间。这个过程有点像在停车场找车位。
-
分配完空间后,JVM 会把对象的所有字节初始化为零,就像把一张白纸准备好等着写东西。
-
接下来,JVM 会设置对象头,这就像是给对象贴上标签,记录一些重要信息。
-
最后,调用对象的构造方法,完成对象的初始化。
-
-
对象的访问:
1. 句柄访问(Handle Access)
优点:
-
稳定性:对象移动(如在垃圾回收时)不会影响到引用,因为引用指向的是句柄,而不是对象本身。句柄中包含了对象的地址,因此引用不需要改变。
-
延迟绑定:可以在某些操作(如懒加载)中延迟解析对象地址,提高性能。
缺点:
-
额外的间接层:访问对象时需要两次引用(一次是句柄,再一次是对象地址),这可能会略微增加访问延迟。
-
内存占用:句柄本身需要额外的内存空间。
2. 直接指针(Direct Pointer)
优点:
-
性能:访问对象不需要经过额外的间接层,直接通过引用就可以访问对象,减少了访问延迟。
-
简单高效:实现简单,不需要额外的句柄结构。
缺点:
-
不稳定性:当对象在内存中移动(如垃圾回收时的压缩)时,引用可能需要更新,这会增加额外的开销。
-
垃圾回收的挑战:在某些垃圾回收算法中,如标记-清扫或标记-整理,对象移动可能导致引用失效,需要更新引用。
应用场景
-
句柄访问:适用于对稳定性要求较高的环境,如需要频繁移动对象的垃圾回收场景。
-
直接指针:适用于性能要求较高的场景,尤其是在对象创建和销毁频繁,但垃圾回收不频繁更新引用的情况下。
总结
选择哪种方式取决于 JVM 的实现和应用的具体需求。现代 JVM 通常使用直接指针方式,因为它提供了更好的性能,尤其是在对象访问频繁的情况下。然而,句柄访问在某些特定的垃圾回收场景下仍然有其优势,尤其是在需要频繁移动对象的情况下。
引用类型总结
JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1. 强引用(Strong Reference)
-
定义:最常见的引用类型,如
Object obj = new Object();
中的obj
。 -
GC 行为:只要存在强引用,垃圾回收器永远不会回收被引用的对象。
2. 软引用(Soft Reference)
-
定义:通过
java.lang.ref.SoftReference
类实现的引用,用于实现内存敏感的高速缓存。 -
GC 行为:当内存不足时,垃圾回收器会考虑回收软引用对象,以释放内存。
3. 弱引用(Weak Reference)
-
定义:通过
java.lang.ref.WeakReference
类实现的引用,用于跟踪对象而不阻止垃圾回收。 -
GC 行为:只要垃圾回收器发现了弱引用,不管当前内存空间足够与否,都会回收其指向的对象。
4. 虚引用(Phantom Reference)
-
定义:通过
java.lang.ref.PhantomReference
类实现的引用,用于跟踪对象被垃圾回收的活动。 -
GC 行为:无法通过虚引用访问对象,它唯一的作用是接收对象被垃圾回收器回收的通知。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。