Java—JVM详情

  • Post author:
  • Post category:java

JVM介绍

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

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

JVM的位置

在这里插入图片描述

JVM的体系结构

在这里插入图片描述

类加载器

加载过程

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

启动类加载器是由C/C++写的,主要负责加载 jre\lib 目录下的类;

扩展类加载器主要负责加jre\lib\ext 目录下的类;

应用程序类加载器主要负责加载我们自己编写的类;

自己写类加载器,即自定义加载器。程序主要由前面三个类加载器相互配合加载的。

双亲委派机制

在这里插入图片描述

双亲委派机制得工作过程:

  1. 类加载器收到类加载的请求;
  2. 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
  3. 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
  4. 重复步骤三;

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,可以定制安全策略。

具体讲解参考:https://blog.csdn.net/qq_30336433/article/details/83268945

Native

package com.lisicheng.JavaEE.JVM.Native;

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

        new Thread(()->{

        },"my thread name").start();

    }
    //native : 凡是带了native 关键字,说明Java作用范围打不到了,去调用底层C语言的库!
    //会进入本地方法栈
    //调用本地方法接口 JNI
    //JNI作用:扩展Java的使用,融合不同的编程语言为Java所用!最初想融合c,c++。
    //Java诞生的时候C,C++流行,想要流行必须调用C,C++的程序
    //它在内存区域中专本开辟了一个一块标志区域:Native Method Stack,登记native方法

    //Java程序驱动打印机,管理系统,掌握即可,在企业及很少使用

    private native void start0();

    //调用其他接口,来实现调用其他语言的程序:Socket,WebService

}

PC寄存器

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

方法区

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

​ static final Class 常量池

  1. 每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。
  2. 某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池。此外,当虚拟机遇到栈内操作指令时,它对当前帧内数据执行操作。
  3. 每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。而这个新帧自然就成为了当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等数据。
  4. Java方法可以以两种方式完成。一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
  5. Java帧上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。

三种JVM

  • Sun公司HotSpot Java Hotspot(TM) 64-Bit Server VM (build 25. 181-b13, mixed mode)
  • BEA JRockit
  • IBM J9 VM

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中? 类, 方法,常量,变量,保存我们所有引用类型的真实对象。
堆内存中还要细分为三个区域:
●新生区(伊甸园区) Young/New
●养老区old
●永久区Perm

在这里插入图片描述

GC垃圾回收,主要在伊甸园区和养老区。

假设内存满了,00M,堆内存不够! java.lang.OutOfMemoryError: Java heap space

在JDK8以后,永久存储区改名为元空间

新生区

  • 类:诞生和成长的地方,甚至是死亡。
  • 伊甸园:所有的对象都是在伊甸园new出来的
  • 幸存区(0,1)

老年区

