Simple了解JVM

JVM探究

  • 谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?

  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?

  • JVM的常用调优参数有哪些?

  • 内存快照如何抓取,怎么分析Dump文件?知道嘛?

  • 谈谈JVM中,类加载器你的认识?

 

1、JVM的位置

2、JVM的体系结构

3、类加载器

作用:加载 Class 文件~

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器

  3. 扩展类加载器

  4. 应用程序加载器

 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());//1360875712
         System.out.println(car2.hashCode());//1625635731
         System.out.println(car3.hashCode());//1580066828
         Class c1 = car1.getClass();
         Class c2 = car1.getClass();
         Class c3 = car1.getClass();
         System.out.println(c1.hashCode());//1627674070
         System.out.println(c2.hashCode());//1627674070
         System.out.println(c3.hashCode());//1627674070
         System.out.println("-----------------------------");
         ClassLoader classLoader = c1.getClassLoader();//这个类模板用什么加载器加载
         System.out.println(classLoader);//AppClassLoader
         System.out.println(classLoader.getParent());//ExtClassLoader   \jre\lib\ext
         System.out.println(classLoader.getParent().getParent());//null  rt.jar 1.不存在 2.java程序获取不到   
     }
 }

 

4、双亲委派机制

  • 类加载器收到类加载的请求!

  • 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器!

  • 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载~

  • 重复上一个步骤

    • Class Not Found ~

    • null:Java调不到~ C、C++

    • Java:C++- -

5、沙箱安全机制

5.1、概述

  • Java安全模型的核心就是Java沙箱(sandbox)

  • 什么是沙箱?

    • 沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,

    • 通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

  • 沙箱主要限制系统资源访问,那系统资源包括什么?

    • CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

    • 所有的Java程序运行都可以指定沙箱,可以定制安全策略。

5.2、历史

  • 在Java中将执行程序分成本地代码和远程代码两种

    • 本地代码默认视为可信任的

    • 而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。

    • 而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 Sandbox 机制。如下图所示JDK1.0安全模型

  • 但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。

  • 因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。

  • 在Java1.2版本中,再次改进了安全机制,增加了代码签名。

  • 不论本地代码或是远程代码,都会按照用户的安全策略设定,

  • 由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

  • 当前最新的安全机制实现,则引入了 域(Domain) 的概念。

  • 虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。

  • 虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。

  • 存在于不同域中的类文件就具有了当前域的全部权限,

  • 下图所示为最新的安全模型(jdk 1.6)

