一:JVM架构之class字节码
JVM整体架构(HotSpot JVM)

java启动命令
java程序的允许需要使用java命令来负责启动
java启动的时候需要找到入口main方法
java启动的时候可以进行一些启动参数的配置,以后我们的各种内存的优化和配置实际上就是通过java启动的时候进行参数的配置来达成我们的目的。
class字节码文件基本介绍
Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”
它是一种8位字节的二进制流文件,各个数据项按顺序紧密的从前向后排列,相邻的项之间没有间隙
class字节码文件结构图
官方网址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1

Javap指令查看class字节码文件结构
使用javap命令查看反编译查看字节码信息
反编译就是查看class字节码的文件结构


二:类加载子系统(类的构造器)
1.类加载子系统的作用
负责从文件系统或者网络中加载Class文件,Class 文件在文件开头有特定的文件标识(魔数)。
ClassLoader只负责Class文件的加载,是否可以运行,则由执行引擎决定。
Class文件加载到JVM中,被称为DNA元数据模板(类的信息),存放在方法区。 除了静态常量池(类的信息),方法区中还会存放运行时常量池信息、字符串常量池(JDK8移到堆空间)、整数常量池
(-128——127)
2.类加载过程
Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking)
在类初始化完成后就可以使用该类的信息了,当这个类不再被需要时可以从JVM中卸载(条件很苛刻)。