年老代主要存放JVM认为生命周期比较长的对象(默认经过15次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

永久区

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

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

  • jdk1.6之前 :永久代,常量池是在方法区;

  • jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中

  • jdk1.8之后:无永久代,常量池在元空间

    在这里插入图片描述

    元空间并不在虚拟机中,而是使用本地内存(下面会有证明)

堆内存调优

什么是OOM?

OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。

OOM的两种情况

内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

常见报错:

  1. java.lang.OutOfMemoryError: Java heap space ——>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改,例如:-Xms1024m -Xms1024m -XX:+PrintGCDetails
  2. java.lang.OutOfMemoryError: PermGen space ——>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  3. java.lang.StackOverflowError ——> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
public class Dome01 {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory(); //字节 1024 * 1024
        //返回JVM的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max = " + max + "字节\t" + (max / (double)1024 / 1024) + "MB");
        System.out.println("total = " + total + "字节\t" + (total / (double)1024 / 1024) + "MB");

        //默认情况下:分配的总内存为电脑内存的1/4     初始化总内存为电脑内存的1/64
        //                Xms : 设置初始化内存分配大小
        //                Xmx : 设置最大分配内存
        //                -XX:+PrintGCDetails : 打印GC信息
        //                -XX:+HeapDumpOnOutOfMemoryError   : 打印OOM DUMP
        //设置:VM options:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
        //  新生代  + 老年代 = 305664K + 699392K = 1005056K = 981.5MB
        //结论:元空间并不在虚拟机中,而是使用本地内存

    }
}
//-Xms8m -Xms8m -XX:+PrintGCDetails
public class Dome02 {
    public static void main(String[] args) {
        String str = "1111111111111";

        while(true) {
            str += str + new Random().nextInt(999999999) + new Random().nextInt(999999999);
        }

    }
}
/*
[GC (Allocation Failure) [PSYoungGen: 1536K->504K(2048K)] 1536K->716K(7680K), 0.0052151 secs] [Times: user=0.06 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 1799K->490K(2048K)] 2011K->1169K(7680K), 0.0012372 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2012K->408K(2048K)] 2691K->1332K(7680K), 0.0011145 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1694K->360K(3584K)] 3602K->2760K(9216K), 0.0014674 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2878K->408K(3584K)] 7246K->5759K(9216K), 0.0020309 secs] [Times: user=0.09 sys=0.01, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 408K->0K(3584K)] [ParOldGen: 5351K->1707K(5632K)] 5759K->1707K(9216K), [Metaspace: 3524K->3524K(1056768K)], 0.0092068 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 3072K->96K(7680K)] 622646K->619670K(630784K), 0.0061823 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 96K->0K(7680K)] [ParOldGen: 619574K->126787K(137728K)] 619670K->126787K(145408K), [Metaspace: 3573K->3573K(1056768K)], 0.1621748 secs] [Times: user=0.16 sys=0.13, real=0.16 secs] 
[GC (Allocation Failure) [PSYoungGen: 1162K->416K(7680K)] 1009494K->1008755K(1045504K), 0.0097663 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 416K->416K(12800K)] 1008755K->1008755K(1050624K), 0.0078646 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 416K->0K(12800K)] [ParOldGen: 1008339K->378855K(414208K)] 1008755K->378855K(427008K), [Metaspace: 4028K->4028K(1056768K)], 0.2112864 secs] [Times: user=0.44 sys=0.11, real=0.21 secs] 
[GC (Allocation Failure) [PSYoungGen: 231K->32K(12800K)] 882826K->882626K(1050624K), 0.0090386 secs] [Times: user=0.01 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(18944K)] 882626K->882626K(1056768K), 0.0097156 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(18944K)] [ParOldGen: 882594K->630724K(690176K)] 882626K->630724K(709120K), [Metaspace: 4028K->4028K(1056768K)], 0.2113894 secs] [Times: user=0.61 sys=0.06, real=0.21 secs] 
[GC (Allocation Failure) [PSYoungGen: 352K->32K(18944K)] 882946K->882626K(1056768K), 0.0091699 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->0K(27648K)] 882626K->882602K(1065472K), 0.0104752 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(27648K)] [ParOldGen: 882602K->252920K(328192K)] 882602K->252920K(355840K), [Metaspace: 4028K->4028K(1056768K)], 0.2446579 secs] [Times: user=0.27 sys=0.13, real=0.24 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(27648K)] 1008529K->1008529K(1065472K), 0.0095208 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(27648K)] [ParOldGen: 1008529K->756667K(843264K)] 1008529K->756667K(870912K), [Metaspace: 4028K->4028K(1056768K)], 0.1825665 secs] [Times: user=0.64 sys=0.06, real=0.18 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(39424K)] 756667K->756667K(1077248K), 0.0061858 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(39424K)] [ParOldGen: 756667K->756605K(852992K)] 756667K->756605K(892416K), [Metaspace: 4028K->4028K(1056768K)], 0.3882432 secs] [Times: user=1.53 sys=0.03, real=0.39 secs] 
Heap
 PSYoungGen      total 39424K, used 1153K [0x00000000e0580000, 0x00000000e2e00000, 0x0000000100000000)
  eden space 38912K, 2% used [0x00000000e0580000,0x00000000e06a07c0,0x00000000e2b80000)
  from space 512K, 0% used [0x00000000e2d80000,0x00000000e2d80000,0x00000000e2e00000)
  to   space 1024K, 0% used [0x00000000e2c00000,0x00000000e2c00000,0x00000000e2d00000)
 ParOldGen       total 1037824K, used 756605K [0x00000000a1000000, 0x00000000e0580000, 0x00000000e0580000)
  object space 1037824K, 72% used [0x00000000a1000000,0x00000000cf2df440,0x00000000e0580000)
 Metaspace       used 4059K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 452K, capacity 460K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.lisicheng.JavaEE.JVM.元空间.Dome02.main(Dome02.java:11)

Process finished with exit code 1

 */

GC(垃圾回收)

JVM在进行GC时,并不是对这三个区域统- -回收。 大部分时候,回收都是新生代

  • 新生代
  • 幸存区(form, to)
  • 老年区

GC两种类:轻GC (普通的GC), 重GC (全局GC)

GC题目:

  • JVM的内存模型和分区~详细到每个区放什么?
  • 堆里面的分区有哪些? Eden, form, to, 老年区,说说他们的特点!
  • GC的算法有哪些? 标记清除算法,标记整理算法,复制算法,分代收集算法,引用计数法

引用计数法

在这里插入图片描述

复制算法

在这里插入图片描述

内存变换过程
在这里插入图片描述

  • 好处:没有内存的碎片
  • 坏处:浪费了内存空间:多了-半空间永远是空to。 假设对象100%存活(极端情况)
  • 复制算法最佳使用场景:对象存活度较低的时候,新生区。

标记清除算法

在这里插入图片描述

  • 优点:不需要额外的空间。
  • 缺点:两次扫描,严重浪费时间,会产生内存碎片。

标记压缩算法

在这里插入图片描述

JMM

什么是JMM?
JMM: (ava Memory Model的缩写)

作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)。

JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

在这里插入图片描述

解决共享对象可见性这个问题:volatile

JMM :抽象的概念, 理论

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load. store和write操作之一 单独出现。 即使用了read必须load,使用了store必须
    write

  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  • 不允许一一个线程将没有assign的数据从工作内存同步回主内存

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一一个未被初始化的变量。就是怼变量实
    施use、store操作之 前,必须经过assign和load操作

  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引|擎使用这个变量前,必
    须重新load或assign操作初始化变量的值

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock- 一个被其他线程锁住的变量

  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

    JMM对这八种操作规则和对yolatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以- -般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

总结


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

思考一个问题:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法— GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法

老年代:

  • 区域大:存活率高
  • 标记清除 (内存碎片不是很多)+ 标记压缩(内存碎片过多进行一次压缩)混合实现

版权声明:本文为weixin_46560570原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。