美文网首页
jvm简单研究及垃圾回收

jvm简单研究及垃圾回收

作者: nich | 来源:发表于2022-01-18 15:26 被阅读0次

jvm是一种规范,java程序执行过程,.java文件通过javac编译成.class文件,然后jvm将其加载到方法去,执行引擎将会执行字节码文件,调用操作系统函数。简单概括就是java文件-》编译器-〉字节码-》jvm-〉机器码。

jvm是个翻译机器,jre提供了很多类库,就是jar包。jdk就是用来调试代码,打包代码反编译什么的,他还提供了很多小工具

一般常用的jvm虚拟机有hotspot等

jvm内存区域

java虚拟机在运行java的时候会把他管理的内存划分不同的数据区域

JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等

按照与线程关系也可以这么划分区域


数据区

直接内存就是没有被虚拟机化操作系统使用的其他内存,比如jvm可能会使用开工使用这一块内存

虚拟机栈

栈数据结构:先进后出原则


image.png

执行main方法,然后执行方法1,2,3出栈的时候是321
虚拟机栈的作用:存储线程的数据,指令和返回地址
虚拟机栈的生命周期跟线程有关
栈帧:Java的一个方法调用就相当于一个栈帧
栈帧基本包含四个区域:
1.局部变量表:放局部变量,如果是对象就放引用地址
2.操作数据栈:用来操作的,任意java数据类型
3.动态连接
4.返回地址:正常情况下,调用程序计数器中的地址返回

程序计数器

很小的内存空间,主要使用来记录线程执行的字节码地址。啥循环异常跳转都依赖他,记录运行的指令

本地方法栈

本地方法栈和虚拟机栈非常相似,服务对象是native方法,在hotspot中本地方法栈和虚拟机栈合二为一

方法区

运行时常量池,字段和方法数据,构造函数和普通方法等等等

堆是jvm最大的内存区域,几乎所有对象都在这里存储
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。
当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

溜代码

public class JvmTest {
    public final static String name = "111";
    private static int test = 1;
    JvmTest jvmTest = new JvmTest();

    public static void main(String[] args) {
        JvmObject jvm = new JvmObject();
        jvm.age = 1;
    }

}

class JvmObject{
    int age;
    int name;

    public int getAge() {
        return age;
    }
image.png

用代码简单看下虚拟机栈的执行过程


image.png
public static void main(String[] args) {
        getCount();
    }

