【 java 虚拟机知识 第一篇 】
目录
1.内存模型
1.1.JVM内存模型的介绍
1.2.堆和栈的区别
1.3.栈的存储细节
1.4.堆的部分
1.5.程序计数器的作用
1.6.方法区的内容
1.7.字符串池
1.8.引用类型
1.9.内存泄漏与内存溢出
1.10.会出现内存溢出的结构
1.内存模型
1.1.JVM内存模型的介绍
内存模型主要分为五个部分:虚拟机栈,本地方法栈,堆,方法区(永久代或元空间),程序计数器,当然还有一部分是直接内存。
虚拟机栈:每个线程各有一个,线程独有,当执行方法(除了本地方法native修饰的方法)之前,会创建一个栈帧,栈帧里面包含局部变量表和操作数栈和动态链接和方法出口等信息,而每个栈帧就是存入栈中
本地方法栈:每个线程各有一个,线程独有,当执行本地方法时类似于虚拟机栈,一样会创建栈帧,存入对应信息
程序计数器:每个线程各有一个,线程独有,它的作用是记录当前线程下一次执行的二进制字节码指令地址,如果执行的是本地方法那么它会记录为定值(null)
堆:所有线程共享,堆的回收由垃圾回收机制管理,堆中主要存入对象实例信息,类信息,数组信息,堆是JVM内存中最大的一个
永久代:在jdk1.7及以前是方法区的实现,使用的是jvm内存,独立于堆,主要存入类信息,静态变量信息,符号引用等信息
元空间:在jdk1.8及以后是方法区的实现,使用的是本地内存,主要存入类信息,静态变量信息,符号引用等信息
直接内存:该内存属于操作系统,由NIO引入,操作系统和Java程序都可以进行操作,实现共享
----
常量池:属于class文件的一部分,主要存储字面量,符号引用
----
运行时常量池:属于方法区,其实就是将常量池中的符号引用替换成了直接引用,其余一样
1.2.堆和栈的区别
五个点:用途,生命周期,存储速度,存储空间,可见性
用途:栈主要存储方法返回地址,方法参数,临时变量,每次方法执行之前会创建栈帧,而堆存储对象的实例信息,类实例信息,数组信息
生命周期:栈的生命周期可见,每次方法执行完栈帧就会移除(弹出),而堆中的数据需要由垃圾回收器回收,回收时间不确定
存储速度:栈的速度更快,栈保持"先进后出"的原则,操作简单快,而堆需要对对象进行内存分配和垃圾回收,并且垃圾回收器本身运行也会损耗性能,速度慢
存储空间:栈的空间相对于堆的空间小,栈的空间小且固定,由操作系统管理,而堆的空间是jvm中最大的,由jvm管理
可见性:栈是每个线程都有的,而堆是所有线程共享的
1.3.栈的存储细节
如果执行方法时,里面创建了基本类型,那么基本类型的数据会存入栈中,如果创建了引用类型,会将地址存入栈,其实例数据存入堆中
1.4.堆的部分
堆主要分为两部分:新生代,老年代,它的比例:1:2
新生代:新生代分为两个区:伊甸园区和幸存者区,而幸存者区又平均分为S0和S1区,伊甸园区与S0与S1之间的比例:8:1:1,每次新创建的对象实例都会先存入伊甸园区,它们主要使用的垃圾回收算法是复制算法,当伊甸园区的内存使用完时,会使用可达性分析算法,标记不可存活的对象(没有被引用的对象)将存活对象复制移入S0或S1中,这个过程叫Minor GC,如果这次移入的是S0,那么下次就会将伊甸园区和S0中的对象移入S1中,循环反复,每经历一次Minor GC过程就会给对象年龄加一,直到大于等于15时,会认为该对象生命周期长,移入老年代中
细节:其实新创建的对象不会直接存入伊甸园区,如果多线程情况下同时进行存入对象(线程竞争压力大)会导致性能的损失,因此会给每个线程从伊甸园区中先申请一块TLAB区域,先将对象存入该区,如果该区内存使用完,会重写申请或直接存入伊甸园区
老年代:老年代就是存储生命周期长的对象(不经常回收的对象),主要使用的垃圾回收算法为标记清除算法或标记整理算法,看场景出发,其中老年代还包含一个大对象区
大对象区:主要存储的就是新创建的大对象比如说大数组,会直接将该对象存入大对象区中,不在存入新生代
可达性分析算法:从GC Root出发找对应引用对象,如果一个对象没有被直接引用或间接引用,那么会被标记,GC Root可以是java的核心库中的类,本地方法使用的类,还未结束的线程使用的类,使用了锁的类
标记清除算法:对没有被引用的对象进行标记,然后进行清除(不是真正的清除,而是记录其对象的起始地址和结束地址到一个地址表中,下次要添加新对象时会先从表中找,找到一个适合大小的就会进行覆盖),清除:记录地址,新对象进行覆盖,好处:速度快,缺点:内存碎片化严重(内存不连续了,本来可以存入的对象存入不了)
标记整理算法:同理进行标记,然后再对可存活对象进行整理,最后清除,好处:避免了内存碎片化问题,缺点:速度慢
复制算法:将内存空间分为两份,一份存对象from,一份为空to,当要回收时,复制可存活对象移入为空的内存空间to中(移入既整理),然后对存对象的空间from整体清除,然后名称from和to换过来
为什么会有大对象区:因为伊甸园区的内存空间本身就不大,如果你直接创建一个大于它空间的对象,会出现问题,还有就是即使没有超过伊甸园区的空间,但是其对象依旧很大,频繁的复制移动很影响性能
1.5.程序计数器的作用
简单来说:线程1执行到某个地方时,线程2抢到了执行权,那么等到线程1执行时是不是需要知道上次执行到哪里了,所以程序计数器就是记录执行到哪里的,并且每次线程都需要有一个来记录
1.6.方法区的内容
方法区主要包含:类信息,静态变量信息,运行时常量池,即时编译器的缓存数据
1.7.字符串池
在jdk1.6及以前字符串池属于永久代,jdk1.7字符串池移入堆中但是还是属于永久代的,jdk1.8及以后还是存入堆中,但是不属于元空间了(1.7以前是永久代,1.8以后是元空间)
细节:String s1 = "a";它的过程是:先去字符串池中找,看是否能找到该字符,找到了直接复用池中地址,没有找到会先在堆中创建一个String对象,jdk1.6它会将数据复制一份重新创建一个新的对象存入池中,jdk1.7会将其地址复用给池中
String s2 = new("b");同理
String s3 = "a" + "b";常量进行相加,与new("ab")基本一致
String s4 = s1 + s2;变量相加,底层使用的是new StringBuilder.append("a").append("b").toString(),如果池中存在"ab",它也不会复用,而是直接创建,如果池中不存在,而不会将新创建的对象存入池中
1.8.引用类型
引用类型:强引用,软引用,弱引用,虚引用,(终结器引用)
强引用:比如new就是,只要有强引用指向对象,那么该对象永远不会被回收
软引用:如果出现内存溢出的情况,再下次GC时会对其回收
弱引用:每次进行GC过程都会进行回收
虚引用:每次进行GC过程都会进行回收
细节:这些都是对象,等级依次递减
软引用:创建一个软引用对象时你可以指定引用队列,如果不指定会导致软引用为null一个空壳,比如说出现了GC Root强引用软引用对象,导致软引用对象无法被回收,你想要其对象被回收,可以使用引用队列,简单来说就是出现了这种情况,将软引用对象存入队列中,下次GC会扫描队列进行回收,当然这是特殊情况,总结来说:软引用可以使用引用队列也可以不使用
public class SoftRefDemo {public static void main(String[] args) throws InterruptedException {// 1. 创建引用队列ReferenceQueue<Object> queue = new ReferenceQueue<>();// 2. 创建大对象(确保能被GC回收)byte[] data = new byte[10 * 1024 * 1024]; // 10MB// 3. 创建软引用并关联队列SoftReference<Object> softRef = new SoftReference<>(data, queue);// 4. 移除强引用(只保留软引用)data = null;System.out.println("GC前: ");System.out.println(" softRef.get() = " + softRef.get());System.out.println(" queue.poll() = " + queue.poll());// 5. 强制GC(模拟内存不足)System.gc();Thread.sleep(1000); // 给GC时间System.out.println("\nGC后: ");System.out.println(" softRef.get() = " + softRef.get());System.out.println(" queue.poll() = " + queue.poll());}
}
GC前: softRef.get() = [B@15db9742queue.poll() = nullGC后: softRef.get() = nullqueue.poll() = java.lang.ref.SoftReference@6d06d69c
弱引用:与软引用相同
WeakHashMap<Key, Value> map = new WeakHashMap<>();Key key = new Key();
map.put(key, new Value());// 移除强引用
key = null;System.gc();// GC后Entry自动被移除
System.out.println(map.size()); // 输出: 0
虚引用:最好的例子就是直接内存:它就是使用了虚引用,直接内存就是从操作系统中申请了一块空间来使用,因此GC是不能对其进行回收的,如果当强引用消失只剩下虚引用,那么会将虚引用对象存入引用队列中,等队列来执行本地方法释放直接内存
public class PhantomRefDemo {public static void main(String[] args) {// 1. 创建引用队列ReferenceQueue<Object> queue = new ReferenceQueue<>();// 2. 创建虚引用Object obj = new Object();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);// 3. 模拟直接内存分配ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MBSystem.out.println("GC前:");System.out.println(" phantomRef.get() = " + phantomRef.get()); // nullSystem.out.println(" queue.poll() = " + queue.poll()); // null// 4. 移除强引用(触发回收条件)obj = null;directBuffer = null; // 释放DirectByteBuffer强引用// 5. 强制GC(实际应用中会自动触发)System.gc();try { Thread.sleep(500); } catch (Exception e) {}System.out.println("\nGC后:");System.out.println(" phantomRef.get() = " + phantomRef.get()); // nullSystem.out.println(" queue.poll() = " + queue.poll()); // 返回phantomRef对象// 6. 实际效果:DirectByteBuffer分配的1MB堆外内存已被释放}
}
终结器引用:在所有父类Object中有一个终结器方法finalize()方法,如果重写该方法,那么执行GC之前会先执行该方法,当没强引用指向了,而这个对象还重写了finalize()方法,那么会将这个终结器引用对象加入队列中,下次GC时会先由队列来执行finalize()方法,但是指定执行的队列是一个优先级不高的队列,会导致资源释放缓慢
public class ResourceHolder {// 重写finalize方法(不推荐!)@Overrideprotected void finalize() throws Throwable {releaseResources(); // 释放资源super.finalize();}
}
1.9.内存泄漏与内存溢出
内存泄漏:就是说没有被引用的对象没有被回收,导致可用内存空间减少
比如:
- 静态集合没有释放:一直存在
- 线程未释放:线程应该执行完了,但是没有释放
- 事件监听:事件源都不存在了,还在监听
例子:使用对应的文件流,字节流,但是没有释放该流,就会导致内存泄漏
解决:释放流
内存溢出:就是说内存不足了
比如:
- 一直创建新对象
- 持久引用:集合一直添加但是没有被清除
- 递归
例子:ThreadLocal,每个线程都有一个ThreadLocal,本质就是每个线程存在一个ThreadLocalMap对象,key(弱引用)存入的是TreadLocal的实例,value(强引用)为自己指定的Object对象,如果没有使用该TreadLocal了,也就是说没有强引用指向TreadLocalMap对象,那么其中的key就会被设置为null,那如果该线程一直不结束,导致key不能被回收,随着key为null的情况增多就会导致内存溢出
解决:使用TreadLocal.recome();
1.10.会出现内存溢出的结构
会出现该问题的内存结构:堆,栈,元空间,直接空间