5.3、组成沙箱的基本组件

  • 字节码校验器(bytecode verifier) :

    • 确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。

    • 但并不是所有的类文件都会经过字节码校验,比如核心类。如:new String();

  • 类裝载器(class loader) :

    • 其中类装载器在3个方面对Java沙箱起作用

      • 它防止恶意代码去干涉善意的代码;

      • 它守护了被信任的类库边界;

      • 它将代码归入保护域,确定了代码可以进行哪些操作。

  • 虚拟机为不同的类加载器载入的类提供不同的命名空间。

    • 命名空间由一系列唯一的名称组成, 每一个被装载的类将有一个名字,

    • 这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。

  • 类装载器采用的机制是双亲委派模式。

    • 1.从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;

    • 2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。

      • 存取控制器(access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

      • 安全管理器(security manager) : 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

      • 安全软件包(security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

        • 安全提供者

        • 消息摘要

        • 数字签名

        • 加密

        • 鉴别

6、Nactive

6.1、概述

  • native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!

  • 会进入本地方法栈,然后通过本地接口 (JNI)( Java Native Interface ),调用本地方法库

  • JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序

  • 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法

  • 在最终执行的时候,通过本地接口 (JNI),加载本地方法库中的方法

  • 如private native void start0();

6.2. Method Stack & JNI

本地方法栈(Native Method Stack)

  • 它的具体做法是 Native Method Stack 中登记 native方法,

  • 在执行引擎执行的时候通过本地接口 (JNI),加载本地方法库(Native Libraies)。

本地接口(Native Interface)JNI

  • 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序

  • Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,

  • 它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。

  • 目前native方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java程序驱动打印机 或者 Java系统管理生产设备,在企业级应用中已经比较少见。

  • 现在的异构领域间通信很发达,比如可以使用 Socket通信,也可以使用 Web Service 等等,不多做介绍!

7、方法区、堆、桟

1、PC寄存器

  • 程序计数器: Program Counter Register

  • 每个线程都有一个程序计数器,是线程私有的

  • 就是一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址——将要执行的指令代码,执行引擎读取下一条指令

  • 是一个非常小的内存空间,几乎可以忽略不计

2.方法区Method Area

  • 方法区存放被所有线程共享的所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义

  • 简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

这句话需要背下!:

     /*
 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量和数组的内容存在堆内存中,和方法区无关
     */

方法区就存:static final Class信息 运行时常量池

3、桟:数据结构

为什么 main 方法最先执行,但最后结束?——栈

  • 栈:栈内存,主管程序的运行,生命周期和线程同步;

  • 线程结束,栈内存也就是释放,

  • 对于栈来说,不存在垃圾回收问题

 

一旦线程结束,栈就Over!

栈有可能放的东西:8大基本类型 + 对象的引用 + 实例的方法

栈运行的原理:每执行一个方法,都会产生一个栈帧

栈如果满了,就会 StackOverFlowError

4、堆

一个JVM仅有一个堆内存,堆内存大小可以调节

  1. JVM内存划分为堆内存和非堆内存,

    1. 堆内存分为年轻代(Young Generation)、老年代(Old Generation),

    2. 非堆内存就一个永久代(Permanent Generation)。(这个非堆,严格意义上来说也是堆,但逻辑操作上又不是堆…)

  2. 年轻代又分为Eden和Survivor区。

    1. Survivor区由FromSpace和ToSpace组成。

    2. Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。

    3. Eden满了就触发轻GC,

      1. 经过轻GC存活下来的就到了幸存者区,

      2. 幸存者区满之后意味着新生区也满了,则触发重GC,

      3. 经过重GC之后存活下来的就到了老年代。

  3. 堆内存用途:

    1. 存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

  4. 老年代:

    1. 在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。

    2. 老年代中的对象生命周期较长,存活率比较高,

    3. 在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

  5. 非堆内存用途:永久代,也叫方法区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

1、分代概念

  1. 新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。

  2. 老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

    1. Minor GC : 清理年轻代

    2. Major GC : 清理老年代

    3. Full GC : 清理整个堆空间,包括年轻代和永久代

    4. 所有GC都会停止应用所有线程。

2、元空间

JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存

  • 元空间有注意有两个参数:

    • MetaspaceSize :初始化元空间大小,控制发生GC阈值

    • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

3、移除永久代原因

为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了!

 

8、三种JVM

  • Sun公司 Hotspot 这一种就是我们使用的!

  • DEA JRckit

  • IBM J9 VM

9、堆

  • Heap

    • 一个JVM只有一个堆内存,堆内存的大小是可以调节的。 类加载器读取了类文件后,一般会把什么东西放到堆中?

  • 类, 方法,常量,变量~,保存我们所有引用类型的真实对象! 堆内存中还要细分为三个区域:

    • 新生区:Eden , From , To

    • 养老区 Old

    • 永久区 Perm

幸存区:某次垃圾回收中,幸存下来的,又被判定为新生区

图 堆内存详细划分

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

  • 假设内存满了,OOM,堆内存不够——java.lang.OutOfMemoryError:Java heap space

  • 永久存储区里存放的都是Java自带的,例如lang包中的类,如果不存在这些,Java就跑不起来了

  • 在JDK8以后,永久存储区改了个名字(元空间)

图 堆内存溢出

1、新生区、老年区

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

  • 伊甸园,所有的对象都是在 伊甸园new出来的!

  • 幸存区(0,1)

真理:经过研究,99%对象都是临时对象!

2、永久区

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

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

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

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

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

元空间:逻辑上存在,物理上不存在~

3、探究

1.什么时候在永久区就崩了呢?

  • 一个启动类,加载了大量的第三方jar包。

  • Tomcat 部署了太多的应用

  • 大量动态生成反射类,不断地被加载

  • 直到内存满,就会出现 OOM

2.元空间

逻辑上存在,物理上不存在 (因为存储在本地磁盘内) 所以最后并不算在JVM虚拟机内存中

10、对内存调优

1、概述

 System.out.println(“最大内存Max_memory=”+Runtime.getRuntime().maxMemory()/(double)1024/1024+”M”);
 System.out.println(“初始化内存大小Total_memory=”+Runtime.getRuntime().totalMemory()/(double)1024/1024+”M”);

虚拟机参数打印信息:

2、JVM内存分析

默认情况下:

  • 分配的总内存,是电脑的 1/4

  • 而初始化 JVM 的内存是 1/64

 public class Test {
     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=>"+max+"字节\t"+(total/(double)1024/1024)+"MB");
 ​
         // 默认情况下:分配的总内存 是电脑的1/4,而初始化的内存: 1//64
     }
     //    -Xms1024m -Xmx1024m -XX:+PrintGCDetails
     //    OOM:
     //      1.尝试查看扩大对内存看结果
     //      2.分析内存,看一下那个地方出现了 问题(专业工具)!
 }

3、JVM 内存排错

遇到OOM:对空间错误

  • 1、尝试扩大对内存(设置 VM OPtions),产看结果:-Xms1024m -Xmx1024m XX:+PrintGCDetails

    • 如果解决,就说明默认分配的内存不够

  • 2、分析内存,看一下哪里出现了问题(专业工具)

 public class Hello {
     public static void main(String[] args) {
         String str = "hello,stayawakeatalltime";
 ​
         while (true){
             str += str + new Random().nextInt(8888888) + new Random().nextInt(8888888);
         }
         /*
             //改成小内存执行Jvm
             // -Xms8m -Xmx8m -XX:+PrintGCDetails
             // -Xms 设置初始化内存分配大小 1/64
             // -Xmx 设置最大分配内存,默认 1/4
             // -XX:+PrintGCDetails :控制台打印 GC 的回收信息
             // -XX:+HeapDumpOnOutOfMemoryError :输出 OOM dump 信息
          */
     }
 }

在一个项目中,如果出现了OOM 故障,如何进行排错

  • 内存快照分析工具:JProfile、MAT是eclipse集成的使用工具~

  • 工具作用:

    • 分析Dump 内存文件,快速定位内存泄露

    • 探知堆中的数据,获得大的对象

    • 安装插件,完成之后重启idea

    • 下载客户端工具 JProfile

      工具使用:

  • 安装路径,确保无空格无中文!

  • 测试程序

  • 报错,但无法定位错误

  • 参数设置

  • 再次执行,得到错误文件

  • 使用Jprofiler打开

  • 分析

11、GC

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

  • 新生代

  • 幸存区(from , to)

  • 老年区

GC分为两种:轻GC(Partial GC))普通GC/ 重GC( Full GC)全局GC

 