    private static int getCount(){
        int i = 10;//首先把i=10移到操作数栈,然后移到局部变量表,程序计数器记录
        int j = 20;//把j=20移动到操作数栈,然后移动到局部变量表,程序计数器记录
        return  i+j;//iload i  和iload j 做 iadd操作 ,丢到布局变量表,然后在iload,然后安全出口跑出来,在期间程序计数器记录
    }

jvm运行流程:JVM在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法,过程中创建的对象放在堆中

堆又分新生代和老年代,新生代又分为Eden 和 Survivor 区 survivor又分为from ,to。

内存溢出

1.栈溢出,方法调用方法跑
2.堆溢出,某些对象生命周期太长,垃圾回收不了
3.方法区溢出,常量爆炸了,方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置
4.本机直接内存溢出,一般不会

对象分配

jvm中对象创建过程
1.类加载
2.检查加载
3.分配内存
4.内存空间初始化
5.设置
6.对象初始化

检查加载

当java方法调用new的时候一般会检查这个指令的参数是否能在常量池中找到这个类的符号引用,检查这个类是否已经被加载解析和初始化过(符号引用就是比如你在family对象里面有个son对象但在编译family时候并不知道son的实际内存地址,所以只能用符号引用代替,当装载器装在family的时候,通过虚拟机获取son实际地址,符号引用就可以被替换成实际地址)

分配内存

为对象在堆中分配一块内存地址

指针碰撞

当你堆中内存是完整的,一边是用过的内存,一边是没有用过的内存,这时候分配内存会把指针向空闲的空间移动和对象大小相等的距离,就叫指针碰撞

空闲列表

如果堆中内存不是完整的,那就没办法就行指针碰撞,虚拟机就会维护一个列表,记录哪些内存块是可用的,然后找到一块空间给对象实例,并更新列表上的记录

具体是哪种分配方式,是由堆空间是有规整和是哪种垃圾回收器决定的

当然这种情况是线程不安全的
解决这个问题有两种方案,第一种cas保证操作的原子性。第二种是分配缓冲,每个线程在堆中指定大小的一块空间

内存空间初始化

内存空间分配完成后,虚拟机会将内存空间初始化,就是基础类型默认值boolen为false啥的

设置

设置对象

对象初始化

执行new指令之后会按照构造函数初始化

对象的内存布局

在hotspot虚拟机中对象在内存中的布局分为(对象头,实例数据,对齐填充(起到占位作用))。。。对象头分为(存储对象自身的运行时数据,类型指针,若为数据还有记录数组长度数据),运行时数据还分为(哈希码,gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳)

对象访问定位

通过栈上的reference数据来操作主要是访问句柄和直接指针

句柄:reference中寸的句柄地址,堆中开辟一块空间存的是实例数据和地址
直接指针:存储的直接是对象地址

可达性分析算法

来判断对象是否存活,这个算法是通过gc roots对象作为起点,向下搜索,搜索的路径叫做引用链,当一个对象到gcroots没有任何引用链就代表对象不可用
gc roots对象主要包括下面重点是四种
1.虚拟机栈中引用的对象,就是局部变量表中引用的对象
2.方法区的静态变量和字符串常量池的引用
3.本地方法栈jni引用的对象,一般说的是native引用的对象
4.jvm内部引用
5.同步所持有的对象
6.其他

class对象回收必须同时满足很多条件
1.类实例都回收了
2.加载类的classload回收了
3.该类对呀的java.lang.class对象没有引用无法通过反射调用方法

Finalize 方法
通过算法来判断不可达对象,还可以拯救下,如果对象覆盖了Finalize方法,我们还可以通过这个方法拯救下

各种引用

强引用 =new
软引用softreference 内存溢出之前会被回收
虚引用weakreference在下次垃圾回收时候就会被回收
虚引用PhantomReference 随时被回收,主要是可以用来监控垃圾回收器是否正常工作

对象分配策略

逃逸分析:就是一个方法里面的对象有没有被外部方法引用
逃逸分析前提是必须出发jit执行

解释执行:指令按照顺序执行
jit执行:调用次数很高的代码

比如你一个for循环重复调用new 对象,当你开启逃逸分析时候,他创建的对象会直接在栈上分配,没开启还是会在堆上分配,频繁触发gc回收影响性能。

对象一般情况下都在新生代的eden区分配,当没有足够内存会发生minor gc,当发生一次minor gc后仍然活着会到survivor里面,对象里面的gc分代年龄+1,每熬过一次都会+1直到并发垃圾回收器默认的参数次数后,会到老年代(特殊点是大数据一般会直接到老年代,避免大内存分配,提前进行垃圾回收),但有种情况是survivor中相同年龄对象大小占空间的一半,大于等于他年龄的对象会直接跑到老年代,不需要等到默认参数次数

空间分配担保
老年代最大连续空间大于新生代所有对象总空间,说明minor gc是安全的,如果不成立,看下设置参数是否可以担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历 次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小 于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。

分代回收理论

对象区域分配一般分为新生代(eden,from,to),老年代

垃圾回收一般分为新生代回收,老年代回收,整堆回收

垃圾回收算法

复制算法:将内存划分为相同大小的两块,一块内存用完了,会将还活着的对象复制到另一块上面,清理掉前一块,但要注意对应引用需要调整,这种方法试用用新生代。

appel式算法

分配一块较大eden区和两块比较小的survivor空间,当回收时候,会把edea和survivor还活着的对象一次性到另外一块survivor上面,然后清理掉之前两块,当survivor内存不足,需要依赖老年代进行分配担保

标记清除算法

扫描所有对象标记需要回收的对象,扫描所有被标记的对象回收,后果可能产生大量不连续碎片,试用用老年代,不暂停

标记整理算法

就是先标记,然后移动所有存活的对象,然后清理,同时需要全程暂停用户线程才能进行,同时所有引用 对象的地方都需要更新(直接指针需要调整)。

jvm垃圾回收器

stop the world
单线程进行垃圾回收的时候,必须暂停所有用户线程,所有jvm团队一直努力在消除stw时间

serial/serial old

单线程垃圾回收器
新生代复制算法,老年代标记整理算法

Parallel Scavenge /old

新生代复制算法,老年代标记整理算法,并行的多线程回收器

关注吞吐量的垃圾回收器,吞吐量意思就是,jvm运行了100分钟,垃圾回收花掉了一份力,那么吞吐量就是99%

ParNew/cms

新生代复制算法,老年代标记清除算法

cms
image.png

1.初始标记:仅标记gc roots能直接关联到的对象速度很快
2.并发标记:和用户线程并发处理,进行gc roots追踪的时候,关联的所有对象进行可达分析路径对象,这个时间比较长
3.重新标记:为了修正并发标记阶段因为用户线程标记改动的那一部分对象标记记录
4.并发清楚:就是清除咧

在并发标记的时候,其实也进行了预清理和并发可终端预清理

预清理:如果在并发的时候在eden中分配了一个对象指向了老年代的b那么老年代的对象内部引用发生了变化,会把那个对象所在的crad标记成dirty,通过扫描这些table重发标记

并发可中断预清理:就是预清理不会一直执行下去,当最多循环次数,活着执行时间到达了阀值,新生代eden区内存的使用率达到了阀时都会退出循环

这种垃圾回收器缺点
cpu敏感,采用了并发要求比较高

浮动垃圾:因为是并发清理的,用户线程运行,不断产生垃圾,一部分垃圾产生没有gc回收,只能到下次gc回收再清理,消耗了老年代一部分内存

标记清除算法,会产生空间碎片

垃圾回收器退化:如果晋级的大对象找不到连续的空间存放,cms会退化使用serial old进行标记整理算法处理,暂停用户线程进行垃圾回收

jvm调优,扩容
当你扩容了,minorgc间隔时间变成了,当你对象a生命周期小于间隔时间,那么直接进行回收了,不会经过复制算法,那么单次gc时间就短了

jvm如何避免minorgc进行全堆扫描

当出现跨代访问的时候,老年代引用卡表,jvm一旦出现这种情况,会标记了老年代的卡表,扫描的时候只要扫描那些有问题卡表里面对象就行了

String字符串研究

看string源码,String对象都是由final修饰的,代表不可变,那么说明他是放在常量池中的,这样保证了string安全性

String I = add
String l = new String("add")
区别
上一个是直接在常量池地址add运行时直接返回常量池中的字符串引用,下一个String j = add的话就可以直接返回地址就行了

new String的话会在堆中创建String对象

intern()

相关文章

网友评论

      本文标题:jvm简单研究及垃圾回收

      本文链接:https://www.haomeiwen.com/subject/udrkhrtx.html