JVM入门

发布时间:2023年12月20日

JVM概述

JVM位置

image.png

JVM体系结构

在这里插入图片描述

注意:栈中一定不存在垃圾,栈中数据用完一个弹出一个,总结来说,栈区、本地方法栈、程序计数器这三块必定不存在垃圾。JVM调优主要是针对方法区、堆(99%)进行调优。

常用的第三方插件(如Lombok)都是操作执行引擎区域,生成对应getter、setter方法

  1. 本地方法栈,例如Thread类中的start0()方法。凡是native修饰的方法,说明Java作用范围达不到,需要调用底层C语言的库,会进入到本地方法栈,进而调用本地方法接口JNI
    JNI的作用是:扩展Java的使用,融合不同的编程语言为Java所用
    JVM单独在内存区域中开辟一块标记区域:Native Method Stack,登记native方法(仅登记,并不执行),最终执行的时候,通过JNI加载本地方法库的方法
  2. PC寄存器
    每个线程都有一个程序计数器,是线程私有的,就是一个指针,在执行引擎读取下一条指令
  3. 方法区(本质也属于堆)
    被所有线程共享,所有字段和方法字节码、一些特殊方法,如构造函数,接口代码也在此定义。简单说,所有定义的方法信息都保存在此区域,此区域为共享区间。
    注意:静态变量(static)、常量(final)、类信息Class模板(构造方法、接口定义)、运行时的常量池存在方法区中,但是,实例变量存在堆内存中,与方法区无关
  4. Java栈,详见3
  5. 堆,详见4
  • 线程私有:Java栈、程序计数器、本地方法栈
  • 线程共享:方法区、堆

类装载器

类装载过程

Java中所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,其作用是:将class文件从硬盘读取到内存中
类加载方式有两种:

  • 隐式装载:程序运行中碰到new等方式创建对象时,隐式调用类装载器加载到对应的类到JVM中
  • 显式装载:通过class.forname()等方法,显式加载

备注:Java的加载是动态的,并不会一次性加载所有类后才运行,而是保证运行的基础类完全加载到JVM中,其他类只有在需要的时候才加载,为了节省内存开销
在这里插入图片描述

类装载执行过程:

  1. 加载:根据查找路径找到对应的class文件后导入
  2. 验证:检查加载的class文件的正确性
  3. 准备:给类中的静态变量分配内存空间
  4. 解析:虚拟机将常量池中的符号引用替换为直接引用的过程。
    符号引用——可理解为一个标识;直接引用——直接指向内存中的地址
  5. 初始化:对静态变量和静态代码块进行初始化工作

类加载器分类

定义:实现通过类的全限定名获取该类的二进制字节流的代码块
分类:

  • 启动类加载器(Bootstrap ClassLoader):加载Java核心类库,无法被Java程序直接引用;是虚拟机的一部分,用来加载Java_HOME/lib目录下的类库
  • 扩展类加载器(ExtClassLoader):加载Java扩展库。负责加载\lib\ext目录的所有类库
  • 系统类加载器(AppClassLoader):位于jre环境下的rt.jar内,根据Java应用的类路径(classpath)加载Java类,基本上我们创建的类都是它完成加载,可通过ClassLoader.getSystemClassLoader()获取该类加载器
  • 用户自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。
public class Car {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();

        Class<? extends Car> aClass = car1.getClass();
        Class<? extends Car> bClass = car1.getClass();
        // 输出一致,说明两个实例由一个Class模板实例出
        System.out.println(aClass.hashCode());
        System.out.println(bClass.hashCode());

        ClassLoader classLoader = aClass.getClassLoader();
        ClassLoader classLoader2 = classLoader.getParent();
        ClassLoader classLoader3 = classLoader2.getParent();
        // AppClassLoader
        System.out.println(classLoader);
        // ExtClassLoader
        System.out.println(classLoader2);
        // null(Java程序读取不到,Java早期由C,C++语言编写,去除了C++中 指针和内存管理,所以早期也叫C++--)
        System.out.println(classLoader3);
    }
}

5592464
5592464
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@424c0bc4
null

双亲委派机制

重新定义一个java.lang.String类,启动时报错“无法找到main方法”
image.png
解释:最终会通过“Bootstrap ClassLoader”加载器找到 “rt.jar”包下的“java.lang.String”类,所以不会加载到自定义的String.java
出现这种情况是由于类加载器的“双亲委派机制”

