JVM

JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

1.JVM的位置

在这里插入图片描述

2.JVM的体系结构

在这里插入图片描述

  • GC调优都是在堆中调优
  • 方法区也是特殊的堆,所以调优都是在方法区和堆中

1.程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

2.Java栈

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

3.本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

4.Java堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常

5.方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

6.运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

7.直接内存

非虚拟机运行时数据区的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

3.类加载器

在这里插入图片描述

  • .java文件通过编译后变成.class文件,然后将.class文件传给ClassLoader(类加载器)
  • 类加载器通过加载和初始化,可以在栈中生成一个类(不是实例对象,是一个抽象的对象)
  • 生成的类可以通过实例化,在堆中生成类的实例(具有类属性)
  • 通过反射,可以由实例对象调用getClass得到类;类可以调用getClassLoader获得类加载器

4.双亲委派机制

假设我们新建一个String类,包的位置和jdk的包位置一样
在这里插入图片描述
那么这时候编译器就会报错(之前的idea是不会报编译时错误的)
在这里插入图片描述
说是两个类冲突了,需要使用一个不相冲突的类。
那么使用了正常的类之后又会有下面的问题
在这里插入图片描述
会说找不到main方法
所以这就引出了jvm的一个机制:双亲委派机制
先来看下几个class loader:

  1. Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  2. ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  3. AppClassLoader:主要负责加载应用程序的主函数类

再来看下源代码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

其实从代码就可以看出来:

  1. 当我们加载一个类中,不会先考虑我们自定义的类加载器,会首先在AppClassLoader中检查是否加载过,如果有就不需要加载了,如果没有就会去拿父加载器,然后调用父加载器的loadClass方法。
  2. 父类中同样会检查自己是否被加载过,如果有就不用加载,如果没有就继续往上。
  3. 在到加载Bootstrap classLoader之前没有哪个加载器是自己选择加载的,如果父加载器无法加载,就会返回到子加载器去加载,一直到最底层。
  4. 如果没有任何加载器可以加载,就会抛出ClassNotFoundException异常。

所以我们自己定义的String 类,是在 AppClassLoade中的,在加载时会优先去加载Bootstrap classLoader,而这里已经有String类了,所以是没有办法加载我们自己定义的String类的。

5.native 关键字

  1. 凡是带了native关键字的,说明Java的作用范围达不到了,就要去调用底层C,C++的库了
  2. 进入本地方法栈
  3. 调用本地方法接口JNI(Java Native Interface)
  4. JNI作用:最初是为了开拓Java,融合不同的变成语言,因为一开始Java刚出来的时候正好是C,C++流行的时候,要想立足就必须兼容和可以调用C,C++的程序。
  5. 它在内存中专门开辟了一块标记区域:Native Method Stack(本地方法栈),用于登记natice方法。
  6. 在最终执行的时候,就通过JNI去加载调用本地方法库。

在这里插入图片描述

6.方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
在这里插入图片描述

  1. 实例化一个类的时候,就会在栈中创建一个实例的声明,相当于是一个指针,真正的实例对象是在堆中,通过指针指向堆中的实例对象
  2. 如果是属性默认值就存在常量池中,当给属性赋值的时候就会在堆中赋值

7.栈

栈:栈内存,主管程序的运行,生命周期和线程同步;线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就Over!
栈内存中:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧

在这里插入图片描述

8.堆(heap)

一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般把类, 方法,常量,变量~,保存我们所有引用类型的真实对象放到堆中。

堆内存可分为三个区域:新生区(伊甸园区(亚当夏娃的原居地),Young,New),养老区 (old),永久区(perm)
在这里插入图片描述

  1. 垃圾回收主要在伊甸园区和养老区
  2. 假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:Java heap space。
  3. 永久存储区里存放的都是Java自带的 例如lang包中的类 如果不存在这些,Java就跑不起来了
  4. 在JDK8以后,永久存储区改了个名字(元空间)

1.新生区

  • 类:诞生和成长的地方,甚至是死亡

  • 伊甸园区:所有的对象都是在伊甸园区new出来的

  • 幸存区(0,1)

  • 伊甸园区满了就触发轻GC

  • 经过轻GC存活下来的就到了幸存者区

  • 幸存者区满了就意味着新生区就满了,那么就会触发重GC(full GC)

  • 经过重GC之后存活下来的就到了养老区

2.永久区

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

  • jdk1.6之前:永久代,常量池是在方法区
  • jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
  • jdk1.8之后:无永久代,常量池在元空间

在这里插入图片描述

模拟OOM

public class Demo02 {
    public static void main(String[] args) {
        String s="";
        while (true){
            s+=s+new Random().toString();
        }
    }
}

调整JVM参数
在这里插入图片描述

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

最大内存和总内存

编写程序

public static void main(String[] args) {
        long lm = Runtime.getRuntime().maxMemory();
        long lt = Runtime.getRuntime().totalMemory();

        System.out.println("最大内存:"+lm+"字节"+(double)lm/1024/1024+"M");
        System.out.println("总内存:"+lt+"字节"+(double)lt/1024/1024+"M");
    }

在这里插入图片描述

在这里插入图片描述

默认的:最大内存为我电脑内存的1/4,总内存为1/64

在一个项目中,突然出现了OOM故障,那么该如何排除 研究为什么出错~

使用内存快照分析工具JProfile

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象

设置jvm参数
// -Xms设置初始化内存分配大小/164
// -Xmx设置最大分配内存,默以1/4
// -XX: +PrintGCDetails // 打印GC垃圾回收信息
// -XX: +HeapDumpOnOutOfMemoryError //oom DUMP

9.GC算法

1.引用计数法

在这里插入图片描述

  1. 对于每个对象都分配一个计数值,每次引用都让这个次数加一
  2. 在规定时间内,或者规定运行次数内,次数最少的对象将被GC回收

2.复制算法

在这里插入图片描述

  • 好处:没有内存的碎片~
  • 坏处:浪费了内存空间~ :多了一半空间永远是空to。假设对象100%存活(极端情况)

复制算法最佳使用场景:对象存活度较低的时候;新生区~

3.标记清除算法

在这里插入图片描述

  1. 扫描对象,标记存活的对象
  2. 对于没有被标记的对象就进行垃圾回收

4.标记清除压缩算法

在这里插入图片描述

  1. 在原先标记清除算法的基础上,再次扫描,将清除后的空间再进行移动。

5.总结

  • 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
  • 内存整齐度:复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记压缩算法=标记清除算法>复制算法

但是没有最优的算法,只有最合适的算法------->>“GC分代算法”

年轻代:–>>存活率低–>>复制算法
老年代:–>区域大:存活率高–>>标记清除(内存碎片不是太多)+标记压缩混合实现

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值