文章目录
内存结构
1、程序计数器(寄存器)
作用:
存储下一条JVM指令的执行地址
特点:
(1)线程私有的 (2)jvm内存中唯一不会存在内存溢出的结构
场景举例:
比如在java多线程运行情况下,线程的执行过程中是受CPU时间片调度的,当线程1的时间片用完就会切到线程2执行,那么这时线程1需要记住下一条指令执行到哪里了,在切换时线程1就会将下一条指令的地址存储在程序计数器中。这也能解释为什么程序计数器是线程私有的了。
2、虚拟机栈
- 每个线程运行时所需要的内存称为虚拟机栈
- 虚拟机栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在运行的方法
特点:
(1)当栈帧过多时会存在内存溢出,比如递归调用 (2)当栈帧过大时会存在内存溢出 (3)属于线程私有
常见问题:
1、方法内的局部变量是否安全?
不一定,如果该局部变量是一个引用变量,则可能存在逃离方法的作用范围,则存在线程安全问题。
2、内存占用过高,排查常用指令?
(1)用ps -ef |grep 进程名,可以根据进程名模糊匹配出进程 相关信息
(2)用jstack 进程id,找到线程查看相关信息
3、本地方法栈
4、堆区
特点
- 通过new关键字创建的对象都在堆区
- 堆区是属于线程共享的
- 堆区具有垃圾回收机制
堆区诊断,常用的工具
- jsp 工具:用来查看系统中具有哪些java进程
- jmap工具:用来查看内存占用情况,jmap - heap 进程id
- jconsole工具:图形界面工具
5、方法区
JDK1.8以前的方法区在逻辑上是在堆区的永久代实现的,如图
JDK1.8以后没有方法区的概念了,是在本地内存上实现了一个元空间,和方法区功能基本一致,如图:
常量池:
当代码被编译为.class文件后,.class文件里面就会产生一张常量池表,虚拟机指令根据这张表找到将要执行的类名,方法名,参数类型,字面量等
运行时常量池:
当class文件被加载到内存中后,常量池就成了运行时常量池,并把常量池中的符号地址变成真实地址。
StringTable(字符串常量池)
- 常量池中的字符串仅仅是一个符号,当第一次用到时(当方法被活动栈帧指向时)才会被创建成对象,并将这个对象存储在StringTable中
- StringTable不允许放入重复对象,因此这个机制避免了创建重复对象,节约了内存
- 字符串拼接是重新创建了StringBuilder对象,如下代码:
String s1 = "a"; // 在常量池中为符号a,当执行当前行对应的指令后,会产生对象"a",存储在StringTable
String s2 = "b";
String s = s1+s2; //这里是创建了一个StringBuilder对象s,并存储在堆内存中;
// new StringBuiler().append("a").append("b").toString();
- 字符串常量拼接,在编译期进行了优化,拼接后的对象是存储在StringTable中的,如下代码:
String s = "a" + "b"; //由于是字符串常量拼接,所以拼接后的对象是确定的,相当于编译期优化成了String s="ab",存储在StringTable中
- 可以使用intern方法,将没有放进串池的字符串对象放入串池
(1)jdk1.8,如果该字符串没有放入串池,则放入串池,并把串池的对象返回;如果串池中已经存在,则不会放入。
(2)jdk1.6,如果该字符串没有放入串池,则复制一份放入串池,再将串池中的对象返回;如果串池中已经存在,则不会放入。
例题:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
System.out.println(s3 == s6); //false
String x2 = new String("c") + new String("d"); String x1 = "cd"; x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 System.out.println(x1 == x2);
垃圾回收
堆内存的划分
在堆内存中,垃圾回收主要是分区进行的,堆内存划分为年轻代和老年代,年轻代划分为伊甸园区,幸存区1和幸存区2这三个分区,其中年轻代和老年代的容量比为1:3,伊甸园区和两个幸存区的容量比为8:1:1。
minor gc/young gc
是指在年轻代发生的垃圾回收,当伊甸园区的容量不足时,会触发minor gc,其过程是使用标记复制法先将伊甸园区存活的对象放到幸存区1,然后清空伊甸园区,此时对象的年龄加1;当下次minor gc时,会使用标记复制法同时标记伊甸园区和幸存区1中存活的对象,然后将其放入到幸存区2,并清空伊甸园区和幸存区1,此时幸存区1和幸存区2交换位置,对象的年龄再次加1。如此循环,当对象的年龄增加到15时就会放入到老年代。
full gc/major gc
是指在老年代发生的垃圾回收,full gc耗时久,一般是minor gc的10倍以上。full gc使用的是标记清除算法。
触发full gc的场景:
当年轻代的对象年龄达到15后自动进入老年代
当创建大对象时,对象的大小超过设置的阈值,对象会自动进入老年代,老年代容量不足则会发生full gc
当调用system.gc()时,可能会触发full gc,此方法会建议JVM进行full gc,但是jvm可以不接受建议
垃圾回收算法
标记清除
主要分为标记和清除两个阶段,首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。
缺点:(1)效率低,当需要回收的对象很多时,标记和清除两个步骤的效率都很低。
(2)容易产生碎片,当被标记的对象清除后,会产生很多不连续的碎片空间。
标记复制
是指将内存空间分为两个区域,当其中一个区域需要回收时,就将活着的对象复制到另一个区域中,再将原先的区域进行清除。
优点: (1)当存活的对象较少时,效率比较高,因为只需要将少部分存活的对象复制走,就可以全部进行清除。(2)不会产生内存碎片
缺点: 因为是将原有的对象一分为2,所以真正使用的内存空间减少了。
标记整理
分代算法
标记整理算
当存活的对象较多时,使用标记复制算法,复制的过程会相对耗时,效率低。而标记整理法是指将存活的对象移到一端,然后直接清理掉其他地方的内存。
分代算法
将堆内存分为年轻代和老年代,根据每个区域的特点使用不同的算法,这就是分代算法。
比如年轻代,需要回收的对象比较多,存活的对象相对少,就适合用标记复制算法。
而老年代,存活的对象相对比较多,考虑使用标记清除和标记整理法。
如何判断对象是否可以被回收?
引用计数法
对象被引用,则引用计数器加1;取消对对象的引用,则引用计数器减1。当引用计数器为0时则可以回收。
存在的问题: 当两个对象存在循环依赖时,两个对象的引用计数器都为1,则此时都不能被回收
可达性分析法
是指根据gc-roots对象往下进行追溯,当一个对象没有被任何gc-roots对象引用时,则说明此对象可以被回收。
可以作为gc-roots对象的对象:
虚拟机栈中存活的对象
方法区中的全局变量、静态对象
本地方法栈中的native对象