GC题目:

  • JVM的内存模型和分区~详细到每个区放什么?

  • 堆里面的分区有哪些?Eden、from、to、老年区,说说他们的特点!

  • GC的算法有哪些?标记清除法、标记压缩、复制算法、引用计数器,怎么用的?

  • 轻GC和重GC分别在什么时候发生?

 

引用计数法:

复制算法:

  • 好处:没有内存的碎片~

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

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

 

标记清除算法

  • 优点:不需要额外的空间!

  • 缺点:两次扫描,严重浪费空间,会产生内存碎片~

标记压缩

在优化~

标记清除压缩

先标记几次

在压缩~

总结:

内存效率:复制算法 > 标记压缩算法 > 标记清除算法

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

 

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

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

 

年轻代:

  • 存活率低

  • 复制算法!

 

老年代:

  • 区域大:存活率低

  • 标记清除(内存碎片不是太多)+ 标记压缩算法混合实现

 

JMM

1、了解

1、什么是JMM?

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据。

Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。

2、可以做什么?

  • JMM是用来定义一个一 致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。(找到这个规则! )

  • 参考规则:https://www.jianshu.com/p/8a58d8335270

3、JMM和JVM的区别

  • JMM是围绕原子性,有序性、可见性展开。

  • JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。

4、主内存和工作内存

JMM数据同步八大原子操作

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

  • lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态。

  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 。

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中 。

  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量 。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作 。

  • write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中 。

2.内存可见性

在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中。

那怎样保持这变量之间的一致性呢?

当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。 白话:当我在修改 A 时,其他的线程都读不了 A,得等我改好并重新放回去。

JMM是如何解决原子性&可见性&有序性问题 原子性问题 除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

可见性问题

volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。 synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。 有序性问题 通过volatile可以保证有序性。 可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 指令重排序 Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它的顺序情况的结果相等,那么指令的执行顺序可以与代码顺序不一样,此过程叫指令你的重排序

指令重排序的意义是什么?

JVM能根据处理器特征适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器特性。

 

这个学习只是为了简单了解入个门,之后还会继续学习的!

posted @   nakano_may  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示