JVM内存管理
Java从编译到运行
-
Java程序运行过程
-
解释执行:
JVM C++语言,main方法 sout… ->
C++解释器
if (new) {
C++ 代码完成Java中的new
}
相对 经过JVM的翻译 速度慢一点
- JIT执行(hotspot):
java代码 翻译成 汇编码(codecache) 机器码(1010101)
速度比较快
-
JVM是一种规范
-
JVM的跨平台
-
JVM的语言无关性
-
-
常见的JVM实现
JVM的内存区域
运行时数据区域
-
定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
-
类型:线程私有与线程共享
8g机器,jvm启动,若运行时数据区 5G 剩余3G
Java中 通过某个方法 申请。使用,释放。—直接内存。
Java方法的运行与虚拟机栈
-
虚拟机栈
存储当前线程运行Java方法所需的数据,指令,返回地址
- 大小限制-Xss
- 栈溢出
-
栈帧
public class MethodAndStack {
public static void main(String[] args) {
A();
}
private static void A() {
B();
}
private static void B() {
C();
}
private static void C() {
}
}
当执行方法时,栈帧会入栈
当方法执行完了,栈帧就会出栈
-
程序计数器
指向当前线程正在执行的字节码指令的地址
-
虚拟机栈
存储当前线程运行方法所需的数据、指令、返回地址
- 栈帧 (一个方法对应一个栈帧)
- 局部变量表
- 操作数栈
- 动态连接
- 完成出口
- 大小限制-Xss
- 栈帧 (一个方法对应一个栈帧)
public class Person {
public int work() throws Exception {
int x = 1;
int y = 2;
int z = (x + y) * 10;
return z;
}
public static void main(String[] args) {
Person person = new Person();
Person.work();
person.hashCode();//方法数据本地方法 ---本地方法栈
}
}
栈帧执行对内存区域的影响
-
线程执行Java方法
-
main方法执行
-
work方法执行
-
栈帧入栈
-
字节码的执行细节
iconst:将数据(-1~5 int) 送到操作数栈
istore:进入局部变量表
iload:将局部变量表中对应下标的数据加载到操作数栈
iadd:加法(操作数栈的数据先出栈,计算,后将结果再压入操作数栈)
运算或算术指令对于两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈
bipush:对于较大的int(-1~5以外的int),使用bipush将数据送到操作数栈栈顶
imul:相乘
ireturn:将操作数栈的结果返回
为什么字节码的行号出现了不连续的情况?
0、1、2、3···· :字节码的偏移量。针对这个方法,有的指令大,偏移量大
为什么程序计数器只需要记录栈帧偏移量?
程序计数器内容可能会重复,虚拟机只会同时运行一个栈帧(栈顶的栈帧),不需要记录全局的地址,只需要记录栈帧的偏移量就行了,确保jvm多(单)线程运行的正常
**动态连接:**和多态有关
-
-
work方法执行完
- 栈帧出栈
java中直接操作不了线程,本地方法—操作系统,特殊的库,提供了一些接口。
运行时数据区中的其他区域
-
本地方法栈
Java虚拟机实现可能会使用到传统的栈(通常称之为"C Stacks")来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)。
注:
- 在hotspot中不区分jvm虚拟机栈和本地方法栈,使用同一块内存
- 程序计数器能记录虚拟机栈,但不能记录虚拟机栈的程序运行的字节码,始终为null
-
方法区
NIO中的DirectByteBuffer
永久代与元空间
运行时常量池
-
直接内存
NIO中的DirectByteBuffer
-
堆
import java.nio.ByteBuffer;
/**
* 代码的对象是如何在JVM中分配
*/
public class ObjectAndClass {
static int age = 18;//todo 静态变量 (基本数据类型)
final static int sex = 1;//todo 常量(基本数据类型)
final static ObjectAndClass object = new ObjectAndClass();//todo 成员变量指向(对象) 在类加载是不会执行
private boolean isKing;//todo 成员变量 放在哪里?
public static void main(String[] args) { //启动一个线程,创建一个虚拟机栈,数据结构,单个,压入一个栈帧
int x = 18;//todo 局部变量(基本数据类型)
long y = 1;//todo 局部变量(基本数据类型)
ObjectAndClass lobject = new ObjectAndClass();//todo 局部变量 引用 (对象)
lobject.isKing = true;//isKing根据对象,堆空间
lobject.hashCode();//方法中调用方法 本地方法(C++语言写 JNI)
ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1024);//todo 直接分配128M的直接内存
//这个地方 分配在哪里 128M
}
}
unsafe类:跨越了Java虚拟机的规范,绕过了内存虚拟化,不安全,通过反射的方法使用,能直接操作内存和对象
在框架、缓存、经典框架中都有使用 unsafe
直接内存申请问题:
绕过JVM的垃圾回收,手动的方式:
好处:速度稍微快点
缺点:明显,忘记 -> 内存泄漏
手动,多线程 覆盖
JVM的整体内存结构
方法区
- 大小设置
- 存放内容
Java堆
- 大小设置
- 存放内容
直接内存
- 大小设置
- 存放内容
栈区
- 大小设置
- 存放内容
深入理解JVM内存区域
- JVM申请内存
- 初始化运行时数据区
- 类加载
- 执行方法
- 创建对象
/**
* VM参数
* -Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m -XX:+UserConcMarkSweepGC -XX:+UserCompressedOops
*/
public class JVMObject {
public final static String MAN_TYPE = "man";// 常量
public static String WOMAN_TYPE = "woman";// 静态变量
public static void main(String[] args) throws Exception {
Teacher T1 = new Teacher();
T1.setName("Mark");
T1.setSexType(MAN_TYPE);
T1.setAge(36);
Thread.sleep(Integer.MAX_VALUE);//线程休眠
for (int i = 0; i < 1; i++) {
System.gc();//主动触发GC 垃圾回收 15次--- T1存活
}
Teacher T2 = new Teacher();
T2.setName("King");
T2.setSexType(MAN_TYPE);
T2.setAge(18);
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
}
class Teacher {
String name;
String sexType;
int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSexType() { return sexType; }
public void setSexType(String sexType) { this.sexType = sexType; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
从底层深入理解运行时数据区
![image-20240123193401103](https://img-blog.csdnimg.cn/img_convert/56b6d2069fc061329352923589a862d7.png)
-
堆空间分代划分
-
GC概念
-
JHSDB工具
查看内存 虚拟+真实
深入理解什么是运行时数据区?
真实的内存地址进行虚拟化
HSDP工具怎么使用?
- 在jdk的lib文件夹中使用cmd输入一下指令:
java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
- 使用jps指令
列出正在运行的Java虚拟机的进程信息的命令行工具
jps 23296 Launcher 5360 HSDB 17316 Jps 14728 JVMObject 21404
10144 JVMObject — JVM的进程
-
附着
Heap Prarameters:可以查看堆的分代划分
Gen 0: 新生代
Eden
From
To
Gen 1:老年代
-
代码改造
-
JHSDB中查看对象
-
JHSDB中查看栈
新生代和老年代:
对于新生代和老年代的的回收机制会有不同
对象经历一次垃圾回收,没有被回收掉 age+1
age达到15 晋级老年代
为什么是15后晋级老年代?
底层记录对象的年龄 指端 4位二进制 1111 15
内存溢出
- 栈溢出
- 堆溢出
- 方法区溢出
- 本机直接内存溢出
对象的分配及垃圾回收机制
JVM中对象对的创建过程
![image-20240123193640759](https://img-blog.csdnimg.cn/img_convert/4eea9d59aaf6d8c7cc4caf775a33fe89.png)
-
检查加载
-
分配内存
-
划分内存方式
-
指针碰撞
堆空间规整
不需要进行查表,效率更高
-
空闲列表
堆空间中有内存碎片,不太规整
-
-
并发安全问题
-
CSA
可能造成线程空转
-
Thread Local Allocation Buffer(TLAB)
JVM默认方式
在Eden区给每一个线程分配一个缓冲区
-
-
-
-
初始化
- “零”值
-
设置
- 对象头
-
对象初始化
- 构造方法
对象的内存布局:
对象的访问定位:
-
使用句柄
-
直接指针
-
判断对象的存活
- 引用计数算法
- 可达性分析(根可达)
**常见GC roots(GC RootSet):**静态变量、线程栈变量、常量池、JNI(指针)
除了这四种还有其他的吗?
内部引用:如class对象、异常对象Exception、类加载器
同步锁:synchronized对象
内部对象:JMXBean等
临时对象:跨代引用
-
Class回收条件
回收条件比较苛刻
- class new出的所有对象都回收掉了
- 对应的内加载器也要被回收掉
- 类 Java.lang.class对象,在任何地方没有被引用,并且无法通过反射调用这个类的方法
- 参数控制
-Xnoclassgc
-
Finalize
finalize()
方法是Object类的一个方法,它将在垃圾收集器删除对象之前被调用。这给了一个对象在被垃圾收集之前清理自身的机会
Finalizer方法优先级很低,需要等待(会另起一个线程调用Finalizer
方法),不推荐使用
下面是重写finalize方法的一个示例
@Override
protected void finalize() Throwable {
super.finaize()
FinalizeGC.instance = this; //把引用接上
}
由于Finalizer方法优先级很低,下面代码中使用了线程休眠
各种引用
![image-20240124091729766](https://img-blog.csdnimg.cn/img_convert/3c57806f5784ee0bdb4dbe053f59373e.png)
-
强引用 =
只要强引用还存在,垃圾收集器就永远不会回收被引用的对象,例如
Object obj = new Object()
-
软引用 SoftReference
软引用是用来描述一些有用但并非必需的对象。只有在内存不足时,垃圾收集器才会回收这些对象
-
弱引用 WeakReference
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止
-
虚引用 PhantomReference
虚引用是最弱的一种引用关系。它不能单独使用,也不能通过它访问对象,它的唯一用处就是在这个对象被收集器回收时收到一个系统通知
对象的分配策略
对象的分配原则
几乎所有的对象都在堆中进行分配
-
对象优先在Eden分配
-
空间分配担保
Java的垃圾回收器在进行Minor GC时,需要有足够的内存空间来存放新生代Survivor区中存活的对象。如果在Minor GC后,Survivor区的空间不足以容纳所有存活的对象,那么这些无法放入Survivor区的对象将会被直接晋升到老年代。这就是所谓的"空间分配担保"。 这种策略的目的是为了确保Minor GC可以顺利进行,避免因为Survivor区空间不足而导致的系统崩溃。在进行Minor GC之前,垃圾回收器会检查老年代连续空闲的最大内存是否大于新生代所有对象总空间,如果条件满足,那么Minor GC可以安全进行,否则,垃圾回收器会先进行一次Full GC。
-
大对象直接进入老年代
同时满足下面:
大对象必须超过老年代的一半。
这个值可以通过参数
-XX:PretenureSizeThreshold
来设定。当对象的大小超过这个设定值时,就会直接在老年代中进行分配。
默认情况下,这个阈值是0,即所有的对象都在新生代中分配。
垃圾回收器 serial parnew这两款才生效
-
长期存活的对象进入老年代
-
动态对象年龄判定
在Java的内存模型中,堆内存被分为新生代和老年代。新创建的对象首先被分配到新生代,当新生代空间不足时,垃圾回收器会进行一次Minor GC,清理掉新生代中不再使用的对象,这个过程中,存活的对象会被移动到Survivor区,并且这些对象的年龄会加1。当对象的年龄达到一定阈值(默认15)时,这个对象就会被晋升到老年代。 然而,如果Survivor区空间不足以容纳这些存活对象,或者有一些对象的年龄虽然没有达到默认的阈值,但已经达到了Survivor区的一半,那么这些对象也会被直接晋升到老年代,这就是动态年龄判断。
虚拟机的优化技术
-
逃逸分析 + 触发JIT(热点数据)
循环次数足够大,触发了JIT,同时经过逃逸分析(其他线程无法调用到这个对象),会触发栈上分配
-
本地线程分配缓存
垃圾回收基础知识
-
什么是GC
GC,全称垃圾回收(Garbage Collection),是Java内存管理的一部分。
它自动回收程序中不再使用的内存,以防止内存泄漏和浪费。
在Java中,当一个对象不再被引用时,它就成为了垃圾,可以被回收。垃圾回收器会自动找出这些垃圾对象,并释放它们占用的内存。
-
分代回收理论
分代回收(Generational Garbage Collection)是Java垃圾回收的一种主要策略。其基本思想是将Java堆分为新生代和老年代,这样可以根据各个年代的特点选择合适的垃圾回收算法。
新生代:新创建的对象首先被分配到新生代。新生代又被分为Eden区和两个Survivor区(S0和S1)。大部分情况下,新创建的对象都在Eden区。当Eden区满时,会触发一次Minor GC,清理掉Eden区中不再使用的对象,存活的对象会被移动到Survivor区,如果Survivor区也满了,就会被移动到老年代。
老年代:存活时间较长的对象会被移动到老年代。当老年代满时,会触发一次Major GC或Full GC,这种GC会暂停用户线程,所以应尽量避免。 分代回收的理论基础是:大部分对象都是朝生夕死的(即,很多对象创建后很快就变得不可达),少数生命周期长的对象会被移到老年代。
因此,通过分代,我们可以用不同的回收方式来处理新生代和老年代,使得垃圾回收更加高效。
新生代主要采用复制算法,每次垃圾回收时,都将存活的对象复制到Survivor区,然后再清理掉Eden区和另一个Survivor区的所有对象。这样可以快速清理大量短生命周期的对象。
老年代主要采用标记-清除-整理算法,标记出所有存活的对象,然后清除掉所有未被标记的对象,最后进行一次内存整理,将存活的对象向一端移动,清理掉端的空间。这样可以处理生命周期长的对象。
这种分代回收的方式,可以大大提高垃圾回收的效率,减少程序的暂停时间。
-
GC分类
Java的垃圾回收(GC)主要可以分为以下几种类型:
-
Minor GC:这是最常见的垃圾回收。它发生在新生代(Young Generation)的Eden区,当Eden区满时,就会触发Minor GC。这种GC会清理掉Eden区中不再使用的对象,存活的对象会被移动到Survivor区,如果Survivor区也满了,就会被移动到老年代。
-
Full GC:这种垃圾回收会清理整个Java堆,包括新生代和老年代。Full GC通常会在老年代空间不足或者系统内存资源紧张时触发。由于Full GC会暂停所有的用户线程,所以它的执行时间通常比较长,应尽量避免。
-
Major GC:这种垃圾回收主要发生在老年代。它通常会在Minor GC后,Survivor区的对象无法放入老年代时触发。Major GC的执行时间通常比Minor GC长,但比Full GC短。
-
CMS(Concurrent Mark Sweep)GC:这是一种以获取最短回收停顿时间为目标的垃圾回收器。它允许垃圾回收线程和用户线程同时工作,但是在标记和清除阶段仍然需要停顿用户线程。
-
G1(Garbage-First)GC:这是一种面向服务器的垃圾回收器,它可以处理大内存(大于4GB)的系统,且能保证GC停顿时间的可预测性,即可以明确指定GC的停顿时间。
以上就是Java中常见的垃圾回收类型,不同的垃圾回收器适用于不同的场景,需要根据实际情况选择合适的垃圾回收器。
-
垃圾回收基础知识——复制算法(Coping)
复制算法是Java垃圾回收中常用的一种算法,主要应用于新生代的垃圾回收,特别是在Eden区和Survivor区。
复制算法的基本思想是将内存分为两个相等的区域,每次只使用其中一个区域。当这个区域内存用完时,就将还存活的对象复制到另一个区域中,然后再把已使用过的内存空间一次性清理掉。这样就把内存的回收和整理合二为一,每次垃圾回收后都能得到一块连续的内存空间。
- 特点
- 实现简单,运行高效
- 没有内存碎片
- 利用率只有一半,需要两块相等的内存空间进行交换,且总有一块内存是闲置的
Appel式的复制回收算法
在Java的新生代中,内存被分为一块较大的Eden区和两块较小的Survivor区(S0和S1)。每次新创建的对象都首先在Eden区分配,当Eden区满时,会触发一次Minor GC。GC过程中,Eden区中还存活的对象,以及从上次GC后Survivor区中存活的对象,会被复制到另一个Survivor区(S0和S1会在每次GC后交换角色)。
- Eden区的来源
- Appel式回收
- 提高空间利用率和空间分配担保
垃圾回收基础知识——标记-清除算法(Mark-Sweep)
标记-清除算法是垃圾回收的一种基本策略,主要用于老年代的垃圾回收。它包括两个阶段:标记阶段和清除阶段。
标记阶段:在这个阶段,垃圾回收器会遍历所有的对象,对所有存活的对象进行标记。一个对象被视为存活的,如果它被其他对象引用,或者它是一个正在执行的方法的局部变量或参数
清除阶段:在这个阶段,垃圾回收器会遍历所有的对象,清除所有未被标记的对象,回收它们占用的内存。
标记-清除算法的优点是实现简单,且不需要移动存活的对象。
但是,它有两个主要的缺点:
效率问题:标记和清除两个过程的效率都不高。
空间问题:标记清除之后会产生大量不连续的内存碎片,空间利用率不高。当需要分配较大对象时,可能会找不到足够的连续内存,即使堆空间是足够的。
- 特点
- 位置不连续,产生碎片
- 可以做到不暂停
垃圾回收基础知识——标记-整理算法(Mark-Compact)
- 特点
- 没有内存碎片
- 指针需要移动