定义:如果一个类加载器收到了类加载的请求,它首先不会自己去加载该类,而是把请求委派给父类加载器,每一层都是如此,这样所有加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求时,抛出异常通知子加载器尝试加载
这样设计的目的是:为了安全,防止开发人员修改一些系统类。
简单理解:当一个类收到类加载请求时,不会自己去加载,而是将其委派给父类,如果父类不能加载,再交由子类去完成加载
image.png

沙箱安全机制(非重点)

Java安全模型核心就是Java沙箱(sandbox),所谓沙箱机制,就是将Java代码限定在虚拟机(JVM)特定运行环境中,并严格限制代码对本地系统资源访问。通过这种保护措施保护代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,系统资源包括:CPU、内存、文件系统、网络。不同级别的沙箱对资源访问限制也可以不一样
所有Java程序运行都可以指定沙箱,定制安全策略
在Java中将执行程序分为本地代码和远程代码两种,本地代码默认是可信任的,远程代码则被看作是不受信任的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码,在早期Java实现中,安全依赖于沙箱机制。

但如此严格的安全机制也给程序功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时,就无法实现,因此在后续Java1.1版本中,对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限
在jdk1.2版本中,再次改进安全机制,增加了代码签名,无论本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机权限不同的运行空间,实现差异化代码执行权限控制

当前最新的安全机制实现,则引入域(Domain)的概念,虚拟机会把所有代码加载到不同系统域和应用域,系统域专门负责与关键资源进行交互,而各应用域部分则通过系统域的部分代理来对各种需要的资源进行访问,虚拟机中不同的受保护域对应不同权限,存在于不同域中的类文件就具有当前域的全部权限

组成沙箱的基本组件:

  • 字节码校验器:确保Java类文件的语法规范,可实现内存保护,但并非所有类文件都会经过字节码校验,比如核心类(例如java,javax包下)
  • 类装载器:主要有3个方面
    • 防止恶意代码干涉善意的代码(双亲委派机制)
    • 守护被信任的类库边界
    • 将代码归入保护域,确定代码可进行哪些操作

虚拟机为不同类加载器载入的类提供不同的命名空间,命名空间由一系列唯一名称组成,每一个被装载的类将有一个名字,该命名空间由Java虚拟机为每个类装载器维护,他们之间互相不可见
类装载器采用双亲委派机制:

  1. 由最内层Java自带类装载器开始加载,外层恶意同名类得不到加载而无法使用
  2. 由于严格通过包区分访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码自然无法生效
  • 存取控制器:可以控制核心API对操作系统的存取权限,而这个控制策略设定,可由用户指定
  • 安全管理器:核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高
  • 安全软件包:java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名(keytools、https)
    • 加密
    • 鉴别

栈的特点:先进后出(所以main()方法,最先执行,最后结束)
栈:每个线程都有自己的线程栈,主管程序的运行,生命周期和线程同步;线程结束,栈内存释放。故栈中不存在垃圾回收问题!
栈中主要存放:8大基本类型、对象的引用(引用地址)、实例的方法

栈运行原理:每运行一个方法产生一个栈帧。当栈满了就会抛出错误StackOverflowError

JVM版本

三种JVM版本:

  1. Sun公司 的 HotSpot HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)

image.png

  1. BEA公司 的 JRockit
  2. IBM公司的 J9VM JIT编辑器

一个JVM只有一个堆内存(垃圾回收基本都在此),堆内存大小是可以调节的。
类加载器读取了类文件后,一般存放 类、方法、常量、变量、以及引用类型的真实对象。
堆内存细分为三个区域:

  • 新生区(伊甸园区)
  • 老年区(Old)
  • 永久区(Perm)


GC垃圾回收,主要发生在新生区和老年区,假若内存满了就会出现OOM异常
注意:

  • JDK以后,永久存储区,被称为 元空间

新生区

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

  • 假若 伊甸园区 空间达到阈值,触发一次 轻GC,该对象仍存活,就被移到幸存区

  • 当新生区内存达到阈值,触发 重GC,存活的对象移到 老年区

  • 特点:对象更新速度快,短时间内产生大量的“”死亡对象,产生连续可用的空间,所以使用复制清除算法和并行收集器进行垃圾回收,称为初级回收(minor gc)

  • 发展变化:当对象在Eden出生后,经过一次minor GC后,若对象还存活着,并能被另一块survivor区域所容纳,则使用复制算法将其复制到另一块survivor区域,并将该对象年龄+1,当对象年龄达到某个值(默认为15)时,该对象即可变为老年代

老年区
老年代的GC算法采用标记-清除算法
注意:标记-清除算法收集垃圾的时候会产生许多的内存碎片。

