网站短信接口怎么做,鲜花网网站开发的意义,江夏网站建设,加盟型网站建设Java虚拟机#xff08;JVM#xff09;从入门到实战【上】#xff0c;涵盖类加载#xff0c;双亲委派机制#xff0c;垃圾回收器及算法等知识点#xff0c;全系列6万字。
一、基础篇
P1 Java虚拟机导学课程 P2 初识JVM
什么是JVM
Java Virtual Machine 是Java虚拟机。…Java虚拟机JVM从入门到实战【上】涵盖类加载双亲委派机制垃圾回收器及算法等知识点全系列6万字。
一、基础篇
P1 Java虚拟机导学课程 P2 初识JVM
什么是JVM
Java Virtual Machine 是Java虚拟机。
JVM本质上是一个运行在计算机上的程序职责是运行Java字节码文件。
因为计算机只能运行机器码所以Java虚拟机负责将字节码转化为机器码。
JVM可以自动为对象和方法分配内存具有自动的垃圾回收机制回收不再使用的对象。
JVM的功能
JVM包含内存管理、解释执行虚拟机指令、即时编译三大功能。 功能1即时编译
Java语言如果不做优化性能不如C和C语言因为C类语言可以将源代码文件直接通过编译和链接转化为机器码文件。 Java多了一步实时解释目的是为了能够支持跨平台特性将字节码指令解释为不同平台的机器码文件。 热点代码就是多次反复出现的代码会被优化保存到内存中再次执行可以直接调用。
常见的JVM P3 Java虚拟机的组成
1.类加载器把字节码文件的内容加载到内存中。
2.运行时数据区域JVM管理的内存负责管理JVM使用到的内存比如创建对象和销毁对象。
3.执行引擎即时编译器、解释器、垃圾回收器。执行引擎负责本地接口的调用。
4.本地接口。native方法用C编写。 字节码文件的组成
P4 正确打开字节码文件
字节码文件中保存了源代码编译之后的内容以二进制方式存储无法用记事本直接打开阅读。
可以通过NotePad使用十六进制插件查看class文件 推荐使用jclasslib工具查看字节码文件。
P5 基础信息
1.基础信息包含魔数、字节码文件对应的Java版本号访问标识。父类和接口。
文件是无法通过文件扩展名来确定文件类型的文件扩展名可以随意修改但不影响文件的内容。
文件是通过文件的头几个字节去校验文件的类型如果软件不支持该种类型就会出错。
Java字节码文件中将文件头称为magic魔数。 主副版本号指的是编译字节码文件的JDK版本号主版本号用来标识大版本号。注意JDK1.2版本号是46之后每升级一个大版本就加1。所以1.2之后大版本号计算方法是主版本号-44。比如主版本号为5252-448主版本号52就是JDK8。
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。
报下面错误是兼容性出现问题。 方法1升级IDEA编译的Jdk版本。容易引发其它的兼容性问题
方法2改变依赖的版本替换包名降低版本。工作中推荐选用该方法
2.常量池保存了字符串常量类或接口名字段名主要在字节码指令中使用。
3.字段当前类或接口声明的字段信息。
4.方法当前类或接口声明的方法信息字节码指令。
5.属性类的属性比如源码的文件名内部类的列表等。
P6 常量池和方法
常量池作用避免相同内容重复定义节省空间。
在常量池中存放1份字符串在别处引用节省空间。
常量池中的数据都有一个编号编号从1开始。在字段或字节码指令中通过编号可以快速的找到对应的数据。
字节码指令中通过编号引用到常量池的过程称之为符号引用。
i0;ii问i的值为答案0 iconst_值把操作数的值放入到操作数栈中。
istore_下标弹出会把操作数栈中的数据弹出存放到局部变量表下标对应的数组中。操作数栈-局部变量表。
iload_下标将局部变量表下标中的数据放入操作数栈。局部变量表-操作数栈。 iinc 1 by 1将局部变量表中1号位置上的数据1。 P7 字节码文件常见工具使用1
javap -v javap是JDK自带的反编译工具可以通过控制台查看字节码文件的内容适合在服务器上看字节码文件内容。
使用步骤
1.如果是jar包需要先使用jar -xvf命令解压。
2.输入javap -v 字节码文件名称查看具体的字节码信息。
如果想查看哪个文件的字节码只需要javap -v 绝对路径即可。
下载一个jclasslib Bytecode Viewer选中源代码文件选择下面 可以查看字节码 P8 字节码文件常见工具使用2
阿里的arthasArthas是一款线上监控诊断产品通过全局视角实时查看应用的load、内存、gc、线程的状态信息并能在不修改应用代码的情况下对业务问题进行诊断大大提升线上问题排查的效率。
启动java -jar arthas-boot.jar会出现进程id输入想进入的进程id。 监控面板查看字节码信息方法监控类的热部署线上某个类有问题可以在不停机的情况下把类的代码替换掉内存监控垃圾回收监控应用热点定位。
当前系统的实时数据面板按ctrlc退出。 cls可以清除所有命令。
dashboard -i 2000 -n 3 隔2秒执行3次。 第1部分展示了每个线程的信息第2部分展示了内存区第3部分是运行中的配置信息。
dump 类的全限定名dump已加载类的字节码文件到特定目录。
jad 类的全限定名反编译已加载类的源码。
jad 包名.类名 通过arthas可以获取到当前运行的状态和字节码信息甚至是反编译出来的源代码信息。 P9 类的生命周期加载阶段
总结根据类的全限定名把字节码文件的内容加载并转换成合适的数据放入内存中存放在方法区和堆上。 类的生命周期描述了一个类加载、使用、卸载的整个过程。
加载连接初始化使用卸载。 1.加载阶段第一步是类加载器根据类的全限定名通过不同渠道本地文件通过网络传输的类动态代理生成以二进制流的方式获取字节码信息。
2.类加载器在加载完类之后Java虚拟机会将字节码中的信息保存到方法区中。
3.生成一个InstanceKlass对象保存类的所有信息里面还包含实现特定功能比如多态的信息。 4.Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中获取类的信息以及存储静态字段的数据。 对于开发者来说只需要访问堆中的Class对象而不需要访问方法区中所有信息。
方法去是用C代码写堆区是java代码编写在代码中可以获取到。
把方法区中能让开发者访问的资源拷贝到堆区中Java虚拟机能很好控制好开发者访问数据的范围。开发者不能访问方法区提升了安全性。 P10 类的生命周期连接阶段
总结连接阶段对魔数、版本号等进行验证一般不需要程序员关注。准备阶段为静态变量分配内存并设置初始值。解析阶段将常量池中的符号引用编号替换为直接引用内存地址。
连接
1.验证验证内容是否满足Java虚拟机规范。 major是主版本号常量一般是45对jdk1.8来说最高版本号是52对jdk8只能支持45-52之间的主版本号。副版本号不能大于0 2.准备给静态变量赋初值。
准备阶段为静态变量static分配内存并设置初始值。 特殊情况final修饰的基本数据类型的静态变量准备阶段直接会将代码中的值进行赋值。 3.解析将常量池中的符号引用替换成指向内存的直接引用。
直接引用不再使用编号而是使用内存中的地址进行访问具体的数据。
P11 类的生命周期初始化阶段
总结执行静态代码块和静态变量的赋值。
初始化阶段会执行静态代码块中的代码并为静态变量赋值。
初始化阶段会执行字节码文件中clinitclass init类的初始化部分的字节码指令。 putstatic是给类中的静态字段赋值。静态字段的名字会从常量池中获取。值会从操作数栈中弹出。将操作数栈中的值赋值给常量池中的变量。
clinit方法中的执行顺序与Java中编写的顺序是一致的。
以下几种方式会导致类的初始化
1.访问一个类的静态变量或者静态方法如果变量时final修饰的并且等号右边是常量不会触发初始化因为在连接阶段会直接赋常量值
2.调用Class.forName(String className)
3.new一个该类的对象时。
4.执行Main方法的当前类。
ldc #9是从常量池中将字符串D加载到操作数栈中。 invokevirtual是调用Println方法打印操作数栈上的内容。
clinit指令在特定情况下不会出现
1.无静态代码快且无静态变量赋值语句。
2.有静态变量的声明但没有赋值语句。
3.静态变量的定义使用final关键字这类变量会在准备阶段直接进行初始化。
注意下面
1.直接访问父类的静态变量不会触发子类的初始化
2.子类的初始化clinit调用之前会先调用父类的clinit初始化方法。
下面这题因为B02是A02的子类所以会先调用A02的方法再调用B02的方法。 如果去掉new因为a是在A02中所以直接访问A02中的变量即可。
3.数组的创建不会导致数组中元素的类进行初始化。
4.final修饰的变量如果赋值的内容需要执行指令才能得出结果会执行clinit方法进行初始化。
P12 类加载器的分类
类加载器ClassLoader是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
类加载器负责在类加载过程中的字节码获取并加载到内存中。通过加载字节码数据放入内存转换成byte[]接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据。
本地接口JNI是Java Native Interface的缩写允许java调用其它语言编写的方法。在hotspot类加载器中主要用于调用Java虚拟机中的方法这些方法使用C编写。 企业级应用SPI机制类的热部署Tomcat类的隔离。大量的面试题什么是类的双亲委派机制如何打破类的双亲委派机制自定义类加载器。解决线上问题使用Arthas不停机修复BUG解决线上故障。
类加载器被分为2部分。 JDK9之后出现模块化所以JDK9是分水岭。 P13 启动类加载器 Bootstrap加载Java中最核心的类。
启动类加载器Bootstrap ClassLoader是由Hotspot虚拟机提供的使用C编写的类加载器。
默认加载Java安装目录/jre/lib下的类文件。
rt.jar是最核心的jar包。stringintegerlong日期类。
再Arthas中选择BootstrapClassLoaderDemo输入sc -d 类名sc是search class的简称用来查看jvm已加载的类信息。-d可以输出当前类的详细信息加载ClassLoader等详细信息。 如何让启动类加载器去加载用户jar包
1.把要扩展的类打成jar包放入jre/lib下进行扩展不推荐会要求名称符合规范。
2.使用参数进行扩展。推荐使用-Xbootclasspath/a:jar包目录/jar包名进行扩展。
P14 扩展和应用程序类加载器
扩展类加载器和应用程序类加载器都是JDK提供的使用Java编写的类加载器。
它们源码都位于sun.misc.Launcher中是一个静态内部类。继承自URLClassLoader。或者指定jar包将字节码文件加载到内存中。 默认加载Java安装目录/jre/lib/ext下的文件。
通过扩展类加载器去加载用户jar包
1.放入/jre/lib/ext下进行扩展。
2.使用参数进行扩展。推荐使用-Djava.ext.dirsjar包目录进行扩展这种方式会覆盖掉原始目录可以用;(windows):(macos/linux)追加上原始目录。 应用程序类加载器加载的内容包含启动类加载器和扩展类加载器。
P15 双亲委派机制
Java虚拟机中有多个类加载器双亲委派机制的核心是解决一个类到底由谁加载的问题。
1.保证类加载的安全性通过双亲委派机制避免恶意代码替换JDK中的核心类库。
2.避免重复加载双亲委派机制可以避免同一个类多次加载。
双亲委派机制当一个类加载器接收到加载类的任务时会自底向上查看类是否加载过如果加载过加载流程就结束了把类的class对象返回即可如果所有的加载器都没加载过就会层层向上委派查看是否加载过如果都没加载过就会由顶向下尝试进行加载如果一个类加载器发现这个类在自己的加载路径中就会选择去加载这个类。 一个类优先由启动类加载器加载加载不了才交给扩展类加载器处理。因为底层代码是用C编写。
如果类加载器返回的是null说明是启动类加载器加载因为启动类加载器底层是用C编写。
每个Java实现的类加载器中保存了一个成员变量叫“父”Parent类加载器可以理解为它的上级不是继承关系。 下面很重要
1.先描述双亲委派机制的流程。
2.然后描述类加载器之间的关系。
3.双亲委派机制的好处 P16 打破类的双亲委派机制 自定义类加载器
为什么打破比如一个Tomcat程序中可以运行多个Web应用如果两个应用中出现了相同限定名的类比如Servlet类Tomcat要保证这两个类都能加载并且它们应该是不同的类。
如果不打破双亲委派制当应用类加载器加载Web应用1中的MyServlet之后Web应用2中相同限定名的MyServlet类就无法被加载。
Tomcat为每一个web应用都单独生成了一个类加载器。 ClassLoader的原理
1. loadClass方法是类加载的入口提供了双亲委派机制内部会调用findClass。根据全限定名去找到类并把类的二进制信息加载进来。
2. findClass由类加载器子类实现获取二进制数据调用defineClass比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。
3. defineClass在堆和方法区上创建包含类信息的对象。做一些类名的校验然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中。
4.resolveClass执行类生命周期中的连接阶段。 通过loadClassData方法传递类的全限定名找到字节码文件加载到内存中变成二进制数组。
byte[] data loadClassData(name); 调用defineClass把二进制数组传入在堆和方法区生成对应数据完成加载阶段。
return defineClass(name,data,0,data.length);
如果不给自定义类加载器定义parent它会默认parent为应用程序类加载器。 如果没传入parent会自动默认传入系统类加载器。 P17 打破类的双亲委派机制 线程上下文类加载器
JDBC使用了DriverManager来管理项目中引入的不同数据库的驱动比如mysql、oracle驱动。
DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载这就违反了双亲委派机制。 DriverManager怎么知道jar包中要加载的驱动在哪里
spi全称为Service Provider Interface是JDK内置的一种服务提供发现机制。
spi的工作原理
1.驱动jar包中在ClassPath路径下的META-INF/service文件夹中以接口的全限定名来命名文件名对应的文件里面写该接口的实现。
2.使用ServiceLoader加载实现类 在驱动jar包中暴露出要让别人加载的类放到固定的文件中在META-INF/service下的文件中通过全限定名暴露接下来在DriverManager中就会去使用这个ServiceLoader去加载文件中的类名然后用类加载器去加载对应的类创建对象。
SPI中如何获取到应用程序类加载器的DriverManager是由启动类加载器加载它怎么拿到应用程序类加载器
因为SPI中使用了线程上下文中保存的类加载器进行类的加载这个类加载器一般是应用程序类加载器。 1.启动类加载器加载DriverManager
2.在初始化DriverManager时通过SPI机制加载jar包中的mysql驱动
3.SPI中利用了线程上下文类加载器应用程序类加载器去加载类并创建对象。
这种有启动类加载器加载的类委派应用程序类加载器去加载类的方式打破了双亲委派机制。
是否打破双亲委派机制 没有打破双亲委派机制。因为JDBC只是在DriverManager加载完之后通过初始化阶段出发了驱动类的加载类的加载依然遵循双亲委派机制。
P18 打破双亲委派机制 osgi和类的热部署
同级之间的类加载器互相委托加载。OSGI还是用类加载器实现了热部署在服务不停止的前提下更新字节码文件到内存中的功能。 1.jad命令反编译然后可以使用其它编译器比如vim来修改源码。
jad --source-only com.itheima.springbootclassfile.controller.UserController /opt/jvm/UserController.java 2.记得添加-c参数让类加载器去编译。mc命令用来编辑修改过的代码。
mc -c 21b8d17c /opt/jvm/UserController.java -d /opt/jvm
3.用retransform命令加载新的字节码
retransform /opt/jvm/com/itheima/springbootclassfile/controller/UserController.class
注意事项
1.程序重启之后字节码文件会恢复除非将class文件放入jar包中进行更新。
2.使用retransform不能添加方法或字段也不能更新正在执行的方法。
P19 JDK9之后的类加载器
在JDK8及之前的版本中扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java
JDK9引入了module的概念类加载器在设计上发生了很多变化。
1.启动类加载器使用Java编写位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到返回的仍然是null保持了统一。
2.扩展类加载器被替换为平台类加载器。
平台类加载器遵循模块化方式加载字节码文件所以继承关系丛URLClassLoader变成了BuiltinClassLoaderBuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容本身没有特殊的逻辑。
P20 运行时的数据区程序计数器
Java虚拟机在运行Java程序过程中管理的内存区域称之为运行时数据区。
线程不共享创建一个线程每个线程里都有一份程序计数器、Java虚拟机栈、本地方法栈对应的数据自己的数据由自己维护其它线程不能访问对方线程中的数据。
线程共享放入任何数据每个线程都能访问数据共享。 应用场景Java的内存分成哪几部分详细介绍一下。
Java内存中哪些部分会内存溢出
JDK7和8在内存结构上的区别是什么
工作中的实际问题内存溢出。
内存调优的学习路线
1.了解运行时内存结构了解JVM运行过程中每一部分的内存结构以及哪些部分容易出现内存溢出。
2.掌握内存问题的产生原因学习代码中常见的几种内存泄露性能问题的常见原因。
3.掌握内存调优的基本方法学习内存泄露性能问题等常见JVM问题的常规解决方法。
程序计数器Program Counter Register也叫PC寄存器每个线程会通过程序计数器来记录接下来要执行的字节码指令的地址。
ifne 9 意思是将操作数栈中的数与0进行比较如果相等执行6如果不相等执行9。 程序计数器记录的是下一行字节码指令的地址假如当前执行的是1那程序计数器中记录的就是2。
程序计数器可以控制程序指令的进行实现分支、跳转、异常等逻辑。
在多线程的情况下Java虚拟机需要通过程序计数器线程不共享记录CPU切换前执行到哪一句指令并继续解释运行。 程序计数器在运行中会出现内存溢出吗
内存溢出指的是程序在使用某一块内存区域时存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。
因为每个线程只存储一个固定长度的内存地址程序计数器不会发生内存溢出。程序员无需对程序计数器做任何处理。
P21 栈局部变量表
Java虚拟机栈Java Virtual Machine Stack采用栈的数据结构来管理方法调用中的基本数据。先进后出First In Last Out每一个方法的调用使用一个栈帧来保存。
栈帧用来保存方法的基本信息。 当某个方法执行完栈帧就会被弹出。
Java虚拟机栈随着线程的创建而创建而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行每个线程都有一个自己的虚拟机栈。
栈帧的组成
局部变量表作用是在运行过程中存放所有的局部变量。
操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域。
帧数据包含动态链接、方法出口、异常表的引用。
局部变量表作用是在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容。
Nr表示的是变量的编号按照生命的顺序起始PC保存了从哪一行字节码指令开始可以访问这个局部变量长度以生效那行到销毁那行计算。 栈帧中的局部变量表是一个数组数组中的每一个位置称之为槽slotlong和double类型占2个槽其他类型占用一个槽。 实例方法中的序号为0的位置存放的是this指的是当前调用方法的对象运行时会在内存中存放实例对象的地址。 方法参数也会保存在局部变量表中其顺序与方法中参数的定义顺序一致。
局部变量表保存的内容有实例方法的this对象方法的参数方法体中声明的局部变量。 为了节省空间局部变量表中的槽可以复用一旦某个局部变量不再生效当前槽可以被复用。 P22 栈操作数栈和帧数据
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构如果一条指令将一个值压入操作数栈则后面的指令可以弹出并使用该值。
在编译期就可以确定操作数栈的最大深度从而执行时正确的分配内存大小。 帧数据包含动态链接方法出口异常表的引用。
当前类的字节码指令引用了其它类的属性或者方法时需要将符号引用编号比如#10转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
方法出口指的是方法在正确或异常结束时当前栈帧会被弹出同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中需要存储此方法出口的地址。
异常表存放的是代码中异常的处理信息包含异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。 通过异常表可以知道要在什么范围内捕获异常如果出现异常要跳转到哪一行。
P23 栈内存溢出
Java虚拟机如果栈帧过多占用内存超过栈内存可以分配的最大大小就会出现内存溢出。
Java虚拟机内存溢出时会出现StackOverflowError的错误。
如果我们不指定栈的大小JVM将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。
Linux等操作系统一般是1MB。
一般一个线程的栈能容纳10000-11000个栈帧。创建一个方法会生成一个栈帧。 通过修改-Xss的参数可以让栈帧的大小调小 对windows来说JDK8测试最小值为180K最大值为1024M。
如果局部变量过多操作数栈的深度过大也会影响栈内存的大小。 Java虚拟机栈存储了Java方法调用时的栈帧而本地方法栈存储的是native本地方法的栈帧。
P24 堆内存
一般Java程序中堆内存是空间最大的一块的内存区域创建出来的对象都存在于堆上。
栈上的局部变量表中可以存放堆上对象的引用。静态变量也可以存放堆对象的引用通过静态变量就可以实现对象在线程之间的共享。 堆内存大小具有上限当一直向堆中放入对象达到上限之后会抛出OutOfMemory的错误。
可以通过dashboard 进行访问如果只能看内存只需要输入memory used是已经使用的堆内存total是总共能使用的堆内存max是虚拟机能分配的上限堆内存。 最后发现total还远没有达到max的量级就已经溢出了所以不是当usedmaxtotal的时候堆内存就溢出。
如果不设置虚拟机的参数max默认是系统内存的1/4total默认是系统内存的1/64。在实际应用中一般需要设置total和max的值。
-Xmx4g表示最大堆内存的大小-Xms4g表示total的大小。
为什么arthas中设置的heap堆大小与设置的值不一样呢 arthas中的heap堆内存使用了JMX技术中内存获取方式这种方式与垃圾回收器有关计算的是可以分配对象的内存而不是整个内存。
建议将-Xmx和-Xms设置为相同的值这样程序启动后可使用的总内存就是最大内存无需向java虚拟机再次申请减少了申请与分配内存时间上的开销也不会出现内存过剩后堆收缩的情况。
P25 方法区的实现
方法区是虚拟机中的虚拟概念每款Java虚拟机在实现上各不相同。
JDK7及之前的版本将方法区存放在堆区域中的永久代空间堆的大小由虚拟机参数控制。
JDK8之后的版本将方法区存放在元空间中元空间位于操作系统维护的直接内存占用操作系统的内存中默认情况下只要不超过操作系统承受的上限可以一直分配。 方法区是存放基础信息的位置线程共享主要包含三部分内容
1.类的元信息保存了所有类的基本信息。
方法区是用来存储每个类的基本信息元信息一般称为InstanceKlass对象。在类的加载阶段完成。
InstanceKlass对象包含基本信息常量池字段方法虚方法表。注意常量池和方法在虚拟机中会被单独摘出来用单独的内存去存放而在InstanceKlass中仅仅存储的是引用。
2.运行时常量池保存了字节码文件中的常量池内容。
方法区除了存储类的元信息外还存储了运行时的常量池。常量池中存放的是字节码中的常量池内容。
字节码文件通过编号查表的方式找到常量被称为静态常量池。当常量池加载到内存中后可以通过内存地址快速定位到常量池的内容这种叫作运行时常量池。 JDK7大概11万次方法区溢出JDK8运行上百万次程序也没有溢出。 3.字符串常量池保存了字符串常量。
P26 方法去字符串常量池
字符串常量池存储在代码中定义的常量字符串内容。
运行时常量池与字符串常量池被拆分因为JDK8之后方法区由永久代到元空间。
结果false 变量链接使用StringBuilderStringBuilder的底层toString方法是new了一个strinng所以放在堆。
因为字节码指令中dab涉及到new也就有对象产生存放在堆。
结果true String.intern()方法是可以手动将字符串放入字符串常量池。
JDK7之后版本中由于字符串常量池在堆上所以intern()方法会把第一次遇到的字符串的引用放入字符串常量池。 因为java是系统关键字会在启动时存放入字符串常量池。 JDK7之后版本中静态变量存放在堆中的Class对象中脱离了永久代。 P27 直接内存
JDK8后方法区的内容存在直接内存的元空间中。
直接内存不在虚拟机规范中存在所以并不属于java运行时的内存区域。
在JDK1.4后引入NIO机制使用直接内存。
Java堆中的对象如果不再使用要回收回收时会影响对象的创建和使用。
IO操作比如读文件需要先把文件读入直接内存缓冲区再把数据复制到Java堆中。
现在直接放入直接内存即可减少了一次数据复制的开销Java堆上维护直接内存的引用减少数据复制开销。 设置直接内存区的大小 P28 自动垃圾回收
内存泄露指的是不再使用的对象在系统中未被回收内存泄露的积累导致内存溢出。 Java中为了简化对象的释放引入了自动的垃圾回收Garbage Collection简称GC机制。通过垃圾回收器来对不再使用的对象完成自动的回收垃圾回收器主要负责对堆上内存进行回收。
优点降低程序员实现难度降低对象回收bug的可能性。
缺点程序员无法控制内存回收的及时性。
自动垃圾回收应用场景
1.解决系统僵死的问题。与频繁的垃圾回收有关。
2.性能优化。对垃圾回收器性能优化。
3.高频面试题常见的垃圾回收器
P29 方法区的回收
线程不共享的部分程序计数器、Java虚拟机栈、本地方法栈来说不需要使用垃圾回收机制进行回收。都是伴随线程的创建而创建线程的销毁而销毁。方法的栈帧在执行完之后会自动弹出栈并释放掉对应的内存。 方法区中能回收的内容主要就是不再使用的类。判断一个类可以被卸载需要同时满足下面三个条件
1.此类所以实例对象都被回收在堆中不存在任何该类的实例对象和子类对象。
2.加载类的类加载器已经被回收。
3.该类对应的java.lang.Class对象没有在任何地方被引用。 如果需要手动触发垃圾回收可以调用System.gc()方法。
调用System.gc()方法并不一定会立即回收垃圾仅仅向JAVA虚拟机发送一个垃圾回收的请求具体是否需要执行垃圾回收Java虚拟机会自行判断。
P30 引用计数法
Java中的对象能否被回收是根据对象是否被引用决定的。如果对象被引用了说明该对象还在使用不允许被回收。 执行main方法会在栈内存中创建一个栈帧。A a new A()创建的实例对象A会被保存在堆内存中。 判断堆上对象是否被引用有2种方法引用计数法和可达性分析法。
引用计数法会为每个对象维护一个引用计数器当对象被引用时1取消引用时-1。
引用计数法缺点1.每次引用和取消引用需要维护计数器对系统性能存在影响。2.存在循环引用问题当A引用BB引用A会导致对象无法回收。 因为A引用BB引用A出现循环引用问题无法被回收。
-verbose:gc P31 可达性分析法
可达性分析将对象分为2类垃圾回收的根对象GC Root和普通对象。对象与对象之间存在引用关系。
下图中A到B再到C和D形成了一个引用链可达性分析算法指如果从某个到GC Root对象是可达的对象就不可被回收。 能被称为GC Root对象的是下面4类对象
1.线程Thread对象创建线程之后整个线程对象引用线程栈帧中的方法参数、局部变量等。 2.系统类加载器加载的java.lang.Class对象。
Launcher包含应用程序类加载器和GC Root对象。 3.监视器对象用来保存同步锁synchronized关键字持有的对象。
4.本地方法调用时使用的全局对象。 通过arthas和eclipse Memory Analyzer工具可以查看GC RootMAT工具是eclipse推出的Java堆内存检测工具。
1.使用arthas的heapdump命令可将内存快照保存到本地磁盘中。
2.使用MAT工具打开堆内存快照文件。
3.选择GC Roots功能查看所有的GC Root。
P32 软引用
可达性算法中描述的对象引用一般指的是强引用即GCRoot对象对普通对象有引用关系只要这层关系存在普通对象就不会被回收。除了强引用外Java还设计了其它引用方式 1.软引用
2.弱引用
3.虚引用
4.终结器引用
软引用知识点如下
软引用相对于强引用是一种比较弱的引用关系如果一个对象只有软引用关联到它当程序内存不足时就会将软引用中的数据进行回收。
在JDK1.2版之后提供了SoftReference类来实现软引用软引用用于缓存中。
软引用的执行过程如下
1.将对象使用软引用包装起来new SoftReference对象类型(对象)。
2.内存不足时虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题回收软引用中的对象。
4.如果依然内存不足抛出OutOfMemory异常。
把最大堆内存设置为200M无法容纳2个100M的生成所以当第2个100M生成时内存空间不足会把软引用释放掉所以第2次输出软引用的内容是null。 结果如下 软引用中的对象如果在内存不足时回收SoftReferece对象本身也需要被回收。如何知道哪些SoftReference对象需要回收呢
SoftReference提供了一套队列机制 1.软引用创建时通过构造器传入引用队列。
2.在软引用中包含的对象被回收时该软引用对象会被放入引用队列。
3.通过代码遍历引用队列将SoftReference的强引用删除。 P33 弱虚终结器引用
弱引用整体机制和软引用基本一致区别在于弱引用包含的对象在垃圾回收时不管内存够不够都会直接被回收。
在JDK1.2版之后提供了WeakReference类来实现弱引用弱引用主要在ThreadLocal中使用。
弱引用对象本身也可以使用引用队列进行回收。
虚引用当对象被垃圾回收器回收时可以接收到对应的通知。当对象被回收之后对应的内存也应该被回收。 终结器引用 P34 垃圾回收算法的评价标准
垃圾回收要做的有2件事
1.找到内存中存活的对象。可达性分析法GC Root是否关联
2.释放不再存活对象的内存使得程序能再次利用这部分空间。 Java垃圾回收过程会通过单独的GC线程来完成但是不管使用哪一种GC算法都会有部分阶段需要停止所有的用户线程。这个过程称之为Stop The World简称STW如果STW时间过长则会影响用户的使用。 判断GC算法是否优秀可以从3个方面考虑
1.吞吐量。吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值即吞吐量执行用户代码时间/执行用户代码时间GC时间。吞吐量数值越高垃圾回收的效率就越高。
2.最大暂停时间。最大暂停指的是所有在垃圾回收过程中的STW时间最大值。最大暂停时间越短用户使用系统时受到的影响就越短。
3.堆使用的效率。不同的垃圾回收算法对堆内存的使用方式是不同的。标记清除法可以使用完整的堆内存。复制算法会将堆内存一分为二每次只使用一半内存。从堆使用效率上说标记清除法要优于复制算法。 上面三种评价标准堆使用效率、吞吐量、最大暂停时间不可兼得。
比如堆内存越大最大暂停时间要越长。想要减少最大暂停时间就会降低吞吐量。
不同的垃圾回收算法适用于不同的场景。
P35 垃圾回收算法1
标记清除算法
1.标记阶段将所有存活的对象进行标记。Java中使用可达性分析算法从GC Root开始通过引用链遍历出所有存活对象。
2.清除阶段从内存中删除没有被标记的非存活对象。 优点实现简单只需要在第一阶段给每个对象维护标志位第二阶段删除对象即可。
缺点1.碎片化问题。由于内存是连续的所有对象被删除之后内存中会出现很多细小可用的内存单元。如果我们需要的是一个比较大的空间很有可能这些内存单元的大小过小无法进行分配。
2.分配速度慢。由于内存碎片存在需要维护一个空闲链表极有可能发生每次需要遍历到链表最后才能获得合适的内存空间。
复制算法
复制算法的核心思想是1.准备2块空间From空间和To空间每次在对象分配阶段只能使用其中一块空间From空间。2.在垃圾回收GC阶段将From中存活对象复制到To空间。3.将两块空间的From和To名字互换。 复制算法的优点1.吞吐量高。复制算法只需要遍历一次存活对象复制到To空间即可。比标记-整理算法少了一次遍历的过程因此性能比较好但是不如标记-清除算法因为标记清除算法不需要进行对象的移动。
2.不会发生碎片化。复制算法在复制之后就会将对象按顺序存放入To空间所以对象以外的区域都是可用空间不存在碎片化内存空间。
缺点内存使用效率低。每次只能让一半的内存空间来为创建对象使用。 标记整理算法
标记整理算法也叫标记压缩算法是对标记清理算法中容易产生内存碎片问题的一种解决方案。
核心思想分为2个阶段
1.标记阶段。将所有存活对象进行标记。Java中使用可达性分析算法从GC Root开始通过引用链遍历出所有存活对象。
2.整理阶段。将存活对象移动到堆的一端。清理掉存活对象的内存空间。 优点1.内存使用效率高。整个堆内存都可以使用不会像复制算法只能使用半个堆内存。
2.不会发生碎片化。在整理的阶段
缺点整理阶段的效率不高。整理算法有很多种比如Lisp2整理算法需要对整个堆中的对象搜索3次整体性能不佳。可以通过Two-Finger、表格算法、ImmixGc等高效的整理算法优化此阶段的性能。
P36 垃圾回收算法 分代GC
分代垃圾回收算法是现代优秀的垃圾回收算法会将上述描述的垃圾回收算法组合进行使用其中应用最广的就是分代垃圾回收算法Generational GC。
分代垃圾回收将整个内存区域划分为年轻代和老年代。 在年轻代新生代Yong区中会被划分为Eden区伊甸园区幸存区有2块。 可以通过memory命令来查看内存伊甸园区大概2G幸存者区大概270M老年代大概5G。 -Xms用来设置堆的初始大小即Total。-Xmx设置堆的最大大小即Max。-Xmn设置新生代的大小包含伊甸园区和2块幸存者区。-XX:SurvivorRatio可以设置伊甸园区和幸存者区的比例。-XX:PrintGCDetails verbose:gc打印GC日志。 分代回收时创建出来的对象首先会被放入Eden伊甸园区。
随着对象在Eden区越来越多如果Eden区满新创建的对象已经无法放入就会触发年轻代的GC成为Minor GC或者Young GC。
Minor GC会把需要eden和From中需要回收的对象回收把没有回收的对象放入To区。 如上图当eden区满时想再往里放入对象依然会发生Minor GC此时会回收eden区和S1中的对象并把eden和from区中剩余的对象放入S0。
每次Minor GC中都会为对象记录他的年龄初始值为0每次GC完会加1。 如果Minor GC后对象的年龄达到阈值最大15默认值和垃圾回收器有关对象就会被晋升至老年代。
如果当老年代中空间不足无法放入新的对象时可能是由于年轻代被占满因此有些对象没达到年龄的阈值就会被放入老年代先尝试minor gc如果还是不足就会触发Full GCFull GC会对整个堆进行垃圾回收。
如果Full GC依然无法回收掉老年代的对象那么当对象继续放入老年代时就会抛出Out Of Memory异常。
P37 垃圾回收器1
系统中大部分对象都是创建出来之后很快就不再使用可以被回收的比如用户获取订单数据订单数据返回给用户之后就可以释放了。
老年代中会存放长期存活的对象比如Spring的大部分bean对象在程序启动之后就不会被回收了。
在虚拟机的默认设置中新生代大小要远小于老年代的大小。 1.可以通过调整年轻代和老年代的比例来适应不同类型的应用程序提高内存的利用率和性能假如用户比较多有大量用户会在同一时段访问订单数据如果新生代设置小大量数据创建会频繁发生minor GC所以我们希望这些生命周期短的数据能在。
2.新生代和老年代使用不同的垃圾回收算法新生代一般选择复制算法老年代可以选择标记-清除和标记-整理算法由程序员来选择灵活度高。
3.分代设计中允许只回收新生代minor gc如果能满足对象分配的要求就不需要对整个堆进行回收full gcSTW时间会减少。 垃圾回收器是垃圾回收算法的具体体现由于垃圾回收器分为年轻代和老年代除了G1之外的其他垃圾回收器必须成对组合进行使用。
JDK9之后推荐使用G1。 Serial 是一种单线程串行回收年轻代的垃圾回收器。
如果伊甸园区满了垃圾回收线程就会回收垃圾此时用户线程不能访问。 单CPU下吞吐量出色多CPU吞吐量不佳因为执行时只能但线程跑在一个CPU上。
SerialOld 是Serial垃圾回收器的老年代版本采用单线程串行回收。 注意是通过下面这行代码来设置垃圾回收器使用的参数。 P38 垃圾回收器2
parNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多CPU下的优化使用多线程进行垃圾回收。
-XX:UseParNewGc 年轻代使用ParNew回收器老年代使用串行回收器。 CMSConcurrent Mark Sweep垃圾回收器 老年代
CMS垃圾回收器关注的是系统的暂停时间允许用户线程和垃圾回收线程在某些步骤中同时执行减少了用户线程的等待时间。
参数XX:UseConcMarkSweepGC 浮动垃圾问题某些垃圾可能清理不掉。
CMS执行步骤
1.初始标记用极短的时间标记出GC Roots能直接关联到的对象。
2.并发标记标记所有的对象用户线程不需要暂停。
3.重新标记由于并发标记阶段有些对象发生了变化存在错标、漏标等情况需要重新标记。
4.并发清理清理死亡的对象用户线程不需要暂停。 缺点
1.CMS使用了标记-清除算法在垃圾收集结束之后会出现大量内存碎片CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停可以使用-XX:CMSFullGCsBeforeCompactionN参数调整N次Full GC之后再整理。
2.无法处理并发清理过程中产生的浮动垃圾不能做到完全的垃圾回收。
3.如果老年代内存不足无法分配对象CMS会退化成Serial Old单线程回收老年代。 P39 垃圾回收器3
Parallel Scavenge垃圾回收器是JDK8默认的年轻代垃圾回收器多线程并行回收关注的是系统的吞吐量具备自动调整堆内存大小年轻代、老年代大小年轻代内每一个组成部分包括阈值等都会自动调整的特点。 Parallel Old是为Parallel Scavenge收集器设计的老年代版本利用多线程并发收集。
参数-XX:UseParallelGC或-XX:UseParallelOldGC可以使用Parallel Scavenge Parallel Old这种组合。 Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用组合时不要设置堆内存的最大值垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。 吞吐量设置为99代表用户线程会执行99%的时间垃圾回收线程仅会执行1%的时间。
最大暂停时间为1时的内存大小要远比为10时小堆内存越小回收的范围越小回收时间更少暂停时间更短。当最大暂停时间变短会主动减小堆内存减少最大停顿时间。
P40 g1垃圾回收器
JDK9之后默认的垃圾回收器是G1Garbage First垃圾回收器。
G1对老年代的清理会选择存活度最低的区域进行回收这样可以保证回收效率最高这也是G1Garbage first名称的由来。
Parallel Scavenge关注吞吐量允许用户设置最大暂停时间但是会减少年轻代可用空间的大小。
CMS关注暂停时间但吞吐量会下降。
而G1设计目标就是将上述2种垃圾回收器的优点融合
1.支持巨大的堆空间回收具有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。 在G1出现之前的垃圾回收器内存结构一般是连续的如上图。
G1的整个堆会被划分成多个大小相等的区域称之为区Region区域不要求是连续的。分为EdenSurvivorOld区。Region的大小可以通过堆空间大小/2048计算得到也可以通过参数-XX:G1HeapRegionSize32m指定其中32m指定region大小为32MRegion size必须是2的指数幂取值范围从1M到32M。 G1垃圾回收有2种方式1.年轻代回收Young GC。2.混合回收Mixed GC。
年轻代回收Young GC回收Eden区和Survivor区中不用的对象。会导致STWG1中可以通过参数-XX:MaxGCPauseMillisn默认200设置每次垃圾回收时的最大暂停时间毫秒数G1垃圾回收器会尽可能地保证暂停时间。
1.新创建的对象会存放在Eden区。当G1判断年轻代区不足max默认60%伊甸园区、Survivor无法分配对象时需要回收时会执行Young GC。
2.标记出Eden和Survivor区域中的存活对象。
3.根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中年龄1情空这些区域。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和 Survivor区的平均耗时以作为下次回收时的参考依据。这样就能根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。 4.后续Young GC时与之前相同只不过Survivor区中国存活对象会被搬运到另一个Survivor区。
5.当某个存活对象的年龄到达阈值默认15将被放入老年代。 6.部分对象如果大小超过Region的一半会直接放入老年代这类老年代被称为Humongous区。比如堆内存是4G每个Region是2M只要一个大对象超过了1M就被放入Humongous区如果对象过大会横跨多个Region。 7.多次回收之后会出现很多Old老年代区此时总堆占有率达到阈值时-XX:InitiatingHeapOccupancyPercent默认45%会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成。 初始标记标记Gc Roots引用的对象为存活。
用户线程将第一步中标记的对象引用的对象标记为存活。
最终标记标记一些引用改变漏标的对象不管新创建、不再关联的对象。
并发复制清理将存活对象复制到别的Region不会产生内存碎片优先回收存活度低的。 如果清理过程中发现没有足够的空Region存放转移的对象会出现Full GC。单线程执行标记-整理算法此时会导致用户线程的暂停所以尽量保证应该用的内存有一定多余的空间。 -XX:UseG1GC打开G1的开关JDK9之后默认打开。
-XX:MaxGCPauseMillis毫秒值最大暂停的时间。 优点对比较大的堆如超过6G的堆回收时延迟可控因为它会去判断应该回收哪个区域而不是所有的区域。采用复制算法不会产生内存碎片。采用并发标记的SATB算法效率高。