笨蛋学JVM
文章目录
- 1.认知JVM
- 2.字节码文件
- 3.类加载器
- 4.运行时数据区域
- 5.自动垃圾回收
- ByteBuddy框架
- Arthas
1.认知JVM
1.1JVM = Java virtual Machine
1.2JVM的功能
1.2.1解释和运行
- 对字节码文件中的指令,实时解释运行为机器码,让计算机执行
1.2.2内存管理
- 自动为对象、方法等分配内存
- 自动的垃圾回收机制,回收不再使用的对象
1.2.3即时编译(Just-In-Time=JIT)
- 对**热点代码(很短的时间内被多次调用的代码)**进行优化,提升执行效率
- 支持跨平台性
1.3常见的JVM
1.4JVM的组成
- 字节码文件加载到JVM中,JVM通过类加载器ClassLoader加载class字节码文件中的内容到JVM管理的内存中,在内存中通过执行引擎将字节码指令解释成机器码,执行引擎调用本地接口已经编译好的方法再JVM管理的内存中创建对象。
2.字节码文件
2.1字节码文件
2.1.1字节码文件的组成
2.1.1.1字节码文件的组成部分-Magic魔数
-
文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容
-
软件使用文件的头几个字节(字节头)去校验文件的类型,若软件不支持该种类型就会出错
文件类型 字节数 文件头 JPEG(jpg) 3 FFD8FF PNG(png) 4 89504E47 bmp 2 424D XML(xml) 5 3C3F786D6C AVI(avi) 4 41564920 Java字节码文件(.class) 4 CAFEBABE -
Java字节码文件种,将文件头称为Magic魔数
2.1.1.2字节码文件的组成部分-主副版本号
-
主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号
(主版本号-44 = 当前的JDK版本)
-
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容
2.1.1.3字节码文件的组成部分-常量池
- 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据
2.1.1.4字节码文件的组成部分-方法(避免相同的内容重复定义,节省空间)
- 操作数栈是临时存放数据的地方,局部变量表是存放方法的局部变量的位置
- iconst_0将值放入操作数栈中
- istore_1将值从操作数栈中取出放入局部变量表数组中
- iload_1将局部变量表数组中的指定位置的值放入操作数栈中
- iinc 1 by 1将局部变量表数组中的指定位置的值增加1
2.2字节码常用工具
2.2.1javap -v命令
- javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容
- 直接输入javap查看所有参数
- javap -v 字节码文件名称 查看具体的字节码信息。(若是jar包则需要先使用jar -xvf命令解压)
2.2.2arthas
新建文件夹 -> 执行curl -O https://arthas.aliyun.com/arthas-boot.jar ->运行程序,进入cmd窗口输入java -jar arthas-boot.jar -> 找到正在运行的程序输入对应的编号
dashboard -i 2000 -n 1:每隔两秒对运行的程序进行监控并输出到屏幕,一共执行3次
dump对已加载类的字节码文件到特定目录,dump -d 文件存放的路径 包名.文件名
jad反编译已加载类的源码,jad 包名.类名
2.3类的生命周期
描述了一个类加载、使用、卸载的整个过程
2.3.1加载阶段
通过类加载器将类的字节码信息加载到内存中,Java虚拟机在方法区和堆区中分别创建一个对象以备后用
-
类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
-
类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中
-
生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息
-
Java虚拟机会在堆中生成一份与方法去中数据类似的java.lang.Class对象
作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8之后)
- 在加载阶段,对于开发者来说,就只需要访问堆中的Class对象而不需要访问方法区中的所有信息
Java虚拟机就能很好的控制开发者访问数据的范围
因为方法区中的一些方法是使用C++编写的,而Java并不能访问,所以就将方法区InstanceKlass中我们所需要用到的一些数据复制到堆区中,而开发者就只需要访问堆区中java.lang.Class的对象,从而不再直接方法区中的对象,提高了数据安全性
查看内存中的对象
使用位于JDK安装目录下的lib文件夹中的sa-jdi.jar中自带的hsdb工具
启动命令java -cp sa-jdi.jar sun.jvm.hotspot.HSDB -> 输入对应的进程号,查看相关对象信息
2.3.2连接阶段
2.3.2.1验证
-
验证主要是检测Java字节码文件是否遵守了Java虚拟机规范中的约束
-
主要包含以下四部分:
-
文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求
-
元信息验证,比如类必须有父类,默认有父类Object
-
验证程序执行指令的语义,比如方法内的指令执行中跳转到其他方法总
-
符号引用验证,比如是否访问了其他类中private的方法等
-
2.3.2.2准备
-
准备阶段只会为静态变量(static)分配内存并设置初始值,其初始值根据不同的基本数据类型和引用数据类型的初始值再区分
-
final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值
2.3.2.3解析
- 解析阶段主要是将常量池中的符号引用(编号)替换为直接引用(内存地址)
- 符号引用就是在字节码文件中使用编号来访问常量池中的内容
- 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据
2.3.3初始化阶段
-
初始化阶段会执行静态代码块中的代码,并为静态变量赋值
-
初始化阶段会执行字节码文件中clinit(class init)部分的字节码指令
clinit方法中的执行顺序与Java中编写的顺序是一致的
-
以下几种方式会导致类的初始化
添加-xx:+TraceClassLoading 参数可以打印出加载并初始化的类
- 访问一个类的静态变量或静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
- 调用Class.forName(String className)
- new一个该类的对象时
- 执行Main方法的当前类
-
以下几种方式不会对类进行初始化
- 无静态代码块且无静态变量赋值语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
-
直接访问父类的静态变量,不会触发子类的初始化
-
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
-
数组的创建不会导致数组中元素的类进行初始化
-
final修饰的变量若赋值的内容需要执行指令才能得出结果,则会执行clinit方法进行初始化
3.类加载器
3.1什么是类加载器
-
类加载器(ClassLoader)是Java虚拟机提供给应用程序区实现获取类和接口字节码数据的技术
类加载器只参与加载过程中的字节码获取并加载到内存这一部分
3.2类加载器的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的
3.3JDK8及8以前版本的类加载器
3.3.1虚拟机底层实现
启动类加载器Bootstrap:加载Java中最核心的类
-
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供,由C++编写的类加载器
-
默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等
位置在
C:\Program Files\Java\jdk1.8.0_361\jre\lib
-
若想要运行Java程序,则需要在Java的基础运行环境的jre中运行
-
当运行Bootstrap类加载器时,因为虚拟机是偏向底层应用的,而Java程序是偏向上层应用的,所以当运行该段程序时,会打印出null。(路径在
External Libraires -> <1.8> -> rt.jar -> java -> lang -> String
找到String类)public class ClassLoaderDemo { public static void main(String[] args) throws IOException { ClassLoader classLoader = String.class.getClassLoader(); System.out.println(classLoader); //使程序运行完不再退出 System.in.read(); } }
通过启动类加载器加载自己的jar包:
- 放入/jre/lib下进行扩展(不推荐,会出现文件名不符合jvm规范以致于文件名不匹配的问题)
- 使用参数进行扩展(推荐,使用-Xbootclasspath/a:jar包路径/jar包名)
3.3.2Java中实现
- 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器
- 源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备目录或者指定jar包将字节码文件加载到内存中
- URLClassLoader:将指定jar包的字节码数据加载进来
- ClassLoader:规范了整个类加载器的执行过程
扩展类加载器Extension:加载扩展Java比较通用的类(不是特别重要)
-
扩展类加载器(Extension Class Loader)是JDK中提供的使用Java编写的类加载器
-
默认加载Java安装目录/jre/lib/ext下的类文件
位置在
C:\Program Files\Java\jdk1.8.0_361\jre\lib\ext
通过扩展类加载器加载自定义jar包
-
放入/jre/lib/ext下进行扩展(不推荐,尽可能不要更改JDK安装目录中的内容)
-
使用参数进行扩展(推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,但是会覆盖掉原始目录
windows:“-Djava.ext.dirs=原始jar包目录;自定义jar包目录”
macos/Linux:“-Djava.ext.dirs=原始jar包目录:自定义jar包目录”)
应用程序类加载器Application:加载应用classpath使用的类文件/第三方依赖中的字节码文件
- 应用程序类加载器Application加载的jar包覆盖了启动类加载器Bootstrap和扩展类加载器Extension
3.3.3双亲委派机制(解决类到底由谁加载)
3.3.3.1双亲委派机制的作用
-
保证类加载的安全性
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
-
避免重复加载
双亲委派机制让一个类只能被同一个类加载器加载,就可以避免同一个类被多次加载,减少加载过程中的性能开销
3.3.3.2类加载的双亲委派机制
-
当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
-
向下委派加载起到了加载优先级的作用
-
当类加载器发现一个类在自己的加载路径中就会去加载这个类,若没有发现,则从下往上继续查找是否被加载过,若发现被加载了则直接将这个类进行加载,若一直到最顶层的类加载器都没有被加载,则由顶向下进行加载。
-
若所有类加载器都无法加载该类,则会抛出类无法找到的错误
3.3.3.3双亲委派优先级
- 若一个类重复出现在三个类加载器的加载位置,则由启动类加载器加载,根据双亲委派机制,其优先级是最高的
- 若项目中创建java.lang.String类,不会被加载,因为根据自底向上的查找类加载器过程中会发现启动类加载器早就加载了
3.3.3.4主动加载一个类
-
使用Class.forName方法,使用当前类的类加载器去加载指定的类
-
获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
//获取main方法所在类的类加载器,应用程序类加载器 ClassLoader classLoader = Demo.class.getClassLoader(); System.out.println(classLoader); //使用应用程序类加载器加载 com.aaa.bbb.CCC Class<?> clazz = classLoader.loadClass("com.aaa.bbb.CCC"); System.out.println(clazz.getClassLoader());
3.3.3.5父类加载器
-
每个java实现的类加载器中保存类一个成员变量叫"父"(Parent)类加载器,可以理解为是其上级而不是继承关系
-
应用程序类加载器的父类加载器是扩展类加载器
-
扩展类加载器的父类因为启动类加载器使用C++编写,获取不到,所以为null
-
启动类加载器使用C++编写,没有父类加载器
3.3.3.6双亲委派机制的优势
- 避免恶意代码替换JDK中的核心类库,确保核心类库的完整性和安全性
- 避免一个类重复地加载
3.3.4打破双亲委派机制
3.3.4.1自定义类加载器(重写findClass方法)
- Tomcat程序中可以运行多个Web应用,若两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且两个类不是同一个类
- 若不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中的MyServlet类就无法被加载了
- Tomcat使用了自定义类加载器来实现应用之间类的隔离,每个应用都会有一个独立的类加载器加载对应的类,就不再执行双亲委派机制
-
ClassLoader中包含了4个核心方法
-
public Class<?> loadClass(String name)
类加载的入口,提供了双亲委派机制。内部会调用findClass
-
public Class<?> findClass(String name)
由类加载器子类实现,用来加载字节码文件中的信息,获取二进制数据调用defineClass
URLClassLoader会根据文件路径去获取类文件中的二进制数据
-
protected final Class<?> defineClass(String name,byte[] b,int off,int len)
做一些类名的校验,调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
执行到这,类加载阶段执行完毕
-
protected final void resolveClass(CLass<?> c)
执行类生命周期的连接阶段
-
-
核心代码
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull //否则调用父类加载器的加载方法 if(parent != null){ c=parent.loadClass(name,false); }else{ //调用启动类加载器加载类 c=findBootstrapClassOrNull(name); } //若父类加载器都未加载成功,则由本类加载器加载 if(c==null) c=findClass(name);
-
自定义类加载器不手动设置parent的话,则默认父类是由getSystemClassLoader()方法设置,该方法返回一个应用程序类加载器AppClassLoader
重写private ClassLoader(Void unused, ClassLoader parent)方法完成自定义父类
-
两个自定义类加载器加载相同限定名的类,不会冲突,因为在同一个java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类
3.3.4.2线程上下文类加载器(由启动类加载器委派应用程序类加载器去加载类)
-
JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动
-
DriverManager类位于rt.jar包中,由启动类加载器加载
-
依赖中的mysql驱动对应的类,由应用程序类加载器加载
-
DriverManage使用SPI机制
(Service Provider Interface,是JDK内置的服务提供发现机制
工作原理:
- 在ClassPath路径下的META-INF/service文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现
- 使用ServiceLoader加载实现类),最终加载jar包中对应的驱动类
-
SPI中使用了线程上下文中保存的类加载器进行类的加载,而且该类加载器一般是应用程序类加载器
-
使用自定义类加载器加载:Thread.currentThread().setContextClassLoader(自定义类加载器名)
线程上下文类加载器打破了双亲委派机制
? 因为这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
线程上下文类加载器未打破双亲委派机制
? 因为JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,依旧是通过启动类加载器向下委派直至委派给应用程序类加载器去加载类,类的加载依然遵循双亲委派机制
3.3.4.3OSGi模块化
- OSGi模块化框架,存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能
- 热部署功能就是指在服务不停止的情况下,动态地更新字节码文件到内存中
3.4JDK8之前及JDK9之后的类加载器对比
-
JDK8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java
-
JDK9引入了module的概念,类加载器在设计上发生了变化
-
启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中
Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一
-
扩展类加载器被替换成了平台类加载器(Platform Class Loader)
平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件
平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑
-
4.运行时数据区域
4.1总览
-
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区
- 线程共享:每个线程都可以获取数据并使用,存在线程安全的问题
- 线程不共享:每当创建一个线程后,就会有一个程序计数器、Java虚拟机栈、本地方法栈,提高安全性
4.2程序计数器
- 也叫PC寄存器,每个线程都会通过程序计数器记录当前要执行的字节码指令的地址
- 控制程序指令的执行,实现分支、跳转、异常等逻辑
- 在多线程执行的情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续运行
4.2.1是否会发生内存溢出
- 内存溢出指的是程序正在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限
- 程序计数器中的每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的
- 无需对程序计数器做任何处理
4.3Java虚拟机栈
-
采用栈的数据接口来管理方法调用中的基本数据,先进后出,每一个方法的调用都使用一个栈帧来保存
-
若抛出异常,则会在控制台打印出当前栈中所保存的各个栈帧的情况,获取到栈帧里面保存的对应的方法名
-
Java虚拟机栈是随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈
4.3.1栈帧的组成
4.3.2局部变量表LocalVariableTable
- 局部变量表的作用是在运行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容
- 字节码文件中的局部变量表
- Nr.:局部变量的编号
- 起始PC:从哪一行字节码指令开始可以去访问该局部变量
- 长度:该局部变量的长度,限制生效范围
- 序号:对应的槽的起始序号
- 名字:局部变量存放的内存地址以及局部变量名
- 栈帧中的局部变量表是一个数组,数组中的每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽
- 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址
- 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量
- 局部变量表中的槽为了节省空间,是可以复用的,当一旦某个局部变量不再生效,则当前槽就可以再次被使用
4.3.3操作数栈
- 操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。是一种栈式的数据结构,若一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值
- 在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小
4.3.4帧数据
- 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系
- 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址
- 异常表存放的是代码中出现异常或未出现异常的处理信息,包含了try代码块和catch代码块执行后跳转到的字节码指令位置
4.3.5栈内存溢出
- Java虚拟机栈若栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出
- Java虚拟机栈内存溢出时会出现StackOverflowError的错误
4.3.5.1虚拟机栈默认大小
若不指定栈的大小,JVM将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构
4.3.5.2设置虚拟机栈的大小
若要修改Java虚拟机栈的大小,可以使用虚拟机参数-Xss
语法:-Xss栈大小
单位:字节(默认,必须是1024的倍数)、K或者K(KB)、m或者M(MB)、g或者G(GB)
4.3.5.3注意事项
-
与-Xss类似,也可以使用-XX:ThreadStackSize调整标志来配置堆栈大小
格式为:-XX:ThreadStackSize=1024
-
HotSpot JVM对栈大小的最大值和最小值有要求
Windows(64位)下的JDK8测试最小值为180k,最大值为1024m
-
局部变量过多、操作数栈深度过大也会影响栈内存大小
-
一般情况下,即便使用了递归,栈的深度最多只能到几百,不会出现栈的溢出。
参数可以手动指定为-Xss256k节省内存
4.4本地方法栈
- Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧
- 在HotSpot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。
- 本地方法栈会在栈内存上生成一个栈帧临时保存方法的参数,同时方便出现异常时也把本地方法的栈信息打印出来
4.5堆
-
Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上
-
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享
4.5.1堆空间
- 堆空间有三个需要关注的值
- used --> 当前已使用的堆内存
- total --> Java虚拟机已经分配的可用堆内存
- max --> Java虚拟机可以分配的最大堆内存
- 使用dashboard命令查看,可以手动指定刷新频率(不指定默认5秒一次):dashboard -i 刷新频率(毫秒)
- 随着堆中的对象增多,当total可以使用的内存即将不足时,Java虚拟机会将继续分配内存给堆
- 若堆内存不足,Java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等
4.5.2配置堆空间参数
- 若不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64,在实际应用中一般都需要设置total和max的值
- 使用虚拟机参数 -Xmx(max最大值)和-Xms(初始的total)
- 语法:-Xmx值 -Xms值
- 单位:字节(默认,必须是1024的倍数)、k或者k(KB)、m或者M(MB)、g或者G(GB)
- 限制:Xmx必须大于2MB,Xms必须大于1MB
- arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回收期有关,计算的是可以分配对象的内存,而不是整个内存
- Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值(total==max),在程序启动后可使用的总内存就是最大内存,而无需向Java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况
- -Xmx具体设置的值与实际的应用程序运行环境有关
4.6方法区
4.6.1方法区的组成
- 方法区存放的是基础信息的位置,线程共享,主要包含三部分内容:
- 类的元信息–>保存了所有类的基本信息
- 运行时常量池–>保存了字节码文件中的常量池内容
- 字符串常量池–>保存了字符串常量
4.6.2方法区的作用
-
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象,在类的加载阶段完成
-
方法区除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容
-
字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池
当常量池加载到内存之中后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池
-
方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同
HotSpot设计如下:
- JDK7及之前的版本将方法区存放在堆区域中的永久代空间,有最大值内存空间,堆的大小由虚拟机参数来控制(查看ps_perm_gen)
- JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,占用的是操作系统上的空间,无最大值内存空间,默认情况下只要不超过操作系统承受的上限,可以一直分配(查看metaspace)
4.6.3JDK7和JDK8的虚拟机参数
- JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数
-XX:MaxPermSize=值
来控制 - JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用**-XX:MaxMetaspaceSize=值**将元空间最大大小进行限制
4.7字符串常量池
-
方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)
-
字符串常量池存储在代码中定义的常量字符串内容。比如“123”就会被放入字符串常量池
4.7.1字符串常量池和运行时常量池
4.7.2intern方法
-
String.intern()可以手动将字符串放入字符串常量池中
String a="1"; String b="2"; String c="12"; //由于在底层中使用了StringBuilder, //最后使用了new String,所以d的值在堆中 String d=a+b; //是直接通过在字符串常量池中相加, //所以e的值还是在字符串常量池中 String e="1"+"2"; ----------------------------------------------------------- String input1 =scanner.next().intern(); String input2 =scanner.next().intern(); //若input1和input2输入的值相同,则会相等 input1 == input2;// true
-
JDK6版本的intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代中字符串的引用。JVM启动时会把Java加入到常量池中
JDK6及之前的版本,静态变量是存放在方法区中,也就是永久代
-
JDK7及之后版本中由于字符串常量池在堆中,所以intern()方法会将第一次遇到的字符串的引用放入字符串常量池中
JDK7及之后的版本,静态变量是存放在堆中的Class对象,脱离了永久代
4.8直接内存
-
直接内存并不在《Java虚拟机规范》中,不属于Java运行时的内存区域
使用直接内存,是为了解决:
-
Java堆中的对象若不再使用要回收,回收时会影响对象的创建和使用
-
IO操作比如读文件,需要将文件读入直接内存(缓存区)再把数据复制到Java堆中
现在直接放入直接内存,同时Java堆上维护直接内存的引用,减少了数据复制的开销
-
JDK8之后,主要为了保护在方法区中的数据
-
4.8.1配置直接内存
-
若需要手动调整直接内存的大小,可以使用**-XX:MaxDirectMemorySize=大小**
-
单位k或k表示千字节,m或M表示兆字节,g或G表示千兆字节。
默认不设置该参数情况下,JVM自动选择最大分配的大小
5.自动垃圾回收
5.1内存泄漏
- 内存泄漏指的是不再使用的对象在系统中未被回收
- 内存泄漏的积累可能会导致内存溢出
5.2Java的内存管理
-
为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection 简称GC)机制
通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行会后
5.3自动垃圾回收/手动垃圾回收
5.3.1自动垃圾回收
-
自动根据对象是否使用由虚拟机来回收对象
- 优点:降低程序员实现难度、降低对象回收bug的可能性
- 缺点:程序员无法控制内存回收的及时性
-
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。
而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存
5.3.2手动垃圾回收
- 由程序员编程实现对象的删除
- 优点:回收及时性高,由程序员把控回收的时机
- 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
5.4自动垃圾回收场景
-
解决系统僵死的问题
大厂的系统出现的许多系统僵死问题都与频繁的垃圾回收有关
-
性能优化
对垃圾回收器进行合理的设置可以有效地提升程序的执行性能
-
高频面试题
常见的垃圾回收器、常见的垃圾回收算法、四种引用、项目中有用哪一种垃圾回收器
5.5方法区的回收
-
方法区中能回收的内容主要就是不再使用的类
判定一个类可以被卸载,需要同时满足下面三个条件:
-
此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
Class<?> clazz = loader.loadClass("com.a.b.C"); Object o = clazz.newInstance(); o = null;
-
加载该类的类加载器已经被回收
URLClassLoader loader = new URLClassLoader( new URL[] {new URL("file:D:\\lib\\")} ); loader=null;
例:每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
-
该类对应的java.lang.Class对象没有在任何地方被引用
Class<?> clazz = loader.loadClass("com.a.b.C"); clazz = null
- 打印类被加载的日志:-XX:+TraceClassloading
- 打印类被卸载的日志:-XX:+TraceClassUnloading
-
若需要手动触发垃圾回收,可以调用System.gc()方法
-
语法:System.gc();
-
注意事项:
调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断
5.6堆回收
如何判断堆上的对象可以回收
- 根据对象是否被引用决定,若对象被引用了,则说明该对象还在使用,不允许被回收
5.6.1引用计数法和可达性分析法
5.6.1.1引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
-
优点:实现简单
-
缺点:
-
每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
-
存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
A、B实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏
-
查看垃圾回收日志的信息:使用 -verbose:gc参数
5.6.1.2可达性分析法
- 优点:
- 解决了引用计数法的循环引用问题
- 不需要在对对象的引用发生变化时增加或减少引用的技术
可达性分析算法将对象分为两类,且对象与对象之间存在引用关系:
- 垃圾回收的根对象(GC Root),GC Root一般不会被回收
- 普通对象
可达性分析算法指的是如果从某个普通对象到GC Root对象是可达的(也就是普通对象能通过引用链找到GC Root对象),对象就不可被回收;若引用链不存在就可以被回收。
5.6.1.3GC Root对象
-
Thread线程对象,引用线程栈帧中的方法参数、局部变量等
比如main主线程Thread对象
-
System Class系统类加载器加载的java.lang.Class对象,引用类中的静态变量
比如sun.misc.Launcher系统类加载器
-
Busy Monitor监视器对象,用来保存同步锁synchronized关键字持有的对象
-
JNI Global本地方法调用时使用的全局对象
5.6.1.4查看GC Root
通过arthas和eclipse Memory Analyzer(MAT)工具查看GC Root
- 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
- 使用MAT工具打开堆内存快照文件
- 选择GC Roots功能查看所有的GC Root
5.6.2五种对象引用
-
**强引用:**即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收;通过强引用引用的对象就是不可被回收的,可以被保留
-
**软引用:**相对于强引用是一种比较弱的引用关系,当程序使用完一个对象后,就会解除强引用对象;若一个对象只有软引用关联到它,当程序内存不足时,就会将引用中的数据进行回收,释放一定的堆内存;
在JDK1.2版之后提供了SoftReference类来实现软引用,常用于缓存
-
弱引用:与软引用的整体机制基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收
-
虚引用:也叫幽灵/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现
-
终结器引用:指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。
5.6.2.1软引用的执行过程
- 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
- 内存不足时,虚拟机尝试进行垃圾回收
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象
- 如果依然内存不足,抛出OutOfMemory异常
byte[ ] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[ ]> softReference = new SoftReference<byte[ ]>(bytes);
System.out.println(softReference.get());
5.6.2.2内存不足如何回收掉软引用对象的数据
SoftReference提供了一套队列机制
- 软引用创建时,通过构造器传入引用队列
- 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
- 通过代码遍历引用队列,将SoftReference的强引用删除
-
软引用也可以使用继承自SoftReference类的方式来实现,StudentRef类就是一个软引用对象
通过构造器传入软引用包含的对象,以及引用队列
5.6.2.3实战-软引用实现学生数据的缓存
5.6.2.4弱引用的执行过程
byte[ ] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[ ]> weakReference = new WeakReference<byte[ ]>(bytes);
bytes=null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
5.6.3垃圾回收算法
5.6.4垃圾回收器
ByteBuddy框架
-
引入依赖
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.12.23</version> </dependency>
-
创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0);
-
调用visit方法,创建字节码数据
classWriter.visit(Opcodes.v1_7,Opcodes.ACC_PUBLIC,name,null,"java/lang/Object",null); byte[] bytes=classWriter.toByteArray();
Arthas
Arthas常用命令
参数列表:https://arthas.aliyun.com/doc/commands.html
-
**
dashboard:
**dashboard -i 2000 -n 1:每隔两秒对运行的程序进行监控并输出到屏幕,一共执行3次 -
**
memory:
**展示当前运行的程序的内存 -
**
dump:
**dump对已加载类的字节码文件到特定目录,dump -d 文件存放的路径 包名.文件名 -
**
jad:
**jad反编译已加载类的源码,jad 包名.类名 -
**
sc:
**搜索出所有已经加载到JVM中的Class信息,输出java.lang.String的类信息:sc -d java.lang.String -
**
heapdump:
**heapdump 目录路径 文件名.hprof 将堆内存快照保存到本地磁盘中 -
**
classloader:
**classloader查看类加载器的继承树,urls,类加载信息,使用classloader区getResource
查看所有的类加载器的hash值:classloader -l,查看指定的classloader的jar包:classloader -c hash值,查看类加载器的父子关系:classloader -t
Arthas不停机解决线上问题(热部署)
-
在有问题的服务器上部署一个arthas,并启动
-
jad --source-only 类全限定名 > 目录/文件名.java
jad命令反编译,然后可以用其它编辑器,比如vim来修改源码
-
mc -c 类加载器的hashcode 目录/文件名.java -d 输出目录
mc命令用来编译修改过的代码
-
retransform class文件所在目录/xxx.class
用retransform命令来重新加载字节码文件加载到内存中,起到了刷新的作用
注意:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!