永久区
此区域常驻内存的,用于存放JDK自带的class对象,interface元数据,存储的是Java运行时的环境或类信息(此区域不存在垃圾回收,关闭JVM时释放此区域内存)

  • 在JDK1.6之前:永久代,常量池是存放在方法区的
  • JDK1.7:永久代,但提出去永久代的思想,此时常量池存放于 堆 中
  • JDK1.8:无永久代,常量池在元空间

查看JVM内存参数:

public class JVMDemo {
    public static void main(String[] args) {
        // 虚拟机能使用的最大内存(默认占电脑内存1/4)
        long max = Runtime.getRuntime().maxMemory();// 单位为字节
        System.out.println("max内存:"+max+"字节");
        // JVM初始化总内存(默认占电脑内存1/64)
        long total = Runtime.getRuntime().totalMemory();// 单位为字节
        System.out.println("total内存:"+total+"字节");
    }
}
max内存:3793747968字节
total内存:257425408字节

调整JVM参数:
image.png
-XX:+PrintGCDetails打印GC时内存信息
image.png
from、to对应幸存者0、1区(幸存者01区会随时交换位置)
注意:上述堆空间模型分布,逻辑上存在,物理上不存在(从打印结果可看出,PSYoungGen+ParOldGen就等于堆空间,而元空间并未占用堆内存,实际存于本地内存)

查看GC过程

public class JVMDemo {
    // JVM参数 -Xms8m -Xmx8m -XX:+PrintGCDetails
    public static void main(String[] args) {
        String str = "khfkoanngopaalknfasopajr";
        while (true) {
            str += str + new Random().nextInt(918246192) + new Random().nextInt(189242864);
        }
    }
}

image.png
image.png

JPofiler工具分析OOM原因

当项目出现OOM异常,尝试扩大JVM内存仍无法解决问题时,就需要排查何种原因导致
分析工具有 MAT、JPofiler,作用有:

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

工具安装:

  1. IDEA安装plugins:JPofiler
  2. 安装客户端(安装路径不能有空格、中文)
  3. IDEA配置JPofiler启动位置

image.png

  1. 在启动类上配置 OOM 时输出dump文件

image.png
image.png

  1. 在src目录下找到dump文件,用JPofiler打开

image.png
基本信息:
image.png

大对象数据:
image.png

线程对应异常行:
image.png

GC回收

在Java中,开发者无需显式地释放一个对象地内存,而是由虚拟机自动执行。
在JVM中,有一个垃圾回收线程,它是低优先级的,只在虚拟机空闲或堆内存不足时,才会触发执行,扫描没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。Java没有提供释放已分配内存的显式操作方法。
GC回收主要针对 堆和方法区(本质也在堆中)
GC回收分为:轻GC、重GC(full-GC)

如何判断对象可被回收
一般有两种办法判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时+1,引用被释放时-1,当计数为0时候表示可被回收。
    缺点:无法解决循环引用问题
  • 可达性分析算法:从GC Roots开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链的时候,说明该对象可被回收

GC常见题目:

  • JVM内存模型和分区,每个区放什么?
  • 堆里面的分区有哪些(Eden、from、to、老年区),并说明其特点
  • GC算法有哪些?标记清除法、标记压缩、复制算法、引用计数器,怎么用的?
  • 轻GC和重GC,分别在什么时候发生

垃圾回收算法

  1. 复制算法(主要用于新生区,对象存活率较低):按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块中,然后一次性清理掉已使用的内存空间
    优点:无内存碎片
    缺点:内存使用率只有原来的一半
    image.png
  2. 标记-清除算法(mark-sweep):标记无用对象,然后进行清除回收
    缺点:两次扫描效率不高,产生大量不连续的垃圾碎片
    image.png
  3. 标记-整理算法(常用于老年区):标记无用对象,让所有活着的对象都向一端移动,直接清理掉边界以外的内存
    新生代中可以用复制算法,但老年代由于对象存活率高,会有较多的复制操作,导致效率变低,因此出现该算法
    缺点:再多一次扫描,仍需要进行局部移动,一定程序降低效率
    image.png
  4. 分代收集算法(GC回收使用的算法):根据对象存活周期将内存划分几块,一般是新生代(存活率低)采用复制算法,老年代采用标记-整理算法

内存效率:复制算法>标记-清除>标记-整理
内存整齐度:复制算法=标记-整理>标记-清除
内存利用率:标记-整理=标记-清除>复制算法

调优参数

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。‘

JMM(Java Memory Model)

heppen-before机制

文章来源:https://blog.csdn.net/Yu_Mariam/article/details/135038644
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。