类加载的过程必须严格遵守加载、验证、准备、初始化、卸载这五个顺序过程
解析就不一定,有时候会在初始化之后【因为Java支持运行时绑定,也就是多态】
3.类初始化时机【类什么时候被加载】
类的初始化时机是我们考虑的重点,这一个过程一定在加载、验证、准备之后完成。
主·动使用(类会初始化)
1. 执行四条new、getstatic、putstatic、invokestatic字节码指令的时候
a. 使用new关键字
b. 读取或设置一个类的静态字段(final static除外,因为已经在编译的时候把结果放入到了常量池)
c. 调用静态方法
2. 使用java.lang.reflect包的方法对类进行反射调用的时候
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始
化。
4. java命令启动的一个含有main方法的类
5. JDK7开始提供的动态语言支持:例如:java.lang.invoke.MethodHandle实例的解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic等
被动使用(类不会初始化)
1. 通过子类引用父类的静态字段,不会导致子类初始化
2. 通过数组定义来引用类,不会触发此类的初始化
3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发
定义常量的类的初始化
4.类的初始化和类的实例化
类的实例化是指创建一个类的实例(对象)的过程。
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。
Java对象的创建过程往往包括两个阶段:类初始化阶段和类实例化阶段。
5.加载阶段(加载Class文件,通过类加载器完成)
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中(方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
6.验证【class文件是否有效】
1.主要用于确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,进一步保障虚拟机自身的安全,只有通过验证的 Class 文件才能被JVM加载。
2.文件格式验证:验证字节流是否符合Class文件格式的规范(例如,是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型)
3.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了java.lang.Object之外);
4.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
5.符号引用验证:确保解析动作能正确执行。
验证阶段不是必须的,可以通过启动时使用-noverify参数忽略验证
7.准备【static成员分配内存以及赋值】
1.类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段【实例变量将会在对象实例化时随着对象一起分配在堆中】
2.这里要注意零值和初始值的概念static的将会赋予零值final static的将会直接赋予初始值
这些变量所使用的内存都将在方法区中进行分配
8.解析【符号应用转变为直接引用】
解析【符号应用转变为直接引用】
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
解析阶段可以在初始化之后执行(因为Java支持动态绑定)
9.初始化阶段【clinit方法初始化类,也叫类构造器】
执行类中定义的java程序代码(字节码)。
初始化阶段是执行类构造器()方法的过程
1.静态变量赋值
2.静态代码块合并执行
方法具体细节:
1.JVM 规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行。
2.类中没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法。
3.静态代码块只能访问到出现在静态代码块之前的静态变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
4.接口也需要通过方法为接口中定义的静态成员变量显示初始化。
5.接口中不能使用静态代码块,执行接口的方法不需要先执行父接口的方法,只有当父接口中的静态成员变量被使用到时才会执行父接口的方法。
6.同一个类加载器下,一个类型只会初始化一次。
7.多线程环境下只会有一个线程去执行该类的方法
10.类加载器
启动类加载器
1.这个类加载使用C/C++语言实现的,嵌套在JVM内部。
2.它用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
3.并不继承自ava.lang.ClassLoader,没有父加载器。
4.加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
5.出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
-
案例:
- 查看启动类加载器所加载的核心类库
//查看启动类加载器所加载的核心类库
//启动类加载器无法获取Java实例(因为启动类加载器是c或者c++实现的)
URLClassPath bootstrapClassPath = Launcher.getBootstrapClassPath();
URL[] urLs = bootstrapClassPath.getURLs();
for (URL temp:urLs) {
System.out.println(temp);
}
查看启动类加载器的路径
//查看启动类加载器的路径
String property = System.getProperty("sun.boot.class.path");
System.out.println(property);
扩展类加载器
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类。
父类加载器为启动类加载器。
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器
java语言编写,由sun.misc.LaunchersAppClassLoader实现。
派生于ClassLoader类。
父类加载器为扩展类加载器。
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
通过classLoader#getSystemclassLoader() 方法可以获取到该类加载器。
11.双亲委派机制
工作原理
Java虚拟机按需加载class文件(需要使用该类时才会将它的class文件加载到内存生成class对象)
Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,没有再找孩子
双亲委派机制是一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己
也无法加载该类(通常原因是该类的 Class 文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出 ClassNotFoud 异常。

//加载Stu类
//使用反射
try {
Class<?> stuClass = Class.forName("com.bjpowernode.beans.Stu");
//获取类加载器的对象
System.out.println(stuClass.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
12.类加载器过程
1.loadClass:检查类是否加载,如果为加载则双亲委派,如果未成功则调用findClass
2.findClass:根据全包名发现class,并调用defineClass
3.defineClass:把byte[]类型的字节码转化为java.lang.Class对象(二进制字节流的内容需要符合Class文件规范)
4.defineClass方法最终会调用preDefineClass方法,来限制包名为java.*
loadclass方法
1.首先检查这个类是否被加载了
2.如果没有被加载 递归看有没有父类加载器,有的话调用父的加载方法
3.如果父类加载器为空,则调用虚拟机的加载器Bootstrap ClassLoader来加载类
4.递归一层一层出来执行各个层次的findclass方法 找到了就返回,没有就抛出异常
首先通过一个代码来使类加载
systemClassLoader.loadClass("xxx");
loadclass会
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个类是否被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果没有被加载 看有没有父类加载器,有的话调用父的加载方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 没有的话就自己找 双亲委派机制
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果这个启动类加载器没有找到就按顺序调用findclass找
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass方法
/该类保护,提供给开发人员重写
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass源码会调用preDefineClass方法
/defineClass方法最终会调用preDefineClass方法,来限制包名为java.*
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
结论
- 如果不想打破双亲委派模型,那么只需要重写findClass方法即可
- 如果想打破双亲委派模型,那么就重写整个loadClass方法
- java.包是不可以创建的,如果非要以java.命名包名也可以,但是需要重写一些JVMC++源码,然后编辑就行了
如何判断两个Class对象是否相等
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
类的完整类名必须一致,包括包名。
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
两个类对象(Class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象就是不相等的。
13.自定义类加载器
//继承ClassLoader
public class MyClassLoad extends ClassLoader {
String path;
public MyClassLoad(String path) {
this.path = path;
}
//name参数就是全包名类名
//重写findClass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//把class二进制文件流变成java.lang.Class对象
File file = new File(path);
Class<?> aClass = null;
try {
byte[] classBytes = getClassBytes(file);
//调用defineClass,加载类
aClass = defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return aClass;
}
//把文件变成byte[]数组
private byte[] getClassBytes(File file) throws IOException {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
int i = 0;
while ((i = fc.read(by)) != -1) {
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
破坏掉双亲委派机制(重写loadClass)
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//替换掉源码的向上委派
// if (parent != null) {
// c = parent.loadClass(name, false);
// } else {
// c = findBootstrapClassOrNull(name);
// }
if (name.startsWith("com.bjpowernode")) {
c = findClass(name);
} else {
c = super.loadClass(name, false);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
三:JAVA内存布局(java运行时数据区)

线程私有区域
程序计数器
虚拟机堆栈
本地方法栈
线程共享区域
堆和方法区
1.线程私有区域
线程私有区域
1.程序计数器
指向当前线程正在执行的字节码指令(方法位置)的地址
程序计数器是较小的内存
为什么要有程序计数器呢
java是多线程的,线程可以相互切换(CPU时间片切换),为了确保程序的正常运行,线程切换回来的时候知道上次执行到哪里了

2.虚拟机堆栈
以线程为单位的内存,每开辟一个线程,那么默认线程内存空间为1M,可以使用-Xss 参数调节
栈帧
1.存储的是方法的调用过程
2.每个方法调用的时候,则创建该方法的栈帧,放入虚拟机堆栈的对应的线程栈中,该方法执行完毕,则销毁该方法的栈帧
3.栈帧创建和消亡的一个过程(栈帧入栈创建,栈帧出栈消亡)
栈帧内部有
局部变量表
操作数栈(方法字节码指令执行的过程) 和局部变量表一起完成方法的执行
动态连接(支持多态的)
返回地址(方法的返回地址记录)
可以存储基本数据类型的数值


当虚拟机堆栈中的一个线程栈被栈满是会抛出一个异常
StackOverFlowError
本地方法栈: 与VM栈发挥的作用非常相似,VM栈执行java方法(字节码)服务,Native方法栈执行的是Native方法服务。Native方法是操作系统提供的或者C/C++编写的函数接口。
2.线程共享区
1.方法区
一个进程拥有一个方法区
一个进程拥有一个堆
一个进程中最大的一块内存为堆内存
堆内存区域也是GC主要考虑的地方

方法区【存放静态信息】
方法区中存放的都是在整个程序中永远唯一的元素
存储内容
JVM加载的类信息【class字节码】https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1 这些信息可以使用个代表这个类的java.lang.Class对象,作为方法区该类的各种数据的访问入口【反射】

类的信息
类型的全名
类型的父类型全名
该类型是一个类还是接口
类型的修饰符
所有父接口全名的列表
类型的字段信息
类型的方法信息
所有静态类变量(非常量)信息
一个指向类加载器的引用
一个指向Class类的引用
基本类型的常量池
方法列表(Method Tables)
每一个被加载的非抽象类,这个列表中保存了这个类可能调用的所有实例方法的引用以及父类中可调用的方法【提高效率】
运行时常量池【类加载后存放到方法区】
编译期产生【class字节码中的常量池】
其实方法区的运行时常量池里放的都是class字节码常量池的数据,只是把字面量的一部分分离出来了给到了字符串常量池

运行期间产生【运行期产生】
方法名、字段引用(代理生成的各种class字节码)
运行时产生的字符串常量【字符串的intern()方法】
静态变量【static】
方法区实现方式的演变
方法区实现方式经历的两个阶段
永久代
元空间



**JDK8中字符串常量池迁移到堆区中
JDK8中静态变量和Class对象一起在堆区中**
方法区更改原因
字符串常量池存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
方法区内存大小设置
内存回收
默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)
内存满的时候会触发异常
内存移除情况

方法区回收包含两个部分
废弃的常量
已经没有任何对象引用常量池中的常量
虚拟机中也没有其他地方引用这个字面量
不再使用的类型【大量使用反射、动态代理、CGLib的框架需要具备回收方法区的能力】【需要使用JVM参数配置,允许回收】
该类所有的实例都已经被回收
加载该类的类加载器已经被回收
该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
内存大小设置
JDK8以前:
-XX:PermSize
初始空间大小
-XX:MaxPermSize
最大空间
JDK8:
-XX:MetaspaceSize
初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
-XX:MaxMetaspaceSize
最大空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
方法区内存的溢出测试
JDK1.6的字符串常量池的内存溢出
package com.bjpowrenode.app;
import java.util.ArrayList;
import java.util.List;
//-XX:PermSize=6M -XX:MaxPermSize=6M
//jdk1.6的方法区内存溢出【利用不断增加字符串常量池手法(字符串常量池在方法区)】
public class MyApp {
public static void main(String[] args) {
int js = 0;
//创建一个集合,准备应用字符串常量池里面动态生成的常量
List<String> strings=new ArrayList<String>();
try {
while (true) {
//动态生成字符串常量,然后添加到集合,避免被GC回收
strings.add(String.valueOf(js++).intern());
}
}
//这里的捕获必须使用虚拟机错误类型捕获,而不能使用Exception
catch (OutOfMemoryError e) {
//触发
e.printStackTrace();
System.out.println("捕获...........");
}
System.out.println("dddddddddddddd");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjpowernode</groupId>
<artifactId>demo3</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
<build>
<plugins>
<!--JDK插件-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
JDK1.7的字符串常量池的内存溢出
package com.bjpowrenode.app;
import java.util.ArrayList;
import java.util.List;
//-XX:PermSize=6M -XX:MaxPermSize=6M -Xms6M -Xmx6M
//jdk1.7的方法区内存溢出
//字符串常量池在堆中
public class MyApp {
public static void main(String[] args) {
int js = 0;
//创建一个集合,准备应用字符串常量池里面动态生成的常量
List<String> strings = new ArrayList<String>();
try {
while (true) {
//动态生成字符串常量,然后添加到集合,避免被GC回收
strings.add(String.valueOf(js++).intern());
}
}
//这里的捕获必须使用虚拟机错误类型捕获,而不能使用Exception
catch (OutOfMemoryError e) {
//触发
e.printStackTrace();
System.out.println("捕获...........");
}
System.out.println("dddddddddddddd");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjpowernode</groupId>
<artifactId>demo3</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<build>
<plugins>
<!--JDK插件-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
JDK1.7的代理类内存溢出
package com.bjpowrenode.app;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
//-XX:PermSize=6M -XX:MaxPermSize=6M
//jdk1.7的方法区内存溢出【使用cglib不断的创建代理类】
//【类信息,永远在方法区】
public class MyApp2 {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Stu.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
//生成代理对象【一定是要生成代理类,class字节码,不断的叠加】
enhancer.create();
}
}
public static class Stu{
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjpowernode</groupId>
<artifactId>demo3</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--JDK插件-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
JDK1.8的代理类内存溢出
package com.bjpowrenode.app;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
//-XX:MaxMetaspaceSize=6M
//jdk1.8的方法区内存溢出【使用cglib不断的创建代理类】
//【类信息,永远在方法区】
public class MyApp3 {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Stu.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
//生成代理对象【一定是要生成代理类,class字节码,不断的叠加】
enhancer.create();
}
}
public static class Stu{
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjpowernode</groupId>
<artifactId>demo3</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--JDK插件-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.堆区
一个进程拥有一个堆
JVM启动得时候堆区创建
堆区的一些优化
堆区中可以划分一个TLAB(线程私有缓冲区)
逃逸分析、标量替换可以使得原本在堆上分配的对象直接在栈上分配
堆内存区域也是GC主要考虑的地方

存储内容
对象【一切new关键字创建的对象】
数组【所有数组】
堆空间细分

年轻代(youngGen)
伊甸园(Eden)
幸存区A(Survivor)(from)
幸存区B(Survivor)(to)
老年代(OldGen)
为什么年轻代空间比老年代要小
大量的对象都是朝生夕死,GC去处理年轻代效率高一些(因为空间小)
堆内存监控工具
jdk1.8及以下会自带



堆内存大小调整
注意:JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了AdaptiveSizePolicy,会根据GC的情况自动计算计算 Eden、From 和 To 区的大小
开启:-XX:+UseAdaptiveSizePolicy
关闭:-XX:-UseAdaptiveSizePolicy
堆内存默认大小
初始值:本机内存的1/64
最大值:本机内存的1/4
堆内存整体大小调整
-Xms:调整JVM堆内存的初始值
-Xmx:调整JVM堆内存的最大值
堆内存中年轻代与老年代比例调整
-XX:NewRatio:调整老年代是年轻代的倍数
年轻代内存整体大小调整
-Xmn
设置该参数,则忽略-XX:NewRatio
使用默认值即可
年轻代中伊甸园和幸存者比例调整
-XX:SurvivorRatio:调整伊甸园是幸存者的倍数
-XX:+PrintGCDetails 开启打应GC的日志
在cmd中用JSP来查看java程序的一个进程号
用 jinfo -flag 参数名 进程号 来查看为这个进程分配的参数的具体值
GC垃圾回收初探
Eden区、Old区、方法区空间(一般不考虑)容量不够则会触发GC,清除垃圾对象
GC触发会开启一个GC线程开始回收垃圾对象,用户线程则停止运行即为STW
MinorGC(YGC)
回收Eden和From的垃圾
MajorGC(OGC)
MajorGC大致有两种情况
先回收Eden和From的垃圾,不够了再回收Old的垃圾
直接回收Old的垃圾
FullGC(FGC)
GC区域是Eden、From、Old、Method(方法区)
对象分配
1.先判断对象是否可以栈上分配、TLAB(线程私有缓冲区)、是否大对象(直接进老年代)
2.进入Eden区,如果Eden区的内存满了,则会触发一次YGC,清理Eden区和From区的一个垃圾,YGC之后剩余的对象都会进入幸存者TO区,
,对象的AGE+1,如果TO区装不下了的,则会直接进入Old区,再TO区和From区进行转化,也就是说一次GC之后Eden区和to区总是为空的。
3.加入Old区满了,会触发oldGC。
晋升老年代对象年龄阈值15
-XX:MaxTenuringThreshold
晋升老年代对象年龄阈值默认15(16了就进老年代)
-XX:TargetSurvivorRatio
Survivor区对象使用率默认50%
相同年龄对象的内存之和是Survivor的大小的50%
晋升老年代
内存堆测试
两个字符串常量相加一定会变对象
四:JVM的启动参数解析
JAVA虚拟机(JVM)通过操作系统命令JAVA_HOME\bin\java –option 来启动,-option则为虚拟机参数
通过这些启动参数可对虚拟机的运行状态进行调整,掌握参数的含义可对虚拟机的运行模式有更深入的理解
1.参数分类
标准参数(-)
所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
# 查看标准参数
java
非标准参数(-X)
默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
# 查看非标准参数
java -X
非稳定参数(-XX)
此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
来查看所有的初始参数
java -XX:+PrintFlasInitial
2.书写格式
非稳定参数(-XX)
boolean类型
-XX:(+/-)参数名称
例如:-XX:+PrintGCDetails
非boolean类型
-XX:参数名称=数值
例如:-XX:SurvivorRatio=8
非标准参数(-X)
数值类型
-Xms100M
3.使用方法
java xx -XX:xxxx
在idea中:

4.查看非稳定参数-XX默认值
java -XX:+PrintFlagsInitial 该参数运行不会让自己的程序运行

product – 官方支持, JVM内部选项
rw – 可动态写入的.
C1 – Client JIT 编译器
C2 – Server JIT 编译器
pd – platform Dependent 平台独立
lp64 – 仅 64 位JVM
manageable – 代表可以运行时修改
diagnostic – 用于虚拟机debug的
experimental – 非官方支持的
查看非稳定参数-XX修改值
”=”表示参数的默认值,
”:=” 表明了参数被用户或者JVM赋值了
查看参数的一个最终值
-XX:+PrintFlagsFinal
5.查看非稳定参数-XX运行时值
# 查看进程
jps
# 查看已设置JVM参数的flags
jinfo -flags 进程号
# 查看具体JVM参数的值
jinfo -flag xxx 进程号

6.修改非稳定参数-XX运行时值
# 查看进程
jps
# 查看已设置JVM参数的flags
jinfo -flags 进程号
# 修改非boolean类型的具体JVM参数的值
jinfo -flag xxx=值 进程号
# 修改boolean类型的具体JVM参数的值
jinfo -flag (+/-)xxx 进程号
7.常用标准参数
-server
选择Java HotSpot Server VM。64bit版本隐含设置-server。
8.常用非标准参数
-Xloggc:filename
将GC(garbage collection)信息重定向到filename
-Xms
等价于-XX:InitialHeapSize:堆内存起始值(默认0)
-Xmx
等价于-XX:MaxHeapSize:堆内存最大值(默认0)
-Xmn
等价于-XX:NewSize,-XX:MaxNewSize:堆中年轻代初始及最大大小(默认0)
-Xss
等价于-XX:ThreadStackSize:线程栈大小(默认0)
9.常用非稳定参数
打印参数
-XX:+PrintFlagsInitial
查看所有的参数的默认初始值(默认false)
-XX:+PrintFlagsFinal
查看所有的参数的最终值(默认false)
-XX:+PrintCommandLineFlags
查看被用户或者JVM设置过的详细的XX参数的名称和值(默认false)
-XX:+PrintGCDetails
输出详细的GC处理日志(默认false)
-XX:+PrintTLAB
打印TLAB信息(默认false)
-XX:+PrintGCTimeStamps
打印出来每次GC发生的时间(默认false)
-XX:+PrintCompilation
打印JIT编译日志
-XX:+UnlockDiagnosticVMOptions
打印inlining信息
-XX:+PrintInlining
打印inlining信息
年轻代老年代分配参数
-XX:+UseAdaptiveSizePolicy:根据GC的情况自动计算计算 Eden、From 和 To 区的大小(默认true)
-XX:NewRatio:调整老年代是年轻代的倍数(默认2)
-XX:SurvivorRatio:调整伊甸园是幸存者的倍数(默认8)
-XX:MaxTenuringThreshold:晋升老年代对象年龄阈值(默认15)
-XX:TargetSurvivorRatio:Survivor区对象使用率(默认50%)(相同年龄对象的内存之和是Survivor的大小的50%,则晋升老年代)
-XX:HandlepromotionFailure:是否设置空间分配担保(JDK7永远为true)
-XX:PretenureSizeThreshold:指定了大对象阈值(默认0)
逃逸分析参数
-XX:+DoEscapeAnalysis:开启逃逸分析(默认开启)
-XX:+EliminateAllocations:开启标量替换(默认开启)
-XX:+EliminateLocks:开启同步消除
TLAB参数
-XX:+UseTLAB:启用或关闭TLAB(默认true)
-XX:TLABSize:设置TLAB大小(默认0)
-XX:MinTLABSize:TLAB最小值(默认2048)
-XX:TLABWasteTargetPercent:TLAB占用Eden区的百分比(默认1)
-XX:TLABRefillWasteFraction:能进入TLAB的单个对象大小,默认为64,如果对象大小大于等于TLAB空间的1/64,即直接在堆区分配,如果对象大小小于TLAB的1/64,则在TLAB上分配(默认64)
-XX:+ResizeTLAB:是否自动调整TLABRefillWasteFraction阈值(默认true)
-XX:+FastTLABRefill:是否启用快速填充TLAB(默认true)
-XX:TLABAllocationWeight:控制平均值平均多快忘掉旧值(默认35)
-XX:+TLABStats:TLAB状态(默认true)
-XX:TLABWasteIncrement:动态的增加浪费空间的字节数(默认4)
-XX:-ZeroTLAB:是否清零TLAB区域(默认false)
五:栈上分配(把对象拆成局部变量)

1.什么是栈上分配
栈上分配是java虚拟机提供的一种优化技术,基本思想是对于那些线程私有的对象(指的是不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上.
2.栈上分配的好处
对象可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
3.如何确定栈上分配
采用逃逸分析确定栈上分配
4.逃逸分析
如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
JVM需要Server端才可以(默认为Server端)
5.全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程)
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
//静态变量(逃逸)
static Stu stu1;
public void fun1() {
//没有逃逸
Stu stu=new Stu();
//逃逸 对象本身就是逃逸对象
stu1=new Stu();
}
//方法返回逃逸
public Stu fun2() {
//逃逸
Stu stu=new Stu();
return stu;
}
6.参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
//方法参数(逃逸)
public void fun3(Stu stu) {
//逃逸
stu=new Stu();
}
7.标量替换(栈上分配的具体实现)
若一个数据已经无法再分解成更小数据来表示,JVM中基础数据类型都不能再进一步分解,这些数据可被称为标量。
如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java 中的对象就是典型的聚合量
标量替换就是把可以栈上分配的对象每个属性打撒成标量存储在栈空间
栈空间的一个栈帧,中的一个局部变量表来存储这些打散的标量(就是对象的中的属性),随着栈帧的出栈,这些标量就直接消失了,不用GC来处理

8.同步锁消除
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
public void fun5() {
//不逃逸
Stu stu = new Stu("hzj", 1000, 18, 95.5f);
//锁会消除之前
synchronized (stu) {
System.out.println("ok");
}
}
public void fun5() {
//不逃逸
Stu stu = new Stu("hzj", 1000, 18, 95.5f);
//锁会消除之后
System.out.println("ok");
}
9.逃逸分析参数
-XX:+DoEscapeAnalysis:开启逃逸分析(默认开启)
-XX:+EliminateAllocations:开启标量替换(默认开启)
-XX:+EliminateLocks:开启同步锁消除(默认开启)
六:GC概述
现在一般来说OldGC就是全堆GC
1.GC是什么
GC(Garbage Collection)JAVA中的垃圾回收器。
在C/C++程序中,程序员在内存中主动开辟一段相应的空间来存值。有了GC,程序员就不需要再手动的去控制内存的释放。
当Java虚拟机(VM)发觉内存资源紧张的时候,就会自动地去清理无用对象所占用的内存空间。
如果需要,可以在程序中显式地使用System.gc()来强制进行一次立即的内存清理。
Java提供的GC功能可以自动监测对象是否超过了作用域,从而达到自动回收内存的目的
而不同的垃圾回收方式则有不同的性能表现,因此出现了垃圾回收算法。利用这些垃圾回收算法的特性,又实现了多种垃圾回收器。
2.内存泄漏、内存溢出
内存泄漏:数据不再使用,但没有清除
内存溢出:内存满了,无法存储数据
3.垃圾的判定
当一个对象不被使用时,则认为是需要回收的垃圾对象
引用计数法(不用)
为对象添加一个计数器,记录当前对象被引用的次数,当引用次数为0时则认为是垃圾对象。
缺点:当a和b两个对象相互引用时,引用数永远不为0,而a和b这两个对象同时不被其他对象所引用(是垃圾对象),无法正确判断垃圾对象。因为循环引用的存在,所以Java虚拟机不采用此方法。
可达性分析法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收。

4.GC算法
标记 -清除算法
复制算法
标记-压缩算法
分代收集算法:
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代
⽼年代⼀般使⽤“标记-清除”、“标记-整理”算法
年轻代⼀般⽤复制算法
5.STW
STW(Stop The World)指垃圾收集过程中,需要停止其他所有工作线程,只有垃圾回收线程工作
这段时间内系统无法提供服务,是垃圾回收过程必须要面对的问题
6.GC类型
按照线程数量分,可分为串行垃圾回收器和并行垃圾回收器
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了SWT机制。

按照工作模式分,可分为并发式垃圾回收器和独占式垃圾回收器
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间
独占式垃圾回收器(STW)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束

按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理(就是把存活的对象依次按顺序排列在内存中----移动地址),消除回收后的碎片(清除最后一个活对象以后的所有地址空间)
非压缩式的垃圾回收器不进行这步操作
按工作的内存区间分,可分为年轻代垃圾回收器和老年代垃圾回收器
年轻代,对象朝生夕死,容量比老年代小,一般采用复制算法
老年代,对象不是那么容易死,一般采用标记整理、标记压缩算法
7.评估GC的性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。

8.GC产品
1999年随JDK1.3.1一起来的是串行方式的serialGc,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布
Parallel GC在JDK6之后成为HotSpot默认GC。
2012年,在JDK1.7u4版本中,G1可用。
2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)
2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macos和Windows上的应用
7种经典的垃圾收集器
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1(Garbage-First)
7款经典收集器与垃圾分代之间的关系

年轻代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
垃圾收集器的组合关系

两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除
(绿色虚线)JDK14中:弃用Parallel Scavenge和Serialold GC组合(JEP366)
(绿色虚框)JDK14中:删除CMS垃圾回收器(JEP363)
七:垃圾回收算法
1.标记-清除法
分为两个步骤:
标记:标识出活动对象,初始时会根据 roots【根对象】进行标记,之后递归标记能够被访问到的活动对象,即遍历对象并标记。
清除:回收非活跃对象,在第一步的标记阶段完成后,非活跃对象即应用程序无法再次访问,需要对其进行回收。
缺点:
标记、清除过程需要遍历两次内存,效率不高。在回收之后会产生大量不连续的内存碎片,导致分配大对象时没有足够的连续空间。

2.复制法简单高效。无碎片
From和To区域 相对概念 To永远在YGC前是前是空
将内存空间分为两块(From和To),每次只使用其中一块(From)
在垃圾回收时,将正在使用的内存(From)中的存活对象按顺序复制到未使用的内存中(To)
清除正在使用的内存(From)中的所有对象
交换两个内存的角色(From和To互换),完成垃圾回收。
这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。
缺点:需要两倍的内存空间

3.标记-整理法
分为三个步骤:
标记:标识出活动对象,初始时会根据 roots【根对象】进行标记,之后递归标记能够被访问到的活动对象,即遍历对象并标记。
压缩:移动所有存活的对象(地址移动/覆盖),且按照内存地址次序依次排列
清除:将末端内存地址以后的内存全部回收
缺点:需要移动大量内存空间,效率较低。

分代收集算法
JVM根据不同代的特点采取最适合的收集算法

新生代的特点是每次垃圾回收时都有大量的对象需要被回收–复制算法
1.所有新生成的对象首先都是放在年轻代的Eden
2.年轻代内存按照8:1:1的比例分为一个eden和S0、S1
3.YGC回收时先将Eden存活对象复制到一个S0(TO区),然后清空Eden
4.再把S0(To区)和S1(From)转化,把To区变为空
5.如果Eden区再满之后,触发YGC再把Eden区和From区的存活对象,直接复制到To区,To区放的下就放入后,清空Eden和From区,TO区与From区转化,如果放不下,其他的就直接放入Old区,再清空Eden和From再转
6.如果这时候老年代也满了,那就只能触发一个FullGC全堆清理
老年代的特点是每次垃圾收集时只有少量对象需要被回收–标记整理清除
在年轻代中经历了N次(默认15)垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
内存比年轻代也大很多(大概比例是1:2),当老年代内存满时触发即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率比较高。
八:可达性分析
1.可达性分析(什么是垃圾)
过可达性分析(Reachability Analysis)算法来判定对象是否存活的
这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的,即为垃圾可以被回收

2.GC Roots(只是变量的名称而不是对象)
在Java技术体系里面,固定可作为GC Roots的包括以下几种
1.在虚拟机栈(栈帧中的本地变量表)中引用的对象 √
参数
局部变量
临时变量
2.在方法区中类静态属性引用的对象(static) √
3.在方法区中常量引用的对象(static final) √
字符串常量池
4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象
5.Java虚拟机内部的引用
基本数据类型对应的Class对象
常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等
系统类加载器
6.所有被同步锁(synchronized关键字)持有的对象
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
4.GC Roots案例
package com.bjpowernode.app;
public class Stu {
//对象成员(拥有5M内存)
Byte[] bytes=new Byte[1024*1024*5];
}
package com.bjpowernode.app;
public class MyApp {
//类静态成员(stu->GCROOTS)
public static Stu stu;
public static void main(String[] args) throws InterruptedException {
//局部变量(myApp->GCROOTS)
MyApp myApp=new MyApp();
myApp.fun1();
Thread.sleep(20000);
System.out.println("ok");
//去掉引用链
MyApp.stu=null;
Thread.currentThread().join();
}
public void fun1(){
MyApp.stu=new Stu();
}
}

九:Java四种引用类型—结合可达性分析的GCRoots

提供四种引用的目的在于
可以让程序员通过代码的方式决定某些对象的生命周期
有利于JVM进行垃圾回收
1.强引用
创建一个对象并把这个对象赋给一个引用变量(我们用的最多的等号+new关键字)
如果内存不足,JVM会抛出OOM错误也不会回收对象
想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null
package com.bjpowernode.app;
public class MyApp {
public static void main(String[] args) throws InterruptedException {
//强引用
Stu stu = new Stu("app123");
// 中断强引用
stu=null;
Thread.currentThread().join();
}
}
2.软引用 SoftReference
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它
如果内存空间不足了,就会回收这些对象的内存。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等
使用软引用能防止内存泄露,增强程序的健壮性
package com.bjpowernode.app;
import java.lang.ref.SoftReference;
public class MyApp2 {
public static void main(String[] args) throws InterruptedException {
//强引用
Stu stu = new Stu("aaaa");
//软引用
//new Stu有两个引用【Stu stu】【SoftReference<Stu> stuSoftReference】
SoftReference<Stu> stuSoftReference = new SoftReference<>(stu);
//SoftReference<Stu> stuSoftReference = new SoftReference<>(new Stu("bbbb"));
//强引用取消引用链
stu = null;
//软引用提供了get方法来获取引用的对象【GC前】
System.out.println(stuSoftReference.get());
System.gc();
//软引用提供了get方法来获取引用的对象【GC后】
System.out.println(stuSoftReference.get());
try {
//给出一个80M的对象
byte[] bytes = new byte[1024 * 1024 * 80];
} catch (Throwable throwable) {
throwable.getMessage();
System.out.println("OOM了");
} finally {
//当内存空间不足,垃圾回收器就回收(在内存不足之前也就是OOM之前,会清空软引用)
System.out.println(stuSoftReference.get());
}
Thread.currentThread().join();
}
}
3.弱应用 WeakReference
弱引用也是用来描述非必需对象
当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
package com.bjpowernode.app;
import java.lang.ref.WeakReference;
public class MyApp3 {
public static void main(String[] args) throws InterruptedException {
WeakReference<Stu> stuWeakReference=new WeakReference<>(new Stu("aaaaa"));
//使用get方法获取引用实例
System.out.println(stuWeakReference.get());
System.gc();
Thread.sleep(50); //GC线程需要时间
//弱引用一旦GC所有的引用都理解被垃圾回收
System.out.println(stuWeakReference.get());
}
}
3.虚引用 PhantomReference
监测的是对象:而不是那个变量
要放监测对象和引用队列
不能使用get获取引用对象
检测引用的对象什么时候被垃圾回收 是垃圾就放入引用队列
package com.bjpowernode.app;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class MyApp4 {
public static void main(String[] args) throws InterruptedException {
//强引用
Stu stu = new Stu("ssss");
Stu stu2 = stu;
//虚引用主要用于检测对象是否被回收了
//创建一个引用队列【一旦引用的对象回收了,则进入队列】
ReferenceQueue<Stu> stuReferenceQueue = new ReferenceQueue<>();
//创建一个虚引用【虚引用必须要使用引用队列】
PhantomReference<Stu> stuPhantomReference = new PhantomReference<>(stu, stuReferenceQueue);
//虚引用的get方法永远是null
System.out.println(stuPhantomReference.get());
//isEnqueued()返回代表引用的对象有没有被GC标记为垃圾需要回收,返回为true则代表是需要被回收
System.out.println(stuPhantomReference.isEnqueued());
//stu强引用链打断
stu = null;
//stu2强引用链打断
stu2 = null;
System.gc();
Thread.sleep(50);
System.out.println(stuPhantomReference.isEnqueued());
}
}
自己一波
//强引用 xx为GCRoots
Stu xx = new Stu("xx", 1, 1, 1.0f);
//强引用 x2为GCRoots
Stu x2=xx;
//弱引用监测 xx 对象 ReferenceQueue 是引用队列 是垃圾就放入
PhantomReference<Stu> reference = new PhantomReference<Stu>(xx,new ReferenceQueue<>());
//断开强引用
xx=null;
//GC一次
System.gc(); //那xx就会变为垃圾
Thread.sleep(1000);
/*这里因为x2的强引用没有断开 可达性分析从x2可以到实际的Stu对象 所以
* PhantomReference监测的xx的具体对象不是垃圾 所以为false*/
System.out.println(reference.isEnqueued()); //false
}
十:SerialGC 串行单核GC
1.Serial的简介
在YGC使用mark-copy(复制算法,两倍空间)
在OGC使用mark-sweep-compact(标记-压缩,移动内存)
都是单线程收集器
都会触发stop-the-world
1.Serial存在的问题
在GC时,GC线程会阻塞所有用户线程,等他执行完,才会恢复用户线程
每次GC是都会造成不同程度的卡顿,对用户是极为不友好
2.使用场景(现在基本不用)
几百兆内存和单个cpu的环境下使用
3.开启Serial GC
-XX:+UseSerialGC
2.实验场景

JVM参数配置
垃圾处理器开启
-XX:+UseSerialGC
JDK1.8默认的不是串行处理器,所以要开启
GC日志开启
-XX:+PrintGCDetails
打印GC详细信息
-XX:+PrintGCTimeStamps
打印GC发生的时间
堆内存试验大小(由于S区域会分成2个,且有一个是交换区,因此总内存为200M,S区为10M,那么可以使用的内存只有190M)
-Xmx200M
最大堆
-Xms200M
最小堆
年轻代和老年代分配比例
-XX:NewRatio=3
NewRatio表示老年代和年轻代的比例,3表示3:1
即把整个堆内存分为4份,老年代占3份,年轻代1份
目前堆内存为200M,NewRatio=3时,年轻代=50M,老年代=150M
年轻代分配比例
-XX:SurvivorRatio=3
SurvivorRatio表示Eden区和两个Survivor区的比例,3表示3:2(注意是两个Survivor区)
即把年轻代分为5份,Eden占3份,Survivor区占2份
目前年轻代为50M,Survivor=3时,Eden=30M,Survivor=20M(S1=10M, S2=10M)
3.代码片段

JVM参数配置

完整代码
package com.bjpowernode.app;
import java.util.Random;
/*
//JVM参数配置
-XX:+UseSerialGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xmx200M
-Xms200M
-XX:NewRatio=3
-XX:SurvivorRatio=3
*/
/*
-XX:NewRatio=3
- NewRatio表示老年代和年轻代的比例,3表示3:1
- 即把整个堆内存分为4份,老年代占3份,年轻代1份
- 目前堆内存为200M,NewRatio=3时,年轻代=50M,老年代=150M
-XX:SurvivorRatio=3
- SurvivorRatio表示Eden区和两个Survivor区的比例,3表示3:2(注意是两个Survivor区)
- 即把年轻代分为5份,Eden占3份,Survivor区占2份
- 目前年轻代为50M,Survivor=3时,Eden=30M,Survivor=20M(from=10M, to=10M)
*/
public class MyApp {
public static void main(String[] args) {
//创建二维数据作为内存试验【给出1000个空间】
byte[][] useMemory = new byte[1000][];
//创建随机数对象
Random random = new Random();
System.out.println(useMemory.length);
//开始循环添加内存
for (int i = 0; i < useMemory.length; i++) {
//创建一个10M的对象
useMemory[i] = new byte[1024 * 1024 * 10];
// 20%的概率将创建出来的对象变为可回收对象
//直接使用复制空指针null来进行回收
if (random.nextInt(100) < 20) {
System.out.println("创建出来的10M对象开始复制为null(准备回收)--- " + i);
//复制为空
useMemory[i] = null;
} else {
System.out.println("创建出来的10M对象---" + i);
}
}
}
}
4.基本日志分析

1. GC发生的时间(秒),从程序启动开始计算
2. GC类型,另外还有Full GC
3. GC原因,Allocation Failure(申请内存失败)
4. YGC:DefNew说明新生代用Serail GC回收,即default new generation
5. GC前该区域内存已使用容量(S区分配了10M,因此YGC总容量是40M)
6. GC后该区域内存已使用容量
7. 该区域内存总容量
8. 该内存区域GC所占用的时间(秒)
9. GC前堆内存已使用容量
10. GC后堆内存已使用容量
11. 堆内存总容量(S区分配了10M,因此总容量是190M)
12. 本次回收整体占用时间(秒)
13. user:用户态消耗的CPU时间
14. sys:内核态消耗的CPU时间
15. real:从操作开始到操作结束所经历的墙钟时间
十一:G1垃圾回收器
1.G1概述
1.G1垃圾处理器是什么
G1(Garbage-First)垃圾收集器是一个具有标志性意义的垃圾收集器,有别于CMS等前代的垃圾收集器,它采取了全新的垃圾收集思路,开创了面向局部收集的设计思路和基于Region的内存布局形式
前代的所有包括CMS在内的其他收集器,垃圾收集的目标范围是整个新生代(Minor GC)、整个老年代(Major GC)或者是整个Java堆(Full GC)
G1收集器可以面向堆内存任何部分来组成回收集CSet(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多并且回收收益最大,这也是G1的Garbage First名字的由来
2.G1垃圾收集器的内存区分
G1之前的垃圾收集器统一将内存分成新生代,老年代和持久代,并且分代执行垃圾回收算法。
G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。

G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间和Survivor空间,或者老年代空间。
JVM最多可以有2048个Region。
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,也可以用参数-XX:G1HeapRegionSize手动指定Region大小(但是推荐默认的计算方式)
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
默认年轻代对堆内存的占比是5%,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%。
年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1
Humongous区
Region中还有一类特殊的Humongous区域,专门用来存储大对象。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前原则一样,唯一不同的是对大对象的处理
G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如每个Region是2M,只要一个对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
2.G1垃圾收集器GC步骤

初始标记【STW】
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。(同CMS)
并发标记
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象集合的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。(同CMS)
重新标记/最终标记【STW】
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。(同CMS)
筛选回收【STW】
定义:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划。
比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,通过回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。
这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
回收算法:不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中。
这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本
筛选回收如何实现?
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
并发重置
重置本次GC过程中的标记数据。
3.G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。
G1垃圾收集器刚开始年轻代只占堆内存百分之5,会随着每次计算回收时间而增加,最多不超过百分之60。
MixedGC【混合收集】
不是FullGC,老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值则触发,回收所有的年轻代和部分老年代(根据筛选回收阶段计算优先级后排序)以及大对象区
正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。
Full GC
停止系统程序,然后采用单线程进行标记、清理、压缩整理以空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
4.G1垃圾收集器的特点
并行与并发
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW停顿时间。
部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
分代收集
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
空间整合
与CMS的标记-清理算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部上来看是基于标记-复制算法实现的。
可预测的停顿
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型【后台维护的优先列表】,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数-XX:MaxGCPauseMillis指定)内完成垃圾收集。
可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
默认的停顿目标为200MS,一般不需要调整,即使调整也应尽可能使其合理,不能太短,如果我们把停顿时间调得非常低,譬如设置为二十毫秒, 很可能每次只能回收很小的一部分内存, 导致垃圾慢慢堆积。
十二:JVM架构&GC总结
1.class字节码结构总结
JDK8字节码结构网址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

2.JVM架构总结


1730

被折叠的 条评论
为什么被折叠?



