JVM系列——JVM内存结构&垃圾回收

内存结构

在这里插入图片描述

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对象

JVM中的永久代会发生垃圾回收吗?元空间会吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值