JVM学习,来源于狂神说

本文详细探讨了JVM的结构,包括类加载器的双亲委派机制、方法区、堆内存的各个分区以及垃圾回收算法。通过对JVM的工作原理解析,阐述了内存管理的重要性,特别是如何避免和解决内存溢出问题,以及JProfiler工具的安装和使用,帮助理解JVM的优化和故障排查。
摘要由CSDN通过智能技术生成

JVM的结构

在这里插入图片描述
在这里插入图片描述
详细的JVM架构
在这里插入图片描述

3、类加载器

作用:加载class文件
层级关系:应用程序加载器–>扩展类加载器–>启动类(根)加载器–>虚拟机自带的加载器
我们可以通过对Java中的架包中进行修改,达到公司的一些目的。这是可以做到的。

双亲委派机制

在这里插入图片描述
使用代码查看,上图中的关系。

public class Car {
    public static void main(String[] args) {

        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();

        System.out.println(car1.hashCode());
        System.out.println(car2.hashCode());
        System.out.println(car3.hashCode());

        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();

        System.out.println(aClass1.hashCode());
        System.out.println(aClass2.hashCode());
        System.out.println(aClass3.hashCode());

        ClassLoader classLoader = aClass1.getClassLoader();
        System.out.println(classLoader);

        System.out.println(classLoader.getParent());

        System.out.println(classLoader.getParent().getParent());

    }
}

结果:

1067040082
1325547227
980546781
603742814
603742814
603742814
jdk.internal.loader.ClassLoaders$AppClassLoader@15db9742
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7adf9f5f
null

在这里插入图片描述
在这里插入图片描述

个人理解:(理解还不透彻,可能有一些错误。不过感觉方向大致是对的)
我们定义好一个类时,通过编译器变为.class文件,该文件需要classloader才能装入虚拟机中进行运行。但是此时,双亲委派机制要检查该文件中的类时候已经被加载过。并且从最外层,也就是Appclassloader开始验证,这个类是否被加载过。依次递交到BootStrapClassLoader,如果以上的任意一环已经加载过,那么不再向上委派。如果委派到最上层依旧没被加载过,那么就就地开始加载。又开始依次向下去验证是否自己可以记载此类。
这么做的好处是,避免系统级别的原生代码被修改。举个例子,我们可以自定义一个String类,在加载这个类的时候,我们就开始向上委派,直到BootstrapclassLoader,发现这个类被BootstrapclassLoader加载过了,那么我们就直接使用这个被加载过的类进行实例化。就不会再走我们自定义的String类的中方法逻辑(表面上我们依旧可以定义String类,但是实际上底层的String已经被加载)。这样避免了修改底层代码。保证了一定的安全。

步骤总结

1、类加载器收到类加载的请求!
2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器!
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常。通知子加载器进行加载。
4、重复步骤3.

有趣的记录:java当初叫C++ - -,哈哈,去掉了指针。自动化了回收机制。
有趣的记录:Java中的native关键字,调用的操作系统级别的本地方法,一般是由C/C++写的

沙箱安全机制

nativa, 方法区

native:凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层C语言的库!!
注意事项:
1、会进入本地方法栈
2、待用本地方法本地接口 JNI
3、JNI的作用,扩展Java的使用,融合不同的编程语言为Java所用。比如最初的C,C++
4、Java诞生的时候,C,C++横行,要想立足,必须调用C,C++的程序
5、它在内存区域中,专门开辟了一个标记区域:native Method Stack,登记native 方法
6、在最终执行的时候,加载本地方法库中的方法通过JNI
7、Java程序驱动打印机,管理系统,掌握即可。在企业级应用中较为少见。

在这里插入图片描述

PC寄存器

程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

方法区(Method Area)

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关(主要包含:static,final,Class模板,常量池)
举个例子:
当我们创建一个类的时候,会在方法区中加载类的信息。此时如果对其属性进行了字面量的赋值初始化,其对应的值就会在方法区中的常量池中进行存储。一旦,实例化对象后,进行了重新的赋值。那么属性的值就会存储在堆空间中。
在这里插入图片描述

栈(一种数据结构)

程序=数据结构+算法:持续学习;
程序=框架+业务逻辑:吃饭~;
情景
我们的程序运行,我们的main()总是最先执行,最后结束。其实就是栈的应用,系统先将main()方法压入空栈,之后依旧压入其他的方法。等其他方法运行完后,弹出栈,main方法才被弹出。其中经常遇到“栈溢出”的问题,这可能就是方法栈被错误程序,混乱调用方法将栈压满了。

栈:栈内存,主管程序的运行,生命周期与线程同步;线程结束,栈内存也就释放。
对于栈来说,不存在垃圾回收问题。
一旦线程结束,栈就over。

栈存储:8大基本类型+对象的引用+实力的方法
栈运行的原理: 在栈结构的每一个“栈帧”中的结构:
程序正在执行的方法,一定在栈的顶部
在这里插入图片描述

堆(heap)

注意,不同的JVM中,堆的结构是不一样的。但是我们平时学习,使用的都是sun公司的。
SUN公司:Java HotSpot™ 64-Bit Server VM (build 13+33, mixed mode, sharing)
BEA : JRockit
IBM : J9 VM

一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量等,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域:

  1. 新生区
  2. 养老区
  3. 永久区
    在这里插入图片描述
    GC垃圾回收,主要是在新生区(伊甸园区)和养老区
    假设内存满了,就会报OOM(java.lang.OutOfMemoryError:java heap space),堆内存不够~~
    在JDK8以后,永久存储区改了个名字(原空间

新生区

  • 类:诞生和成长的地方,甚至是死亡;
    真理:经过研究,99%的对象都是临时对象!

在这里插入图片描述

永久区

这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或者类信息,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存!

一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;

  • jdk1.6之前:永久代(永久存储区),常量池在方法区中;
  • jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中;
  • jdk1.8之后:无永久代,常量池在元空间。方法区也在持久代中,也就是永久存储区。

图解:
在这里插入图片描述
持久代(元空间在堆空间中),但是由于其方法去又被很多新生对象所共享,所以又叫非堆
再细分:
在这里插入图片描述
注意:这个方法区在逻辑上存在,但是在物理上不存在。(因为新生代和老年代的空间加起来就已经等于了堆空间总和,所以这个元空间事实上又不在堆空间中。)

堆内存调优的问题:
假如以后报JVM的OOM问题,
-我们可以先通过编辑器将JVM的堆内存扩大看看,如果还报OOM问题,那么就应该是我们的代码的问题。
-分析内存,看一下哪个地方出现了问题。(使用专业工具)
(1)能够看到代码第几行出错:使用内存快照分析工具,MAT(Eclipse早年集成好的插件),Jprofiler(测试插件)
(2)Debug,一行行分析代码!
MAT,Jprofiler的作用:
1、分析Dump内存文件,快速定位内存泄漏;
2、获得堆中的数据;
3、获得大的对象;
。。。。。。等等

Jprofiler安装

首先我们需要在idea中安装我们的Jprofiler插件,setting–>plugins–>搜索Jprofiler,如下图:
在这里插入图片描述
在这里插入图片描述
记得重启idea~
在这里插入图片描述
之后工具栏就会出现该图标。此时还需要安装软件与该插件进行配合使用!

安装后,我们还需要去Jprofiler官网下载该软件:(直接百度即可)
在这里插入图片描述
下载后,傻瓜式下一步安装。
在这里插入图片描述
由于Jprofiler12的注册码没有找到,破解也不方便。所以先用免费的测试10天,用于体验,怎么排除OOM的错误。
下一步,将IDEA与Jprofile进行绑定
在这里插入图片描述
安装步骤,也可根据狂神的网站进行安装:
链接: https://www.kuangstudy.com/bbs/1369899213336367105.
很详细!!以及简单的使用!

测试代码:

package com.chaoxi.java;

import java.util.ArrayList;

/**
 * @author chaoxi
 * @create 2021-08-11 10:34
 * 无限的在动态数组中添加对象,肯定会报OOM错误~!!!!!
 */
public class Demo03 {
    public static void main(String[] args) {

        ArrayList<Demo03> list = new ArrayList<>();

        int count = 0;

        try {
            while (true){
                list.add(new Demo03());
                count++;
            }
        } catch (Exception e) {
            System.out.println("count"+count);
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
这种就是简单的代码不报错,但是就是错了的典型。
接下来,我们对JVM进行配置。让其,DUMP下对应的错误信息。
配置指令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-Xms:设置初始化内存分配大小
-Xmx:设置最大分配内存
-XX:PrintGCDetails :打印GC的一些计数信息,垃圾回收信息
在这里插入图片描述
再次运行,就可以看到dump下的文件。
在这里插入图片描述
找到,在项目根目录下,dump的文件。双击打开。
在这里插入图片描述
即可看到,Jprofiler对错误的分析。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

GC(垃圾回收)

在这里插入图片描述
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是在新生代

  • 新生代
  • 幸存区(from,to)
  • 老年区
    GC两种:轻GC(普通的GC),重GC(全局GC)
    在这里插入图片描述

引用计数法

会给每个对象分配计数器,计数器本身还会占用内存。所以很消耗内存。Java一般不采用这种方式。
在这里插入图片描述

复制算法

在这里插入图片描述
在这里插入图片描述
过程:首先to区永远是空的。当一个对象在新生代中没有被GC,
那么它就会被移动到空的to区,同时from区也会把幸存对象复制到to区。
此时,新生代区就被清空,原来的from区就会变为to区,
原来的to区会变为from区。当一个对象存活超过我们设置的门限时,就会被送到养老区。

好处:没有内存的碎片!
唯一的坏处:浪费了内存空间,因为to区永远是空的。
但是,复制算法最佳使用场景:对象存活度较低的时候,适用于新生区。假如对象100%存活(极端情况),频繁的复制移动也会影响效率。

标记清除算法

在这里插入图片描述
理解:这样做的好处是不像复制算法一样,会浪费一些空间。我们只需要两次扫描。第一次标记存活对象,第二次进行清除即可。但是扫描很耗时间。
优点:不需要额外的空间!
缺点:两次扫描,严重浪费时间,会产生内存碎片(内存碎片即:使得存储对象非常分散)。

标记清除压缩算法

在这里插入图片描述
理解:为了防止内存碎片的产生,我们对清除后的内存再次进行扫描。将存活对象进行一端移动。
可优化方案:理论上可以先进行几次标记清除之后,再进行压缩。可以节约一些时间。

总结

在这里插入图片描述
GC—>分代收集算法

没有最好的算法,只有最合适的算法。

年轻代:

  • 存活率低
  • 使用复制算法较优

老年代:

  • 区域大,存活率高
  • 标记清除+标记清除压缩混合实现

补充:JMM=Java Memory Model(Java内存模型)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值