类加载器

Jvm第九期的 图片素材:
https://www.liuchengtu.com/lct2021/#R6035e93def743092f023343f60cc6173

什么是类加载器

将我们的class文件读取到内存中
类加载器加载的过程
类加载器加载我们的class文件,并且经历过验证、准备、解析,在初始化我们该类。
Class文件读取来源
1.本地磁盘文件 java源代码编译的class文件
2.通过网络下载的class文件
3.War、Jar解压的class文件
4.从专门的数据库中读取的class文件
5.使用java cglib、动态代理生成的代理类class文件
Jvm虚拟机中 通过 类加载器(用户可以自定义类加载器)

类加载器的分类

1.启动(Bootstrap)类加载器:加载JVM自身工作需要的类,它由JVM自己实现。它会加载JAVAHOME/jre/lib下的文件底层是C语言实现2.扩展(Extension)类加载器:它是JVM的一部分,由sun.misc.LauncherExtClassLoader实现,他会加载ExtClassLoader实现,他会加载ExtClassLoader实现,他会加载JAVAHOME/jre/lib/ext目录中的文件(或由System.getProperty(“java.ext.dirs”)所指定的文件)。底层是Java实现3.(应用)AppClassLoader类加载器:应用类加载器,我们工作中接触最多的也是这个类加载器,它由sun.misc.LauncherJAVA_HOME/jre/lib下的文件 底层是C语言实现 2.扩展(Extension)类加载器:它是JVM的一部分,由sun.misc.LauncherExtClassLoader实现,他会加载ExtClassLoader实现,他会加载ExtClassLoader实现,他会加载JAVA_HOME/jre/lib/ext目录中的文件(或由System.getProperty(“java.ext.dirs”)所指定的文件)。 底层是Java实现 3.(应用)AppClassLoader 类加载器:应用类加载器,我们工作中接触最多的也是这个类加载器,它由sun.misc.LauncherJAVAH​OME/jre/lib下的文件底层是C语言实现2.扩展(Extension)类加载器:它是JVM的一部分,由sun.misc.LauncherExtClassLoader实现,他会加载ExtClassLoader实现,他会加载ExtClassLoader实现,他会加载JAVAH​OME/jre/lib/ext目录中的文件(或由System.getProperty(“java.ext.dirs”)所指定的文件)。底层是Java实现3.(应用)AppClassLoader类加载器:应用类加载器,我们工作中接触最多的也是这个类加载器,它由sun.misc.LauncherAppClassLoader实现。他加载我们工程目录classpath下的class及jar包 底层是java实现
4.自定义类加载器: 也就是用户自己定义的类加载器

Launcher 源码解读

Launcher.ExtClassLoader var1;
try {
// 获取到我们扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError(“Could not create extension class loader”, var10);
}

try {
//获取到我们应用类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError(“Could not create application class loader”, var9);
}
// 当前程序启动的线程 默认的 ClassLoader 应用类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty(“java.security.manager”);
if (var2 != null) {
SecurityManager var3 = null;
if (!“”.equals(var2) && !“default”.equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}

if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);
}System.setSecurityManager(var3);

那些操作会初始化类加载器
类的主动使用:

1.调用类的静态方法
2.invokeStatic 调用静态方法
3.Main
4.New
5.Class.formname
6.子类初始化一定会初始化父类
初始化一个类,那么一定会触发类加载器
但是类加载器加载了该类,但是该类不一定初始化。

类加载器的双亲委派机制
首先在我们类加载器分为四种 自定义类加载器、应用类加载器、扩展类加载器、启动类加载器。
当一个类加载器收到请求之后,首先会依次向上查找到最顶层类加载器(启动类加载器),依次向下加载class文件,如果已经加载到class文件,子加载器不会加继续加载该class文件。
双亲委派机制机制的好处
目的就是为了防御开发者为定义的类与jdk定义源码类产生冲突问题,保证该类在内存中的唯一性。
ClassLoader源码解读

Launcher类源码解读

public Launcher() {Launcher.ExtClassLoader var1;try {//获取我们的扩展类加载器var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {// 获取我们的应用类加载器this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}// 默认设置我们的类加载器是为应用类加载器Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}}

双亲委派机制源码分析

1.ClassLoader.getSystemClassLoader().loadClass()

// 查询缓存中是否有缓存 该class
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
}

if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();

// 如果父加载器(扩展和启动类加载器都没有加载class,则使用当前(应用类加载器加载))
c = findClass(name);

    // this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();
}

}
if (resolve) {
resolveClass©;
}
return c;

如何自定义一个类加载器

public class MayiktClassLoader extends ClassLoader {private File fileObject;public MayiktClassLoader(File fileObject) {this.fileObject = fileObject;}public void setFileObject(File fileObject) {this.fileObject = fileObject;}public File getFileObject() {return fileObject;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] data = getClassFileBytes(this.fileObject);return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();return null;}}/*** 从文件中读取去class文件** @throws Exception*/private byte[] getClassFileBytes(File file) throws Exception {//采用NIO读取FileInputStream fis = new FileInputStream(file);FileChannel fileC = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();WritableByteChannel outC = Channels.newChannel(baos);ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (true) {int i = fileC.read(buffer);if (i == 0 || i == -1) {break;}buffer.flip();outC.write(buffer);buffer.clear();}fis.close();return baos.toByteArray();}}

代码测试:

Class<?> aClass = new MayiktClassLoader(new File("D:\\code\\meite-jvm\\com\\mayikt\\days01\\MayiktEntity.class")).loadClass("com.mayikt.days01.MayiktEntity");
Object o = aClass.newInstance();
System.out.println(o.getClass().getClassLoader());

根据类加载器手写热部署插件

自定义类加载器
ClassLoader 类加载器中 双亲委派机制 核心源码部分

findLoadedClass()— 首先,检查类是否已经加载
parent.loadClass(name, false); 读取到parent.loadClass

findBootstrapClassOrNull 使用启动类加载器读取
findClass 扩展和应用类加载器、自定义类加载器

public class MayiktClassLoader extends ClassLoader {private File fileObject;public MayiktClassLoader(File fileObject) {this.fileObject = fileObject;}public void setFileObject(File fileObject) {this.fileObject = fileObject;}public File getFileObject() {return fileObject;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] data = getClassFileBytes(this.fileObject);return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();return null;}}/*** 从文件中读取去class文件** @throws Exception*/private byte[] getClassFileBytes(File file) throws Exception {//采用NIO读取FileInputStream fis = new FileInputStream(file);FileChannel fileC = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();WritableByteChannel outC = Channels.newChannel(baos);ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (true) {int i = fileC.read(buffer);if (i == 0 || i == -1) {break;}buffer.flip();outC.write(buffer);buffer.clear();}fis.close();return baos.toByteArray();}}

热部署插件代码
热部署插件吗?我第一次使用热部署插件 底层就是基于类加载器实现。
Javaweb框架知识
Idea 支持插件 热部署

热部署插件原理 (手写)
1.class 如何判断一个class文件是否发生变化?
MD5、操作系统提供api 文件修改时间
2. 判断该class文件修改日期是否有发生变化,如果
有发生变化,则从新使用类加载器读取最新的class文件
到内存中。
3.如何监听-----class文件是否有发生变化呢? 单独线程

比如 Meite.class new Meite()

public class ClassFileEntity {/*** 类的名称*/private String name;/*** class*/private Class aClass;/*** 最后被更改的时间*/private long lastModified;public ClassFileEntity(String name, long lastModified) {this.name = name;this.lastModified = lastModified;}public ClassFileEntity(String name, long lastModified, Class aClass) {this.name = name;this.lastModified = lastModified;this.aClass = aClass;}public String getName() {return name;}public Class getaClass() {return aClass;}public long getLastModified() {return lastModified;}public void setName(String name) {this.name = name;}public void setaClass(Class aClass) {this.aClass = aClass;}public void setLastModified(long lastModified) {this.lastModified = lastModified;}
}public class HotDeploymentPlug {//存放所有的class文件private Map<String, ClassFileEntity> mapClassFiles = new HashMap<>();private String path;/*** 包的名称*/private String packageName = "com.mayikt.days01.";public HotDeploymentPlug(String path) {this.path = path;}public void start() {listener();}/*** 监听方法*/public void listener() {new Thread(() -> {while (true) {// 1.读取该文件下File files = new File(path);File[] tempList = files.listFiles();// 2.读取class文件 存入到 mapClassFilesfor (File file :tempList) {String name = file.getName();if (StringUtils.isEmpty(name)) {continue;}long l = file.lastModified();// 使用类加载器读取该 classString className = packageName + name.replace(".class", "");if (mapClassFiles.containsKey(className)) {// 则比对该class文件 是否被修改ClassFileEntity mapClassFileEntity = mapClassFiles.get(className);if (mapClassFileEntity.getLastModified() != l) {try {mapClassFileEntity.setLastModified(l);MayiktClassLoader mayiktClassLoader = new MayiktClassLoader(file);Class<?> aClass = mayiktClassLoader.loadClass(className);Object o = aClass.newInstance();log.info(className + "class文件发生了变化");} catch (Exception e) {log.error("e:{}", e);}}} else {ClassFileEntity newClassFileEntity = new ClassFileEntity(className, l);// 如果不存在 则存入到mapClassFiles集合中mapClassFiles.put(className, newClassFileEntity);}try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();}public static void main(String[] args) {HotDeploymentPlug hotDeploymentPlug = new HotDeploymentPlug("D:\\code\\meite-jvm\\com\\mayikt\\days01");hotDeploymentPlug.listener();}
}

什么是SPI机制

Java SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制.
实现方式:

1.首先需要再resources目录下:创建文件夹META-INF services
2.定义接口文件的名称:
D:\mt2020\code\mayikt_jvm\src\main\resources\META-INF\services\com.mayikt.service.MyService
名称规范:包名+类名组成。
com.mayikt.service.impl.MyServiceImpl01
com.mayikt.service.impl.MyServiceImpl02

ServiceLoader load = ServiceLoader.load(MyService.class);
load.forEach((t) -> {
System.out.println(t.get());
});

获取当前线程对应的应用类类加载器,加载该class。

ServiceLoader load = ServiceLoader.load(MyService.class);
load.forEach((t) -> {
System.out.println(t.get());
});

如何绕开双亲委派原则

//        Thread.currentThread().setContextClassLoader(Test02.class.getClassLoader().getParent());
//        Connection root = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8", "root", "root");
//        Class.forName("com.mysql.jdbc.Driver");ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {Driver next = driversIterator.next();System.out.println(next);}} catch(Throwable t) {// Do nothing}

常见Java虚拟机

(1)HotSpot VM
HotSpot VM是目前主流的虚拟机。像Oracle / Sun JDK、OpenJDK的各种变种(例如IcedTea、Zulu),用的都是相同核心的HotSpot VM。从Java SE 7开始,HotSpot VM就是Java规范的“参考实现”,JDK8的HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,也就是传说中的“HotRockit”,只是产品里名字还是叫HotSpot VM。这个合并并不是要把JRockit的部分代码插进HotSpot里,而是把前者一些有价值的功能在后者里重新实现一遍。移除PermGen、Java Flight Recorder、jcmd等都属于合并项目的一部分。
(2)J9 VM
J9是IBM开发的一个高度模块化的JVM。J9 VM的性能水平大致跟HotSpot VM是一个档次的。
(3)JRockit
以前Java SE的主流JVM中还有JRockit,跟HotSpot与J9一起并称三大主流JVM。这三家的性能水平基本都在一个水平上,竞争很激烈。自从Oracle把BEA和Sun都收购了之后,Java SE JVM只能二选一,JRockit就炮灰了。JRockit最后发布的大版本是R28,只到JDK6,原本在开发中的R29及JDK7的对应功能都没来得及完成项目就被终止了。
常见的几款java虚拟机

SUN Classic VM:第一款商用java虚拟机,1996年1月jdk1.0中带的java虚拟机,只能使用纯解释器的方式来执行java代码
Exact VM:准确式内存管理,编译器和解释器混合工作以及两级即时编译 ,只在Solaris平台发布
HotSport VM:即时编译,节约了时间和存储,称霸武林
KVM:简单,轻量,高可以执行,主要在手机平台使用
JRockit:BEA,世界上最快的java虚拟机,专注服务端应用,优势:垃圾回收机制,MissionControl服务套件
j9:IBM  Technology for java virtual Machines IT4J
dalvik:不能直接指向class文件,寄存器架构,执行dex文件,由class文件转化而来
MicrosoftJvm:只能在windows平台运行,
高性能java虚拟机
  Azul VM:专用虚拟机,经HotSport改进得来,运行在本公司专有硬件中
  Liquid VM:不需要操作系统的支持
taobao虚拟机:淘宝深度定制的产品,硬件依赖性比较高

JVM内存结构

1.什么是堆
2.堆内存溢出如何解决
3.堆内存发生内存泄漏如何排查
4.jmap?jps??指令的用法
5.java?jconsole?或者?jvisualvm?工具使用
6.如何排查jvm生产环境内存泄漏问题
7.字符串常量池延迟加载原理
8.如何证明字符串常量池是存放在堆中

程序计数器

1.程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
2.为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。
如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
3.程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。
简单总结:程序计数器的作用:


Java 堆是虚拟机所管理的内存中最?的?块,Java 堆是所有线程共享的?块内存区域,在虚拟机启动时创建。此内存区域的唯??的就是存放对象实例,?乎所有的对象实例以及数组都在这?分配内存

1.通过new关键 ,创建的对象存放在堆中;
2.所有线程会共享到同一个堆内存;
3.在堆内存中是有垃圾回收机制的;

内存泄漏 内存溢出 一年 学会排查
生产环境下 jvm内存泄漏的问题 和内存情况分析
堆内存 1GB

内存泄漏:

创建了很多对象存放在堆内存中,GC回收了很多次就是无法清理该垃圾对象。
哪些情况下 会发生内存泄漏的问题?
1.Threadlocal如何避免内存泄漏问题
2.第七期HashMap 自定义key 发生内存泄漏问题

内存溢出:

如果代码发生了内存泄漏的问题,内存溢出
堆内存大小1GB gc回收垃圾对象就是无法回收
从新向堆内存申请 新的内存存放对象

堆内存泄漏问题
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏的分类

1.常发性内存泄露
发生内存泄漏的代码会多次被执行到,每次被执行到的时候都会导致一块内存泄漏。
2.偶发性内存泄露
发生内存泄露的代码只有在某些特定环境或操作过程下才会发生。 常发性和偶发性是相对的。
3.一次性内存泄露
发生内存泄露的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。 比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以只会发生一次。
4.隐式内存泄露
程序在运行的过程中不断分配内存,直到在结束的时候才释放内存。 但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
5.发生的情况
长生命周期存活的对象,内部持有不适用对象的引用,导致不适用的垃圾对象无法回收。

堆内存溢出问题

1.数据库表中 存放几千万条数据---- limit 0,100000
分页查询 limit 0,20
2.循环代码 一直new 新的对象存放在我们的集合中;
3.Jar bug内存溢出
4.最大堆内存10mb

内存溢出(Out Of Memory)应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的内存。

发生的情况

1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小

堆内存溢出解决办法:

1.修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数 设置堆内存最大和最小值)
2.检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
3.使用java jconsole 或者 jvisualvm 阿里巴巴 Arthas 等。

常见内存解决方案
如何解决内存溢出
设置java虚拟机内存 启动 最大内存是为8m

/*** 演示堆内存溢出 -Xmx8m** @param args*/
public static void main(String[] args) {List list = new ArrayList<>();while (true) {list.add(new UserEntity());}
}

如何分析堆内存情况
1.Jps 查看当前系统中有哪些Java进程
在配合Jmap工具 查看堆内存占用情况 jmap -heap 进程id
2.图形化界面 Jvisualvm 或者是 jconsole.exe

Linux windows 相同
相关代码:

public class Demo04 {/*** -XX:+PrintGCDetails  输出GC回收日志(后期我们会详细学习的)** @param args* @throws InterruptedException*/public static void main(String[] args) throws InterruptedException {System.out.println("1");Thread.sleep(1000 * 30);byte[] bytes = new byte[1024 * 1024 * 10];// 申请10mb内存System.out.println("2");Thread.sleep(1000 * 30);bytes = null;System.gc();System.out.println("3");Thread.sleep(1000 * 10000);}
}

如何分析GC回收多次,还是无法释放内存

相关代码:

public class Demo05 {public static void main(String[] args) throws InterruptedException {ArrayList<UserEntity> userEntities = new ArrayList<>();for (int i = 0; i < 100; i++) {userEntities.add(new UserEntity());}Thread.sleep(100000000);}
}

使用Jvisualvm 工具

点击 堆 dump
点击查找最大对象

如何排查堆内存泄漏问题

内存泄漏发生的案例:
1.ThreadLocal内存泄漏问题
2.HashMap自定义key 避免内存泄漏问题
通过以上案例排查内存泄漏问题
排查思路:查找到 java虚拟机 哪些对象占用空间最大 前20个 列出分析
如何排查cpu飙高问题

1.生产环境cpu飙高的产生的原因
2.Arthas工具如何排查Linux环境下cpu飙高的问题
3.jvisualvm工具如何排查Linux环境下cpu飙高的问题
4.生产环境下内存泄漏的产生的原因
5.Arthas工具如何排查Linux环境下内存泄漏的问题

生产环境中排查cpu飙高的问题 比排查内存泄漏问题。

cpu飙高问题:
1.死循环---- 直接引发cpu飙高问题
2.cas 操作 循环形式 控制 一直运行 (乐观锁)
需要控制 失败循环次数 10次
3.2018 阿里云服务器上安装Redis 开放6379端口
被黑客注入挖矿程序 阿里云服务器cpu使用瞬间飙高
到100%—阿里云服务器报警
4.不要直接循环追加字符串最好StringBuilder
5. 服务器攻击 注入挖矿程序|ddos攻击 接口执行
的时间比较长 建议 最好改成mq异步实现。
Nginx或者网关入口 防御 接口限流、图形验证码 机器
模拟刷接口

cpu飙高问题 线程一直在运行 导致cpu非常忙碌

Cpu瞬间飙高 80

  1. CAS 自旋 没有控制自旋次数; 乐观锁
  2. 死循环----cpu飙高的问题;控制循环的次数
  3. 阿里云Redis被注入挖矿程序; Redis端口不要能够被外网访问
  4. 服务器被DDOS工具 导致cpu飙高; 限流、ip黑名单、图形验证码防止机器模拟攻击
  5. String直接追加字符串
    cpu发生飙高代码演示
    public static void main(String[] args) {
    new Thread(() -> {
    while (true) {
    System.out.println(“1111”);
    }
    }, “mayiktThread”).start();
    }

public static void main(String[] args) {
String s=“a”;
for (int i=0;i<80000;i++){
s+=i;
}
}

创建过程中,需要配置线程名称呢?
阿里巴巴的java开发手册 使用到线程池建议配置线程池名称
方便在后期可以定位是那个业务相关的线程。

Windows情况下排查cpu飙高问题
1.?Win操作系统打开任务管理系统 查看到那个进程占用cpu比较高

Linux情况下排查cpu飙高问题
Linux操作系统查看进程占用cpu飙高 top -c

使用arthas(阿尔萨斯) 排查cpu飙高的问题
1.下载阿尔萨斯
curl -O https://arthas.aliyun.com/arthas-boot.jar
2.java -jar arthas-boot.jar
3.选择Test04 进程 输入1
4.thread -n 3

cpu服务器飙高的问题

服务器监控系统
比如:阿里云 云cpu 飙高 阈值 70 80 90 95 100%----发送邮件报警
服务器如果集群的状态化----
运维----先知道是那台服务器节点cpu飙高 192.168.110.110 cpu飙高 70%
通知开发人员配合排查该服务器 进程中那个线程导致cpu飙高。

堆内存细节

标题:《TLAB(Thread?Local?Allocation?Buffer)》

课程内容:
1.什么是TLAB
2.TLAB?带来的问题
3.TLAB使用的相关参数
4.TLAB相关案例代码演示
5.线上环境逃逸优化分析之标量替换
6.空间分配策略

20点25分准时开始

堆内存细节划分

Java7 及之前堆内存逻辑上分为三部分:新生区+老年代区+永久区
Java8 及之前堆内存逻辑上分为三部分:新生区+老年代区+元空间

新生代:(eden(伊甸园)+from(s0)+to(s1))
老年代
注意:JDK8开始永久代被改为元空间。
新生代中有会分配 s0(from) s1(to) 区 空间是相等的。

YoungGen(新生代)
oldGen(老年代)
s0(from)
s1(to)
PermGen(永久代)
Metaspace(元空间) GC 日志 Young----新生代 old 老年代 JDK8 Metaspace元空间

堆内存参数设置
1.Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xms"和"-Xmx"来进行设置。
2.-Xms用于表示堆区的起始内存 、-Xmx则用于表示堆区的最大内存
3.堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutofMemoryError异常。
4.通常会将-Xms和-Xmx两个参数配置相同的值
原因:频繁的扩容和释放造成不必要的压力,避免在GC之后调整堆内存给服务器带来压力。
如果两个设置一样的就少了频繁扩容和缩容的步骤。内存不够了就直接报OOM
默认情况下:
初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4

  1. 设置堆空间大小的参数
    -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
    -X 是jvm的运行参数
    ms 是memory start
    -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小

  2. 默认堆空间的大小
    初始内存大小:物理电脑内存大小 / 64
    最大内存大小:物理电脑内存大小 / 4

  3. 手动设置:-Xms300m -Xmx300m
    开发中建议将初始堆内存和最大的堆内存设置成相同的值。

  4. 查看设置的参数:方式一: jps / jstat -gc 进程id
    方式二:-XX:+PrintGCDetails

//-Xms1m -Xmx300m -XX:+PrintGCDetails
while (true) {
byte[] bytes = new byte[1024 * 1024 * 10];
Thread.sleep(1000);
}

相关代码:

public class Test02 {public static void main(String[] args) {//返回Java虚拟机中的堆内存总量long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;//返回Java虚拟机试图使用的最大堆内存量long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + initialMemory + "M");System.out.println("-Xmx : " + maxMemory + "M");System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");try {Thread.sleep(3000000);} catch (InterruptedException e) {e.printStackTrace();}}
}

当前我的笔记本是16GB 不足被其他的占用了。

查看堆内存情况

1.Jps 查看当前系统中有哪些Java进程
在配合Jmap工具 查看堆内存占用情况 jmap -heap 进程id
2.图形化界面 Jvisualvm 或者是 jconsole.exe

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 314572800 (300.0MB) ##(最大堆内存)
NewSize = 104857600 (100.0MB)##新生代
MaxNewSize = 104857600 (100.0MB)##最大新生代内存
OldSize = 209715200 (200.0MB)##老年代内存
NewRatio = 2 ### NewRatio值就是设置老年代的占比,剩下的1给新生代
SurvivorRatio = 8 ##设置甸园区(Eden区)s0 s1 占比
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB##元空间
G1HeapRegionSize = 0 (0.0MB)

备注:

Heap Usage:
PS Young Generation
Eden Space:(伊甸园)
capacity = 78643200 (75.0MB)
used = 6301024 (6.009124755859375MB)
free = 72342176 (68.99087524414062MB)
8.012166341145834% used
From Space:(s0)
capacity = 13107200 (12.5MB)
used = 0 (0.0MB)
free = 13107200 (12.5MB)
0.0% used
To Space:(s1)
capacity = 13107200 (12.5MB)
used = 0 (0.0MB)
free = 13107200 (12.5MB)
0.0% used
PS Old Generation(老年代)
capacity = 209715200 (200.0MB)
used = 0 (0.0MB)
free = 209715200 (200.0MB)
0.0% used

1718 interned Strings occupying 155888 bytes.
-XX:+PrintGCDetails
输出gc回收的日志信息
-Xms300m -Xmx300m -XX:+PrintGCDetails
新生代/老年代比例参数
1.在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8 : 1 : 1,
2.当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=8
几乎所有的Java对象都是在Eden区被new出来的。
3.绝大部分的Java对象的销毁都在新生代进行了(有些大的对象在Eden区无法存储时候,将直接进入老年代),IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
4.可以使用选项"-Xmn"设置新生代最大内存大小,但这个参数一般使用默认值就可以了。

-XX:NewRatio
设置新生代比例参数:

配置年轻代与老年代在堆结构的占比
默认
-XX:NewRatio=2新生代占1,老年代占2,年轻代占整个堆的1/3
例如:
-XX:NewRatio=4新生代占1,老年代占4,年轻代占整个堆的1/5
NewRatio值就是设置老年代的占比,剩下的1给新生代
3004/5=240
300
1/5=60

相关比例参数:
-Xms300m -Xmx300m -XX:+PrintGCDetails -XX:NewRatio=4

在默认的情况下 新生代与老年代比例 1:2
设置堆内存大小:300mb 新生代 100mb 老年代200mb

-XX:SurvivorRatio

新生代中可以分为伊甸园区(Eden区),From Survivor 区 (S0区)和 To Survivor 区 (S1区)。 占用的空间分别默认为 8:1:1

设置新生代中eden和S0/S1空间的比例
默认
-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1
假如
-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
SurvivorRatio值就是设置Eden区的比例占多少,S0/S1相同

-Xms300m -Xmx300m -XX:+PrintGCDetails -XX:NewRatio=4 -XX:SurvivorRatio=4

伊甸园区(Eden区) form(s0) to(s1)

默认比例:8:1:1

100mb—新生代—
Eden 75 8 80mb
Form 12.5 1 10mb
to 12.5 1 10mb

Stop the World机制
所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行

GC的分类
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)

部分收集:
1.新生代收集(Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾收集
2.老年代收集(Major GC/Old GC):只是老年代的圾收集。
目前,只有CMS GC会有单独收集老年代的行为。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
整堆收集(Full GC):收集整个java堆和方法区(元空间)的垃圾收集。

年轻代 (Young)GC(Minor GC)触发机制
1.当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满。Survivor满不会主动引发GC,在Eden区满的时候,会顺带触发s0区的GC,也就是被动触发GC(每次Minor GC会清理年轻代的内存)
2.因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
3.Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

演示:-Xms300m -Xmx300m -XX:+PrintGCDetails

相关代码:

public static void main(String[] args) throws InterruptedException {byte[] bytes1 = new byte[1024 * 1024 * 25];Thread.sleep(5000);byte[] bytes2 = new byte[1024 * 1024 * 25];Thread.sleep(5000);byte[] bytes3 = new byte[1024 * 1024 * 25];Thread.sleep(3000000);
}

[GC (Allocation Failure) [PSYoungGen: 57353K->927K(89600K)] 57353K->52135K(294400K), 0.0199434 secs] [Times: user=0.20 sys=0.00, real=0.02 secs]

Full GC/MajorGC

MajorGC
1.指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
出现了MajorGc,经常会伴随至少一次的Minor GC。(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
2.老年代空间不足时,会先尝试触发Minor GC(新生代),如果之后空间还不足,则触发Major GC
Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
如果Major GC后,内存还不足,就报OOM了
大对象直接晋升老年代 演示:MajorGc

Full GC 触发机制
1.调用System.gc()时,系统建议执行FullGC,但是不必然执行
老年代空间不足
方法区空间不足
2.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样STW时间会短一些
大对象直接晋升老年代 演示:Full GC

大对象直接晋升老年代

演示:-Xms300m -Xmx300m -XX:+PrintGCDetails
相关代码:
//-Xms300m -Xmx300m -XX:+PrintGCDetails
byte[] bytes1 = new byte[1024 * 1024 * 80];
Thread.sleep(3000000);

老年代堆内存满了,触发fullGC
//-Xms300m -Xmx300m -XX:+PrintGCDetails
byte[] bytes1 = new byte[1024 * 1024 * 80];
Thread.sleep(5000);
byte[] bytes2 = new byte[1024 * 1024 * 130];
Thread.sleep(3000000);

触发了 fullGC 新生代 老年代 元空间

新生代GC回收

当老年代满时 触发 full GC
堆溢出:
当老年代满时 触发 full GC 如果清理完毕之后 还是没有足够的空间存放
则报错OOM异常

GC日志的分析

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2021-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintGCApplicationStoppedTime // 输出GC造成应用暂停的时间
-Xloggc:…/logs/gc.log 日志文件的输出路径

相关代码:
public static void main(String[] args) throws InterruptedException {
//-Xms300m -Xmx300m -XX:+PrintGCDetails
byte[] bytes1 = new byte[1024 * 1024 * 25];
//年轻代总空间为 89600K 当前占用62592K 回收后 2629K
Thread.sleep(3000000);
}

使用工具手动触发GC回收。

注意的是:远程工具连接java进程,java进程会占用部分空间。

控制台输出:

新生代GC日志分析(PSYoungGen)
[GC (System.gc()) [PSYoungGen: 62597K->2685K(89600K)] 62597K->28309K(294400K), 0.0260174 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[Full GC (System.gc()) [PSYoungGen: 2685K->0K(89600K)] [ParOldGen: 25624K->27986K(204800K)] 28309K->27986K(294400K), [Metaspace: 9732K->9732K(1058816K)], 0.0114928 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

触发系统GC PSYoungGen(新生代GC)

新生代占用堆内存回收前:62597K/1024=61.1m
新生代占用堆内存回收后:2685K=2.6m
新生代占用堆内存大小:89600K/1024=87.5 m

62597K->28309K(294400K)
触发GC回收前:
堆内存整个使用62597K/1024=61.1m
触发GC回收后:
堆内存整个使用28309K/1024=27.5
堆内存使用:294400K/1024=287.5 (新生代eden+to/form)+老年代

0.0260174 secs] [Times: user=0.00 sys=0.00, real=0.03 secs

GC回收的时间:整个GC花费的时间 0.0260174 secs(单位是/s)

user:指的是CPU工作在用户态所花费的时间;
real:指的是在此次GC事件中所花费的总时间;
sys:指的是CPU工作在内核态所花费的时间。

Full GC 日志分析

[Full GC (System.gc()) [PSYoungGen: 2565K->0K(89600K)] [ParOldGen: 25616K->27930K(204800K)] 28181K->27930K(294400K), [Metaspace: 9379K->9379K(1058816K)], 0.0140295 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

PSYoungGen: 2565K->0K(89600K)]
新生代GC回收前占用2565K/1024=2.5mb
新生代堆内存大小89600K/1024=87.5
ParOldGen
老年代GC回收前占用堆内存25616K/1024=25mb
老年代GC回收后占用堆内存27930K/1024=28mb
老年代占用堆内存大小204800K/1024=200mb

Full GC回收前 整个堆内存使用28181K/1024=27.5
Full GC回收后 整个堆内存使用27930K/1024=27.27
整个堆内存大小:294400K

元空间大小
垃圾回收前 元空间占用9379K
垃圾回收后 元空间占用9379K
垃圾回收后 元空间内存1058816K
Full GC执行时间:1058816K

GC日志的分析工具
C Easy是一款在线的可视化工具,易用、功能强大
网站:https://gceasy.io/

配置GC日志参数:
-Xms300m -Xmx300m -XX:+PrintGCDetails -Xloggc:D:\logs\log.log

测试代码:
//-Xms300m -Xmx300m -XX:+PrintGCDetails -Xloggc:D:\logs\log.log
byte[] bytes1 = new byte[1024 * 1024 * 25];
//年轻代总空间为 89600K 当前占用62592K 回收后 2629K
Thread.sleep(3000000);

耐心等待 30-60s左右

实战真实项目如何分析GC日志

-XX:-UseCompressedClassPointers -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=20m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:D:\logs\log.log

启动该springboot项目

报错 元空间内存溢出

java -Xms1M -XX:-UseCompressedClassPointers -XX:MetaspaceSize=1G -XX:MaxMetaspaceSize=1G -XX:+PrintGCDetails -Xloggc:D:\logs\log.log -jar mayikt-thymeleaf-1.0-SNAPSHOT.jar

Java
-Xmx300M
-XX:-UseCompressedClassPointers
-XX:MetaspaceSize=300m
-XX:MaxMetaspaceSize=300m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:D:\logs\log.log

通过工具分析GC回收过程
相关的代码:

/*** -Xms300m -Xmx300m** @param args*/
public static void main(String[] args) throws InterruptedException {ArrayList<Mayikt> arrayList = new ArrayList<>();while (true) {arrayList.add(new Mayikt());Thread.sleep(20);}}static class Mayikt {private byte[] bytes = new byte[1024 * 1024];
}

下载 visual gc 插件

为什么堆会分代?
为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。
老年代:存放新生代中经历多次GC仍然存活的对象。
TLAB(Thread Local Allocation Buffer)
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

TLAB属于Eden区域中的内存,不同线程的TLAB都位于Eden区,Eden区对所有的线程都是可见的。每个线程的TLAB有内存区间,在分配的时候只在这个区间分配。

1.堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
2.由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
3.为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

特点:
TLAB解决了:直接在线程共享堆上安全分配带来的线程同步性能消耗问题(解决了指针碰撞)。
TLAB内存空间位于Eden区。
默认TLAB大小为占用Eden Space的1%。

1.也就是说,虽然每个线程在初始化时都会去堆内存中申请一块TLAB,并不是说这个TLAB区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。

2.并且,在TLAB分配之后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能通过TLAB分配内存,存放在Eden区,但是还是会被垃圾回收或者被移到Survivor Space、Old Gen等。

3.还有一点需要注意的是,我们说TLAB是在eden区分配的,因为eden区域本身就不太大,而且TLAB空间的内存也非常小,默认情况下仅占有整个Eden空间的1%。所以,必然存在一些大对象是无法在TLAB直接分配。

4.遇到TLAB中无法分配的大对象,对象还是可能在eden区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。

TLAB 带来的问题
1.虽然在一定程度上,TLAB大大的提升了对象的分配速度,但是TLAB并不是就没有任何问题的。
2.比如一个线程的TLAB空间有100KB,其中已经使用了80KB,当需要再分配一个30KB的对象时,就无法直接在TLAB中分配,遇到这种情况时,有两种处理方案:
2.1如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。
2.2如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,重新申请TLAB空间再次进行内存分配。
以上两个方案各有利弊,如果采用方案1,那么就可能存在着一种极端情况,就是TLAB只剩下1KB,就会导致后续需要分配的大多数对象都需要在堆内存直接分配。
如果采用方案2,也有可能存在频繁废弃TLAB,频繁申请TLAB的情况,而我们知道,虽然在TLAB上分配内存是线程独享的,但是TLAB内存自己从堆中划分出来的过程确实可能存在冲突的,所以,TLAB的分配过程其实也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。
3.为了解决这两个方案存在的问题,虚拟机定义了一个refill_waste的值,这个值可以翻译为“最大浪费空间”。
3.1当请求分配的内存大于refill_waste的时候,会选择在堆内存中分配。若小于refill_waste值,则会废弃当前TLAB,重新创建TLAB进行对象内存分配。
3.2前面的例子中,TLAB总空间100KB,使用了80KB,剩余20KB,如果设置的refill_waste的值为25KB,那么如果新对象的内存大于25KB,则直接堆内存分配,如果小于25KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间,给新对象分配内存。

TLAB使用的相关参数
1.TLAB功能是可以选择开启或者关闭的,可以通过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。
2.TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳的运行状态。如果需要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB来禁用,并且使用-XX:TLABSize来手工指定TLAB的大小。
3.TLAB的refill_waste也是可以调整的,默认值为64,即表示使用约为1/64空间大小作为refill_waste,使用参数:-XX:TLABRefillWasteFraction来调整。
4.如果想要观察TLAB的使用情况,可以使用参数-XX:+PringTLAB 进行跟踪。

G1收集器

TLAB相关案例代码演示
Eden区指针碰撞,需要模拟多线程并发申请内存空间。且需要关闭逃逸分析 -XX:-DoEscapeAnalysis -XX:+UseTLAB
相关代码:com.mayikt.days07.Test11

/*** 测试 关闭逃逸分析 开启UseTLAB 效果演示* -Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+UseTLAB* -XX:TLABWasteTargetPercent=1 -XX:+PrintCommandLineFlags  -XX:+PrintGCDetails*/
public class Test11 {private static final int threadNum = 100;private static CountDownLatch latch = new CountDownLatch(threadNum);private static final int n = 50000000 / threadNum;private static void alloc() {byte[] b = new byte[100];}public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < threadNum; i++) {new Thread(() -> {for (int j = 0; j < n; j++) {alloc();}latch.countDown();}).start();}try {latch.await();} catch (InterruptedException e) {System.out.println("hello world");}long end = System.currentTimeMillis();System.out.println((end - start) + "ms");}}

开启了tlab效果 代码运行时间:

关闭了tlab效果 代码运行时间:
-Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+UseTLAB -XX:TLABWasteTargetPercent=1 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

经过对比,相差10倍左右

New出来对象一定存放在 堆中吗?

New出来对象一定存放在 堆中吗?
答案:不完全。

如果将对象存入在堆内存中,会发生gc回收,效率非常低。
有些new 对象可能没有存放在堆中,存放在栈帧中。
jvm官方文档 jvm 内存逃逸 --new 对象存放在栈空间
内存逃逸分析

1.随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

2.在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
3.此外,前面提到的基于OpenJDK深度定制的TaoBao VM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

周志明jvm

逃逸分析

如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
1.当一个对象在方法中被定义后,对象只在方法内部使用(栈帧中使用),则认为没有发生逃逸。----new存放在栈空间上。
2.当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸 ----new存放在堆上

当一个对象, 能被其他方法访问到时, 这种逃逸叫做方法逃逸;

当一个对象, 能被其他线程访问到时, 这种逃逸叫做线程逃逸。

逃逸分析案例
逃逸分析案例1
没有发生逃逸的对象,则可以分配到空间上((没有线程安全问题),随着方法执行的结束,栈帧空间就被移除,也无需GC回收。

// 逃逸案例分析
public static void main(String[] args) {}public void mayikt() {User user = new User();
}class User {}

相关代码:com.mayikt.days07.Test01
逃逸分析案例2
StringBuffer sb 发生了逃逸,不能在栈上分配 因为 StringBuffer对象会被外部其他方法使用

public static void main(String[] args) {StringBuffer stringBuffer = createStringBuffer();
}
public static StringBuffer createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb;
}StringBuffer sb不发生逃逸,可以这样写
public static String createStringBuffer2(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();
}

相关代码:com.mayikt.days07.Test02

逃逸分析案例3

public class Test03 {private static User user;public static User getInstance() {return user == null ? new User() : user;}public static void getInstance2() {user = new User();}public void setUser(User user) {this.user = user;}public void getInstance3() {User user = getInstance();}static class User {}
}

开发者使用局部变量,没有发生逃逸,对象会存放在栈空间中 当栈帧方法结束之后 该对象会自动消失
不需要GC回收垃圾。
逃逸分析参数设置
在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析
通过选项“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果

逃逸优化分析
栈上分配
将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配,而不是堆上分配。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景:在逃逸分析中,已经说明了,分别是给成员变量赋值、方法返回值、实例引用传递。

注意:不要 使用debug 运行 测试逃逸分析 效果, 否则无效!!!
测试代码:com.mayikt.days07.Test04

/*** -Xmx256m -Xms256m  -XX:+DoEscapeAnalysis -XX:+PrintGCDetails  开启逃逸分析* -Xmx256m -Xms256m  -XX:+DoEscapeAnalysis -XX:+PrintGCDetails  关闭逃逸分析* <p>* 注意:不要 使用debug 运行 测试逃逸分析 效果, 否则无效!!!!!** @param args* @throws InterruptedException*/
public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();for (int i = 0; i <= 10000000; i++) {mayikt();}long end = System.currentTimeMillis();System.out.println("程序执行的时间:" + (end - start));Thread.sleep(1000000);
}public static void mayikt() {// 未发生逃逸User user = new User();
}static class User {}

开启逃逸分析:
-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails 开启逃逸分析

3s 毫秒 没有触发 任何GC操作
关闭逃逸分析:

触发了新生代GC回收。

相关代码:com.mayikt.days07.Test04

同步省略(锁的消除)
Eden 私有
没有发生逃逸—在方法里面使用
发生逃逸 方法外部需要使用 —对象存放在 堆空间
Jlt 编译期分析

1.线程同步的代价是相当高的,同步的后果是降低并发性和性能。
2.在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被其他线程访问。
3.如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

private Object objectLock = new Object();public static void main(String[] args) {Test05 test05 = new Test05();test05.mayikt();
}public void mayikt() {synchronized (objectLock) {System.out.println(objectLock);}
}

相关代码:com.mayikt.days07.Test05
标量替换
1.标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
2.相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
3.在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

标量 聚合量
开启标量替换:


public static void main(String[] args) {mayikt();
}public static void mayikt() {Mayikt mayikt = new Mayikt();mayikt.x = 10;mayikt.y = 20;System.out.println(mayikt.x + "," + mayikt.y);
}/*** 以上代码,经过标量替换后,就会变成*/public static void mayikt() {int x = 10;int y = 20;System.out.println(x + "," + y);
}static class Mayikt {private int x;private int y;
}

相关代码:com.mayikt.days07.Test07

可以看到,Mayikt 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。
那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。
标量替换参数设置
参数 -XX:+ElimilnateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

标量替换案例演示

默认的情况下 JDK 已经开启了 标量替换

相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:-EliminateAllocations

相关代码:

    public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {mayikt();}long end = System.currentTimeMillis();System.out.println("花费的时间为: " + (end - start) + " ms");}public static void mayikt() {Mayikt mayikt = new Mayikt();mayikt.userName = "mayikt";mayikt.age = 21;}/*** 以上代码,经过标量替换后,就会变成*///    public static void mayikt() {
//        int x = 10;
//        int y = 20;
//        System.out.println(x + "," + y);
//    }static class Mayikt {private String userName;private Integer age;}

没有开启标量替换:
相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:-EliminateAllocations

执行该代码需要花费50毫秒左右

开启标量替换:
相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:+EliminateAllocations
只需要5毫秒

逃逸分析的优缺点
1.关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟的。
其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。2.但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
3.注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot JVM中并未这么做(刚刚演示的效果,是因为HotSpot实现了标量替换),这一点在逃逸4.分析相关的文档里已经说明,所以可以明确在HotSpot虚拟机上,所有的对象实例都是创建在堆上。
目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

逃逸分析优点:

1.减少GC回收的次数 提高程序的效率
2.将new对象存放在栈空间中,当方法执行结束 自动释放内存
3.但是我们在写代码的过程基本上都是逃逸了。
堆空间常见设置参数
1.测试堆空间常用的jvm参数:
2.-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
3.-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps:查看当前运行中的进程
4. jinfo -flag SurvivorRatio 进程id
5.-Xms:初始堆空间内存 (默认为物理内存的1/64)
6.-Xmx:最大堆空间内存(默认为物理内存的1/4)
7.-Xmn:设置新生代的大小。(初始值及最大值)
8.-XX:NewRatio:配置新生代与老年代在堆结构的占比
9.-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
10.-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
11.-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保


1.Java 虚拟机栈也是线程私有的,它的?命周期和线程相同,描述的是 Java?法执?的内存模型,每次?法调?的数据都是通过栈传递的。
2.虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
相关代码:

public static void main(String[] args) {a();
}public static void a() {
}public static void b() {
}public static void c() {
}

方法压栈与出栈:

栈会遵循先进后出原则 每个方法会创建一个栈帧,在栈帧中存放
该方法对应的局部变量表

Idea 调试分析栈帧:

栈帧内部结构原理分析

在每一个方法 栈帧中都会有自己独立的
局部变量表(存放当前方法对应的局部变量);
操作数栈(或表达式栈);
动态连接(或指向运行时常量池的方法引用)
方法出口(或方法正常退出或者异常退出的定义)
栈帧包含方法的所有信息
javap -v Demo03.class

局部变量表
标记清除 标记整理 标记复制 gcroot 引用链
引用计数法
新生代

com.mayikt.days02.Demo04
相关代码:

public static void main(String[] args) {String str = "mayikt";int j = 20;double d = 66.66;boolean b = true;
}

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
Slot(变量槽–index)
局部变量表最基本的存储单元就是变量槽。
在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。
注意:short,byte,boolean等数据也占用一个变量槽,因为jvm会在存储时将上述变量转为int类型(变量槽是最基本存储单元,无法分割,只能整个使用)。

JVM会为局部变量表中每一个变量分配变量槽,并记录其的存储位置,比如main函数方法传递了String [] args 数组 变量args就存储在index为0的变量槽中,变量d因为为64位,需要占用两个变量槽(3和4),变量b因为D占用了两个变量槽,所以直接从index5处开始存储。

jclasslib分析字节码

  1. idea 安装使用
    打开idea 中的settings > plugins 搜索 jclasslib 插件 进行安装 重启生效
    重启后点击view > 选择show bytecode with jclasslib

jclasslib git地址:https://github.com/ingokegel/jclasslib

不赋值(在代码块中赋值)的变量,在局部变量表中不显示,但局部变量表仍会为其预留位置。
相关的代码:
public void mayikt001() {
int a;
int b;
int c;
}

变量槽的复用

public static void mayikt002() {
int a = 0;
{
int b=30;
System.out.println(“mayikt”);
}
int c = 0;
}

此处原因就是JVM对变量槽有一个复用性为,当变量b超出其作用域后不再生效,所以变量c直接占据了b的位置,所以局部变量表中会少一个位置。

This底层原理
如果当前的方法是实例方法或者是构造方法,则jvm默认会在局部变量表中创建 一个
当前对象 变量名称为 this, 存入在我们当前方法对应的局部表 第0个位置 这样我们就可以在实例方法中 使用 this,静态方法不会。
代码演示:
public void mayikt() {
int j = 20;
}

局部变量表总结
局部变量表只对已确定一定有值的变量和方法参数进行记录,在程序执行中得以直接使用,存储在量变槽中,如果是long和double,则需要占用两个变量槽,实例方法和构造方法会自动创建this变量,并且如果代码块结束(作用域结束),jvm会对变量槽有一个复用的行为,以便于节省空间。

操作数栈分析

相关代码:
public int compute() {
int a = 10;
int b = 20;
int c = (a + b) * 10;
return c;
}

iconst_0:将int类型的0值压入操作数栈
istore_1: 弹出操作数栈顶的值赋给局部变量表下标为1的变量
iload_1: 将局部变量表下标为1的位置存储的值压入操作数栈
iinc 1 by 1:取局部变量表下标为1的位置存储的值加上1
istore_1:弹出操作数栈顶的值赋给局部变量表下标为1的变量

底层汇编代码:
0: bipush 10 ## 将一个8位带符号整数压入栈
2: istore_1 局部变量表中槽1的位置存入10;
3: bipush 20 ## 将一个8位带符号整数压入栈 20
5: istore_2 局部变量表中槽2的位置存入20;
6: iload_1 从局部变量表中槽1的位置 获取 变量a=10;
7: iload_2 从局部变量表中槽2的位置 获取 变量b=20;
8: iadd iadd 执行int类型的加法 10+20
9: bipush 将一个8位带符号整数压入栈 10
11: imul imul 执行int类型的乘法30*10
12: istore_3 局部变量表中槽3的位置存入300 c=300;
13: iload_3 最后返回局部变量表中槽3的位置
14: ireturn

++i与i++的底层原理
i++是先赋值,然后再自增;++i是先自增,后赋值。

i++是直接在局部变量表加的,没有在操作数栈里运算
I++ 与++i底层区别

I++ 先将局部变量表中的值 压入放入到操作数栈中
,在直接对局部变量中做+1操作。
++i 先将局部变量表中的 值 做1+的操作,在将局部变量表中 加1
之后的结果 压入到操作数栈中。

动态连接–常量池
方法出口 定义异常

栈溢出
StackOverflowError(栈溢出)
StackOverflowError代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error。

public class StackOverFlow {private int i;public void plus() {i++;plus();}public static void main(String[] args) {StackOverFlow stackOverFlow = new StackOverFlow();try {stackOverFlow.plus();} catch (Error e) {System.out.println("Error:stack length:" + stackOverFlow.i);e.printStackTrace();}}
}

在栈空间内存中 是否会发生线程安全问题呢?
不会的
多线程知识

动态链接
动态链接: 每个栈帧都保存了 一个可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接
方法出口
方法返回地址

方法区(元空间)

?法区与 Java 堆?样,是各个线程共享的内存区域,它?于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。?法区也被称为永久代。

栈、堆、方法区的交互关系

1.UserEntity.class信息存放在 常量池中
常量池在JDK1.6存放在方法区中;
常量池在JDK1.8存放在元空间中;
3.New UserEntity 存放在堆内存中;
4.返回一个内存地址给我们的 userEntity 存放在栈中
方法区的理解
1.官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4

2.《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区可以看作是一块独立于Java堆的内存空间。

3.方法区主要存放的是 Class信息,而堆中主要存放的是实例化的对象
4.方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。
5.方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
6.方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
7.方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace
8. 造成方法区溢出的原因:
A.加载大量的第三方的jar包
B.Tomcat部署的工程过多
C.大量动态的生成反射类
D.JDK1.6 中定义大量的字符串
关闭JVM就会释放这个区域的内存。

D:\path\jdk\jdk8\jdk\bin\jvisualvm.exe 或者是:jconsole.exe

如何模拟方法区内存溢出?
类加载器 加载第三方的jar包(class文件)

JDK6 环境 方法区 默认的初始化 大小 20m 左右
JDK8 元空间 存储 不是依赖虚拟机内存 是依赖直接内存 系统内存 16gb
虚拟机设置是为1GB JDK8 依赖 系统内存16gb

设置方法区大小与 OOM

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
JDK7及以前(永久代)
1.通过-XX:Permsize来设置永久代初始分配空间。默认值是20.75M
2.-XX:MaxPermsize来设定永久代最大可分配空间。
3.当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。
JDK8及以后(元空间)
JDK8 版本设置元空间大小
1.元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
2.默认值依赖于平台,Windows下,-XX:MetaspaceSize 约为21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
3.与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
4.-XX:MetaspaceSize:设置初始的元空间大小。对于一个 64位 的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
5.如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

方法区OOM异常如何解决
MayiktOOMClassLoader 类继承 ClassLoader 类,获得 defineClass() 方法,可自己进行类的加载
JDK7及以前(永久代)

public class MayiktOOMClassLoader extends ClassLoader {/*** jdk6/7中:* -XX:PermSize=10m -XX:MaxPermSize=10m* <p>* jdk8中:* -XX:-UseCompressedClassPointers -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m** @param args*/public static void main(String[] args) {int j = 0;try {MayiktOOMClassLoader test = new MayiktOOMClassLoader();for (int i = 0; i < 10000; i++) {//创建ClassWriter对象,用于生成类的二进制字节码ClassWriter classWriter = new ClassWriter(0);//指明版本号,修饰符,类名,包名,父类,接口classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);//返回byte[]byte[] code = classWriter.toByteArray();//类的加载test.defineClass("Class" + i, code, 0, code.length);//Class对象j++;}} finally {System.out.println(j);}}
}

JDK8及以后(元空间)

public class MayiktOOMClassLoader extends ClassLoader {/*** jdk6/7中:* -XX:PermSize=10m -XX:MaxPermSize=10m* <p>* jdk8中:* -XX:-UseCompressedClassPointers -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m** @param args*/public static void main(String[] args) {int j = 0;try {MayiktOOMClassLoader test = new MayiktOOMClassLoader();for (int i = 0; i < 10000; i++) {//创建ClassWriter对象,用于生成类的二进制字节码ClassWriter classWriter = new ClassWriter(0);//指明版本号,修饰符,类名,包名,父类,接口classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);//返回byte[]byte[] code = classWriter.toByteArray();//类的加载test.defineClass("Class" + i, code, 0, code.length);//Class对象j++;}} finally {System.out.println(j);}}
}

方法区的内部结构

方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

Class 信息

类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
这个类型的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
这个类型的修饰符(public,abstract,final的某个子集)
这个类型直接接口的一个有序列表
域(Field)信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(包括 void 返回类型),void 在 Java 中对应的为 void.class
方法参数的数量和类型(按顺序)
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

----字节码 class
举例分析

Javap -p -v MayiktTest03.class

反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看 private 权限类型的字段或方法
类的信息
Classfile /D:/code/mayikt-new_jvm/out/production/mayikt-new_jvm/com/mayikt/days04/MayiktTest03.class
Last modified 2021-8-12; size 958 bytes
MD5 checksum d6457311d7cfa994e8230a7be7a28b8a
Compiled from “MayiktTest03.java”
public class com.mayikt.days04.MayiktTest03
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER

ACC_PUBLIC 表示该类 是一个 public
JDK运行版本 1.8

域(Field)信息
public int i;
descriptor: I
flags: ACC_PUBLIC

private static java.lang.String mayiktStr;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC

descriptor: I 表示字段类型为 int
flags: ACC_PUBLIC 表示字段权限修饰符为 public

方法信息

descriptor: ()V 表示方法返回值类型为 void
flags: ACC_PUBLIC 表示方法权限修饰符为 public
stack=2 表示操作数栈深度为 2
locals=1 表示局部变量个数为 1 个(实例方法包含 this)
mayikt01() 方法虽然没有参数,但是其 args_size=1 ,这时因为将 this 作为了参数

静态方法访问
静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它

public class Test03 {public static void main(String[] args) {Mayikt mayikt = null;mayikt.mayikt();System.out.println(mayikt.count);}
}class Mayikt {public static int count = 1;public static void mayikt() {System.out.println("mayikt!");}
}

运行结果是可以正常的访问:mayikt方法
汇编代码分析:

#2 #24 25

全局静态常量
全局常量就是使用 static final 进行修饰
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

public class Test04 {private static final int code = 200;private static final Test04 test04 = new Test04();private int z = 30;public static void main(String[] args) {}
}

永久代演进过程
JDK1.7 有永久代,字符串常量池,静态变量移除,保存在堆中 其他的常量池存放在永久代
JDK1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

永久代为什么要被元空间替代?
只有Hotspot才有永久代。BEA JRockit、IBMJ9等来说,是不存在永久代的概念的

1.JDK8 开始 无永久代改为元空间,分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间;

2.为永久代设置空间大小是很难确定的,在某些场景下,如果动态加载类过多,容易产生方法区的OOM。比如某个实际Web工程中,因为功能点比较多,需要加载的类很多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 java.lang.OutOfMemoryError:PermGen space

3.而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
相关证明代码:
com.mayikt.days05.MayiktOOMClassLoader

字符串常量池为什么要调整位置?
1.JDK7中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。
2.这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
如何证明 静态变量是存放在堆中?

静态引用对应的对象实体(也就是这个new byte[1024 * 1024 * 10])始终都存在堆空间
在栈内存中的变量:bytes 在JDK6,JDK7,JDK8存放位置中有所变化

JDK6

JDK8

Linux 内核中 零拷贝

下周二:垃圾回收算法 标记清除 标记整理 标记复制
新生代老老年代 元空间 gc日志 怎么看。
垃圾收集器—理论。

常量池

常量池(Constant Pool)
1.1常量池(Class文件常量池):.java经过编译后生成的.class文件,是Class文件的资源仓库。
Java源代码 编译成 class

常量池分类:
1.运行时常量池 类加载器读取class文件到内存中,该常量池就是运行时常量池
2.静态常量池 java 编译 class 还没有被类加载器加载该class文件
3.字符串常量池 jdk7 之前 方法区、JDK7 存放在堆中 JDK 8方法区改 元空间
字符串常量池还是存放在我们堆中。

1.2 常量池中主要存放:字面量(文本字符串,final常量)和符号引用(类和接口的全局定名,字段的名称和描述,方法的名称和描述)
常量池:通过一张表,虚拟机根据该常量表扎到执行的类名、方法名、
参数类型、字面量。

javap -verbose中或者javap -v class文件名称

常量池的分类
静态常量池、字符串常量池、运行时常量池

静态常量池
class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

类和接口的全限定名
字段的名称和描述符
方法的名称和描述符

数量值
字符串值
类引用
字段引用
方法引用

运行常量池
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在元空间中,我们常说的常量池,就是指方法区中的运行时常量池。
字符串常量池
JDK1.7之前,运行时常量池(字符串常量池也在里边)是存放在方法区,此时方法区的实现是永久带。
JDK1.7字符串常量池被单独从方法区移到堆中,运行时常量池剩下的还在永久带(方法区)
JDK1.8,永久带更名为元空间(方法区的新的实现),但字符串常量池池还在堆中,运行时常量池在元空间(方法区)。

字符串常见面试题
字符串面试题1
字符串面试题1
字符串面试题1
字符串面试题1
String intern方法

String与StringBuffer区别
String延迟加载
字符串是懒加载的,当真正需要加载的字符串才会加载到内存中,如果已经在常量池中存在
则直接复用该常量池内存地址。

如何证明字符串常量池是存放在堆中

JDK7开始 字符串常量池 存放在堆中

JDK1.8字符串常量池存放在堆中
测试代码:

public class StringDemo020 {// JDK1.8 设置 堆内存大小 -Xmx1m -Xms1m  -XX:-UseGCOverheadLimitpublic static void main(String[] args) {ArrayList<String> arrayList = new ArrayList<>();int count = 0;try {for (int i = 0; i < 250000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}

报错:
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

JDK1.6字符串常量池存放在方法区
测试代码:

    // jdk1.6 设置 方法区大小参数:-XX:MaxPermSize=1mpublic static void main(String[] args) {ArrayList arrayList = new ArrayList();int count = 0;try {for (int i = 0; i < 2500000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}

Exception in thread “main” java.lang.OutOfMemoryError: PermGen space

为什么jdk7 String常量池放入到堆中

对象的创建方式
对象内存布局
字节码角度分析对象创建过程
对象创建步骤细节

1.检查这个符号引用代表的类是否已被加载、解析和初始化过

对象访问定位
垃圾回收算法

什么是垃圾
1.垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
外文:An object is considered garbage when it can no longer be reached from any pointer in the running program.
2.如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出。

为什么需要GC

1.对于高级语言(java、python、c#)来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
2.除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
3.随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

C/C++时代(垃圾回收)

1.在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。
2.这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
3.C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了现代开发语言必备的标准。
自动内存管理

自动内存管理优点

1.我们使用动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
2.自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
自动内存管理缺点
1.使用自动内存管理,太过度依赖“自动”,程序员开发者需要掌握 jvm垃圾回收原理,方便在后期定位程序内存溢出的问题。

垃圾回收内存核心区域
堆和元空间(方法区)

回收次数:
1.频繁的回收新生代;
2.较少的回收老年代;
3.基本不回收元空间(方法区);

垃圾回收相关算法
引用计数法
使用一个计数器来记录每个对象被引用的次数。当对象被引用时,计数器加1;当对象的引用被解除时,计数器减1;当引计数器为0时,对象会被回收。
注意:在java中是没有使用引用计数法
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:从空间/时间复杂度考虑:
1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。
循环依赖
相关代码:com.mayikt.days08.Test02
相关图:

当栈空间中 mayikt1 、mayikt2 ==null时,mayikt1对象与mayikt2对象
内部实现相互引用形成相互引用,导致该对象无法被回收,最终导致内存泄漏
的问题。


public static void main(String[] args) {Mayikt mayikt1 = new Mayikt("mayikt", 22);Mayikt mayikt2 = new Mayikt("meite", 22);mayikt1.reference = mayikt2;mayikt2.reference = mayikt1;}static class Mayikt {private String name;private Integer age;public Mayikt(String name, Integer age) {this.name = name;this.age = age;}private Mayikt reference;
}

相关代码:com.mayikt.days08.Test02
如何证明Java没有使用引用计数法
相关代码:com.mayikt.days08.Test03

/*** -XX:+PrintGCDetails* 证明:java使用的不是引用计数算法*/
public static void main(String[] args) {Mayikt mayikt1 = new Mayikt("mayikt", 22);Mayikt mayikt2 = new Mayikt("meite", 22);mayikt1.reference = mayikt2;mayikt2.reference = mayikt1;mayikt1 = null;mayikt2 = null;//这里发生GC,mayikt1和mayikt2能否被回收?System.gc();}static class Mayikt {private String name;private Integer age;//这个成员属性唯一的作用就是占用一点内存private byte[] bigSize = new byte[5 * 1024 * 1024];//5MBpublic Mayikt(String name, Integer age) {this.name = name;this.age = age;}private Mayikt reference;
}

如果使用的是引用计数法不小心直接把mayikt1.reference和mayikt2.reference置为null。则在Java堆中的两块内存依然保持着互相引用,无法被回收。
但是java没有使用引用计数法。

引用计数法解决循环依赖问题

在Python中使用到引用计数法
为了解决循环依赖问题:
1.手动解除 解决对象之间依赖关系;
2.使用弱引用weakref,weakref是Python提供的标准库,解决循环引用。

可达分析算法(GCRoot)

可达性分析算法概念:也可以称为根搜索算法、追踪性垃圾收集
1.相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
2.相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)

可达性分析算法实现思路
所谓"GCRoots”根集合就是一组必须活跃的引用

其基本思路如下:
1.可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
2.使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
3.如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
4.在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

哪些对象可以作为 GC Root 呢?
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
3.方法区中类静态属性引用的对象
4.方法区中常量引用的对象 比如:字符串常量池(StringTable)里的引用
5.所有被同步锁synchronized持有的对象
6.Java虚拟机内部的引用。
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。
7.反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
虚拟机栈中引用的对象

虚拟机栈(栈帧中的本地变量表)中引用的对象

如下代码所示,o 是栈帧中的本地变量,当 o= null 时,由于此时 o充当了 GC Root 的作用,o 与原来指向的实例 new Object() 断开了连接,所以对象会被回收。
public static void main(String[] args) {
Object o = new Object();
o = null;
}
相关代码:com.mayikt.days08.Test04
方法区中类静态属性引用的对象

如下代码所示,当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!

//方法区中类静态属性引用的对象
private static Test05 s;

public static void main(String[] args) {
Test05 t = new Test05();
t.s = new Test05();
t = null;
}

相关代码:com.mayikt.days08.Test05

方法区中常量引用的对象

如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收

//方法区中常量引用的对象
private static final Test06 s = new Test06();public static void main(String[] args) {Test06 a = new Test06();a = null;
}
相关代码:com.mayikt.days08.Test06
所有被同步锁synchronized持有的对象private Object object = new Object();//-XX:+PrintGCDetails
public static void main(String[] args) {Test07 test07 = new Test07();test07.cal();}public void cal() {MayiktLock mayiktLock = new MayiktLock();synchronized (mayiktLock) {mayiktLock = null;System.gc();try {Thread.sleep(50000000);} catch (Exception e) {e.printStackTrace();}}
}class MayiktLock {private byte[] bytes = new byte[5 * 1024 * 1024];
}

相关代码:com.mayikt.days08.Test07
(GCRoot)回收过程中stw问题
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

MAT/分析GC Roots溯源
MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,是一款免费的性能分析工具。
大家可以在http://www.eclipse.org/mat/下载并使用MAT

1.获取 dump 文件方式
这些文件记录了JVM运行期间的内存占用、线程执行等情况,这就是我们常说的dump文件。常用的有heap dump和thread dump(也叫javacore,或java dump)。我们可以这么理解:heap dump记录内存信息的,thread dump是记录CPU信息的。

对象的 finalize方法
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。

1.对象销毁前的回调方法:finalize()
2.Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
3.当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
4.finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

周志明 jvm第三版 jvm

生存还是死亡?
1.由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
2.如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它立即进行回收就是不合理的。为此,定义虚拟机中的对象可能的三种状态。
3.如下:
3.1可触及的:从根节点开始,可以到达这个对象。
3.2可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
3.3不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

finalize() 细节过程
1.判定一个对象objA是否可回收,至少要经历两次标记过程:
2.如果对象objA到GC Roots没有引用链,则进行第一次标记。
3.进行筛选,判断此对象是否有必要执行finalize()方法
4.如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
5.如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
6.finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。

代码演示 finalize() 方法可复活对象

public class Test08 {public static Test08 obj;//类变量,属于 GC Root@Overrideprotected void finalize() throws Throwable {System.out.println("我Test08类重写了 父类中的 finalize 方法 ");obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系}public static void main(String[] args) throws InterruptedException {try {obj = new Test08();obj = null;System.gc();//调用垃圾回收器System.out.println("第1次 gc");// 因为Finalizer线程优先级很低,暂停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("该obj 对象 是为null的呢");} else {System.out.println("该obj 对象 还活着");}System.out.println("第2次 gc");// 下面这段代码与上面的完全相同,但是这次自救却失败了obj = null;System.gc();// 因为Finalizer线程优先级很低,暂停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("该obj 对象 是为null的呢");} else {System.out.println("该obj 对象 还活着");}} catch (InterruptedException e) {e.printStackTrace();}}
}

清除算法

指针碰撞与空闲列表
指针碰撞

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump thePointer)
指针碰撞:
1.假设java堆中的内存是绝对规整的,指针作为已分配内存和未分配内存的分界线;
2.给变量分配内存的过程就是将作为分界线的指针未分配内存空间挪动一个变量的大小。

空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(FreeList)

1.如果java堆中的内存分配不是很规整,虚拟机就会
维护一个空闲列表,用来记录剩余可用的内存空间;
2.每次为变量分配内存后动态的去维护这个列表。

如果区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-压缩算法(Mark-Compact)

标记-清除算法

1.标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
2.清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其
清除

回收后:

标记清除算法缺点
1.标记清除算法的效率不是很高
2.在进行GC的时候,需要停止整个应用程序,stw问题严重
3.会产生空闲内存不连续的,产生大量的碎片问题,需要维护一个空闲列表

标记清除算法原理
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放从空闲列表查找内存地址存放。(也就是覆盖原有的地址)。

标记清除算法应用场景
标记清除算法的老年代垃圾回收器

标记-复制算法
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
标记-复制原理
将活着的内存空间分为两块,每次只使用其中一块(s0/s1 两块空间是相等的),在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收,将存活的对象拷贝到另外区间,内存地址可能会发生变化的。

s0/s1区交换来回拷贝。

标记-复制优缺点

1.优点
1.1复制过去以后保证空间的连续性,解决了“碎片”问题。
1.2.没有标记和清除过程,实现简单,运行高效
2.缺点
2.1此算法的缺点也是很明显的,就是需要两倍的内存空间(s0/s1)。
2.2以空间换时间的形式 提高效率上的问题。
2.3将存活的对象拷贝到另外区间,内存地址可能会发生变化的。

复制算法的应用场景

标记复制算法应用在新生代比较多,因为新生代触发gc回收频率是非常高的
所以最好使用效率比较高的回收算法—标记复制算法

老年代为什么不推荐使用标记复制算法呢?缺点 老年代中的对象是大对象、拷贝效率很低
标记复制算法适合于拷贝一些较小的对象

标记-压缩算法
1.复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高
2.标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。

标记-压缩算法原理

第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的另一端,按顺序排放,之后,清理边界外所有的空间。

标记压缩算法与标记清除算法的区别
1.标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清-压缩(Mark-Sweep-Compact)算法。
2.标记-清除 不会移动内存地址、标记-压缩算法会进行整理碎片会发生移动内存地址
3.标记-压缩算法采用指针碰撞、标记-清除采用空闲列表;

垃圾回收算法总结

标记清除 标记整理    复制算法

速率 中等(
1.不会移动存活对象
2.标记清除 垃圾对象存放空闲列表中
) 最慢(
1.标记清除
2.移动存活对象
3.解决碎片化问题
) 快(
1.将存活对象
拷贝到另外一个区间
2.发生了移动对象
3.栈帧中引用内存地址发生变化


空间开销 少(会发生碎片化) 少(不堆积碎片) 通常需要存活对象的2倍空间(不堆积碎片) s0 s1 来回拷贝
移动对象 否 是 是
应用场景 老年代 老年代 新生代

对象内存布局
字节码角度分析对象创建过程
相关代码:
public static void main(String[] args) {
Mayikt o = new Mayikt();
}

static class Mayikt {

}

javap -v -p Test01.class

对象创建方式

1.new对象 new Mayikt()
2.使用反射创建对象 class.formName
3.使用clone()克隆对象
4.使用反序列化对象 将对象转化成json格式 在将json转化成对象
5.第三方库 Objenesls创建对象
重点new 对象 底层细节
对象创建步骤细节
摘自于周志明 jvm 第三版本 修改过;

设计到非常多知识----

1.检查该对象的类是否已被加载、解析和初始化过(元空间);
细节:
New 符号引用#2 = Class #24 // com/mayikt/days09/Test01$Mayikt

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在(元空间中)常量池中定位到该类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过(判断该类是否被元空间加载过)。如果没有,那必须先执行相应的类加载过程(则使用双亲委派机制 查找该类(类的全路径),如果没有查询到则报错:ClassNotFoundException)。
2.在类加载检查通过后,接下来虚拟机将为新生对象分配内存
2.1如果堆内存规整则使用 指针碰撞;
2.2如果堆内存不规整则使用 空闲列表;

3.处理并发问题
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
解释:
3.1采用CAS+失败重试保证更新的原子性;
3.2 为每个线程在eden(伊甸园区)分配一块 当前线程独有的TLAB空间;
4.属性设置默认值
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
5.设置对象头信息
Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
6.执行init方法进行初始化
从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

对象的访问定位
Java程序会通过栈上的reference数据来操作堆上的具体对象。对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

使用句柄
如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,局部变量中reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

直接指针
如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问。

标记复制
标记整理缺点:移动内存地址 GC 将存活对象内存地址
HotSpot 直接指针访问

直接指针访问 与句柄池?
句柄池—代理 实际需要访问两次效率可能不是很高
直接指针访问—直接指向堆内存实际内存地址,效率是非常高
但是如果移动我们存活的对象,内存地址发生了变化时
需要修改栈帧中对应局部变量表引用到内存地址。

垃圾收集器

System.gc()方法
在默认情况下,System.gc()会显示直接触发Full GC,同时对老年代和新生代进行回收。而一般情况下,垃圾回收应该是自动进行的,无需手工触发。
不建议手动调用 System.gc() 因为容易导致系统stw问题。
并行与并发概念
并发的概念
1.在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
2.并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换。由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
并行的概念

1.当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)
2.其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行

垃圾回收并行与串行

垃圾回收器并行与串行区别
垃圾回收器 经典七款垃圾收集器 比如 CMS、并行收集器 串行收集器等。
垃圾回收算法: 标记清除算法 标记复制算法 标记整理算法; 清理堆内存垃圾
可达分析算法:判断该对象是否为垃圾对象 引用计数法(java中没有使用)
GcRoot算法

垃圾回收器 依赖于垃圾回收算法 依赖于可达分析算法
服务器—多核 单核 配置 内存/cpu核数
垃圾回收器 并行(多核)、串行(单核服务器)、并发(GC与用户线程一起运行)垃圾收集器
并行(多核)或者 并发收集器 G1

1.并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
如ParNew、Parallel Scavenge、Parallel Old
2.串行(Serial)
相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程)

并行收集器(parallel)?与并发收集器(concurrent)区别
GC线程清理垃圾对象时,用户线程暂停。

串行收集器:只会开启一个GC线程清理堆内存垃圾,如果堆内存空间
非常大(3-4gb垃圾对象时),一个GC线清理堆内存垃圾 忙不过来最终
可能会导致用户线程暂停的时间会非常长。
并行收集器:会开启多个GC线程同时清理堆内存垃圾(还是会暂停我们用户线程)
降低用户线程暂停的时间。
并发收集器:GC线程和用户线程交替运行

并行收集器(parallel)?:多条垃圾收集线程同时进行工作,此时用户线程处于等待状态
并发收集器(concurrent):指多条垃圾收集线程与用户线程同时进行(但不一定是并行的,有可能交替进行工作)
如cms(老年代)/G1(整堆)就是典型的并发收集器

评估 GC 的性能指标

Jvm在生产环境的情况下 到底如何实现参数调优?
核心点是什么?
1.降低我们GC回收的频率;
2.降低我们用户线程暂停的时间
GCRoot引用链—
配置垃圾收集器 GC 的性能指标

1.吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
2.垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
3.暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
4.收集频率:相对于应用程序的执行,收集操作发生的频率。
5.内存占用:Java堆区所占的内存大小。
6.快速:一个对象从诞生到被回收所经历的时间。
7.吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
核心:
评估 GC 的性能指标 核心:(吞吐量和暂停用户线程时间指标使用非常多的。)

运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
举例子:
如果gc回收时间 越短 意味着 gc回收的频率越高。
如果gc回收时间 越长 意味着 gc回收的频率越低。

如果gc回收时间 越长(用户线程暂停的时间越长)
,意味着 gc回收的频率越低。
某位用户运气不好—100毫秒—需要阻塞100毫秒

如果gc回收时间 越短(用户线程暂停的时间越短)
意味着 gc回收的频率越。
某位用户运气不好—50毫秒—需要阻塞50毫秒

配置并行收集器中
1.吞吐量优先 gc回收频率就比较低;
2.用户线程暂停时间 优先 gc回收频率就比较高

参数调优 配置 CMS /并行收集器/串行收集器/并发收集器

GC回收指标—ZGC /G1

吞吐量(throughput)
1.吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
2.比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的

暂停时间(pause time)

1.“暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态。
例如,GC期间10毫秒的暂停时间意味着在这10毫秒期间内没有应用程序线程是活动的
2.暂停时间优先,意味着尽可能让单次STW的时间最短:50ms*10=0.5s,但是总的GC时间可能会长
暂停时间越短,但是GC回收频率越高

吞吐量 vs 暂停时间区别

1.高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。

2.低暂停时间(低延迟)较好,是从最终用户的角度来看,不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。因此,具有较低的暂停时间是非常重要的,特别是对于一个交互式应用程序(就是和用户交互比较多的场景)。

3.不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。

4.因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
5.相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。
在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。
5.现在标准:在最大吞吐量优先的情况下,降低停顿时间

垃圾收集器发展历史

1999----流行串行收集器(单线程收集器)SerialGC
----多核cpu
2002—并行收集器(开启多个GC线程)ParallelGC
2012–推出G1收集器(挣堆收集器) JDK9 默认使用
2018-至今zgc、G1优化
2020-3删除 老年代GC收集器CMS

非常频繁推出很多垃圾收集器 不断做优化呢
分布式和微服务—服务器8G 16G 服务器多核
降低用户线程暂停的时间比较短

7款经典的垃圾收集器
串行回收器:Serial、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1
CMS—老年代
G1–整堆收集器
垃圾收集器如何组合—JDK9

经典的垃圾收集器关系

新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial old、Parallel old、CMS;
整堆收集器:G1(JD9默认)

垃圾收集器组合关系
1.Serial/Serial old备注:新生代和老年代都使用串行(单线程回收垃圾对象)
2.Serial/CMS (JDK9废弃)
3.ParNew/Serial Old (JDK9废弃) ParNew( Par Parallel 简写的)new 新生代中
4.ParNew/CMS
5.Parallel Scavenge/Serial Old (可能被废弃)
新生代中使用并行收集器、老年代中使用串行收集器
6.Parallel Scavenge/Parallel Old(JDK8默认)
新生代和老年代中都是并行收集器(开启了多个线程)
7.G1(JDK9默认)
核心点:堆内存 新生代(哪些收集器)、老年代(哪些收集器)
查看默认垃圾收集器

Jdk8 使用是并行收集器
默认组合:

Jdk9 默认使用G1收集器

-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag UseParallelGC 进程ID 相关垃圾回收器参数
jinfo -flag UseParallelOldGC

JDK8 默认使用的 并行收集器:-XX:+UseParallelGC

JDK9 默认使用G1收集器:

串行垃圾收集器Serial、Serial old
串行垃圾收集器Serial----

串行垃圾收集器Serial----只有一个GC线程清理堆内存垃圾,
也会暂停用户线程。如果堆内存非常大的,GC清理垃圾
时间非常长,最终导致我们用户线程暂停时间也会非常长。
串行垃圾收集器—
新生代配置Parallel 并行收集器 老年代中 Serial old

新生代回收频率非常高 刚new对象都是存放在新生代–
多个GC线程同时清理堆内存垃圾
老年代中 —多个线程清理老年代

1.Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
2.Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。
3.Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。
3.Serial Old是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:
3.1与新生代的Parallel Scavenge配合使用
3.2作为老年代CMS收集器的后备垃圾收集方案
这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。

串行收集器优缺点
1.为了消除或减少工作线程因内存回收而导致的停顿,HotSpot虚拟机开发团队在JDK 1.3之后的Java发展历程中研发出了各种其他的优秀收集器,这些将在稍后介绍。但是这些收集器的诞生并不意味着Serial收集器已经“老而无用”,实际上到现在为止,它依然是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。
2.它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。
3.在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不频繁发生,这点停顿时间可以接收。
4.所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
等价于新生代用Serial GC,且老年代用Serial Old GC

老的服务器—单核 或者堆内存50-100mb左右
桌面应用程序 c/s
使用串行收集器
-XX:+PrintCommandLineFlags -XX:+UseSerialGC

/*** 代码演示  使用串行收集器* -XX:+PrintCommandLineFlags  -XX:+UseSerialGC** @param args*/
public static void main(String[] args) throws InterruptedException {Thread.sleep(3000000);
}

jinfo -flag UseSerialGC 22032
ParNew 回收器:并行回收

ParNew 应用场景 是在我们新生代中ParNew
ParNew ?Par(并行) new(新生代)

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。 Par是Parallel的缩写,New:只能处理新生代GC
1.ParNew (新生代) ParNew收集器在年轻代中同样也是采用复制算法、
"Stop-the-World"机制
2.对于新生代,回收次数频繁,使用并行方式高效。
3.新生代使用并行回收(多个GC线程 标记复制算法)、Serial old 串行(标记整理算法)回收老年代

1.ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器,具体内容将在稍后进行介绍。
2.ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。
3.在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。
总结:
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

注意:使用ParNew最好使用到多核cpu下,如果在单核cpu情况下使用 ParNew收集器
效率比一定比Serial高。

什么情况下使用 串行收集器?并行收集器呢?
单核cpu下 配置 并行收集器 新生代ParNew–
单核cpu----串行收集器
多核cpu —推荐并行收集器

设置 ParNew 垃圾收集器

1.在程序中,开发人员可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
2.-XX:ParallelGCThreads限制线程数量,默认开启和CPU核数相同的线程数。

/*** 代码演示  使用并行收集器*  -XX:+PrintCommandLineFlags  -XX:+UseParNewGC* @param args*/
public static void main(String[] args) throws InterruptedException {Thread.sleep(30000000);
}

JDK9环境下设置: -XX:+UseParNewGC报错:

Parallel 回收器:吞吐量优先-- 用户暂停优先
CMS—50分钟 安全点 全局点

周六----g1—talb

下周二 zgc
字节码—
压测----比如:cpu核数4核、堆内存 8gb
访问接口测试 1000次;
g1 吞吐量优先—99% 用户线程
并行收集器测试 92% 用户线程
Jdk11 zgc g1
Zgc jdk11 cms测试—
配置降低gc回收频率 最大堆内存与初始化内存一致
Cms 暂停用户线程 10毫秒
Jdk g1 暂停用户线程5毫秒

Parallel 回收器:吞吐量优先
1.HotSpot的年轻代中除了拥有ParNew(新生代)收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收;
2.Parallel 与ParNew(新生代)收集器区别:
2.1Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
2.2自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。(动态调整内存分配情况,以达到一个最优的吞吐量或低延迟)
2.3高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务
3.Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器,Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。
JDK8 默认的Parallel收集器。

Parallel 回收器:吞吐量优先
3.HotSpot的年轻代中除了拥有ParNew(新生代)收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收;
4.Parallel 与ParNew(新生代)收集器区别:
2.1Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

2.2自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。(动态调整内存分配情况,以达到一个最优的吞吐量或低延迟)
2.3高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务
3.Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器,Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。
JDK8 默认的Parallel收集器。
jinfo -flag UseParallelOldGC 22032

Parallel Scavenge 回收器参数设置

jdk8默认使用的就是 Parallel 并行 收集器
新生代和老年代都是多个GC线程回收。
-XX:+UseParallelGC -----XX:+UseParallelOldGC
ParallelGC----新生代 Paralleold 老年代
1.-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
2.-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。
3.分别适用于新生代和老年代
4.新生代使用UseParallelGC 老年代UseParallelOldGC
5.核心参数:
-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
6.在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]
7.-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
为了尽可能地把停顿时间控制在XX:MaxGCPauseMillis 以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
该参数使用需谨慎。
8.-XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。5% GC 5
取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。
与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,STW暂停时间越长,Radio参数就容易超过设定的比例。
9.-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。

CMS收集器
CMS收集器概述(低延迟)
CMS收集器 串行、并行收集器到底有哪些区别

串行、并行收集器–垃圾回收线程 在清理垃圾时
暂停所有的用户线程 stw 时间非常长

GcRoot 引用链 越长 意味着 gc回收线程的时间越长
导致用户线程暂停的时间越长。

CMS 并发收集器 用户线程与GC线程同时或者是
交替运行。降低stw 时间问题。
CMS 初始化标记节点 暂停我们用户的所有线程。
1.GC线程只标记我们的GCRoot起始点(直接引用到对象)。 当我们的gc
标记GCRoot(直接引用到对象) 恢复用户的线程。
GC线程与用户线程同时运行
5.GC线程 单独根据 GCRoot起始点扫描整个链中的 间接
引用对象。
串行、并行收集器 扫描整个GCRoot引用链时,暂停用户
所有线程
CMS收集器 只扫描我们的 GCRoot起始点 暂停用户线程
间接引用的对象—GC线程与用户线程同时运行。

CMS 并发收集器(用户线程与GC线程同时运行)
老年代
采用标记-清除算法 CMS 并发收集器标记-清除算法而不是标记整理算法?

清除-并发清除 标记整理算法 移动对象 (堆内存连续性) 内存地址
JDK9开始比较推荐使用G1收集器

1.在JDK1.5时期,Hotspot推出了一款在强交互应用中(就是和用户打交道的引用)几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。(并发收集器)
2.CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
3.目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
4.CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"
5.不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作(因为实现的框架不一样,没办法兼容使用),所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
6.JDK9开始同时不推荐使用CMS收集器、JDK14将CMS收集器删除

CMS收集器原理
1.CMS(老年代)并发收集器原理
GC与用户线程同时运行 实际上也会发生暂停用户线程 用户线程暂停时间
变的更短

1.初始标记阶段;
GC回收垃圾时 根据GCRoot引用链扫描对象 整个链都扫描
用户暂停的时间 更长
1.初始标记阶段:GC线程 只会扫描到 GCRoot直接引用到对象
Mayikt1、user1对象。–GCRoot 起始点
暂停我们所有的用户线程。
2.GC回收线程 扫描Mayikt1(1毫秒)0、user1对象 (1)
预计暂停用户线程2毫秒时间
3.当我们GC线程扫描完所有的 GCRoot直接引用到对象
时开始唤醒所有的用户线程。
用户线程与GC线程同时运行。
2.并发标记阶段;
用户线程与GC线程同时运行。

GC线程开始根据GCRoot起始点扫描间接引用到对象
User2 user 3 user4 并发标记阶段 mayikt1对象
并发标记阶段 存在哪些问题呢
因为在并发标记阶段 用户线程与GC线程同时运行
1.可能用户线程改变 GCRoot中 间接引用到对象关系
2.如果发生了新垃圾对象 可能无法被我们cms 清理。
该GCRoot 没有在第一阶段扫描。----浮动垃圾

3.重新标记阶段(第三阶段)(修正);
修正?—已经在第一阶段扫描的GcRoot起始点,
在并发标记阶段中,用户线程与GC线程同时运行
用户线程改变 第一阶段扫描的GcRoot起始点 间接引用对象
的关系。
暂停所有的用户stw问题
重新标记阶段(修正)时间与初始标记(比较短)暂停用户线程时间相比
重新标记阶段(修正) 与重新标记阶段(修正) 暂停用户线程
时间更加长。

4.并发清除阶段;
用户与GC线程同时运行的?
并发清除 标记清除算法?标记整理算法
并发清除标记清除算法 不是标记整理算法?

用户与GC线程同时运行的,GC使用并发清除算法 清理堆内存垃圾

为什么并发清除阶段并发清除算法呢?
是因为并发清除阶段用户与GC线程同时运行的 同时运行,
只能使用并发清除算法,不需要移动堆内存地址,采用空闲
列表。

CMS/并行收集器

CMS 发生几次?Stw问题? 2次
第一次初始标记
重新修正

重新修正 暂停用户线程的时间比初始标记暂停用户线程
时间 长。
CMS/并行收集器

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即
1.初始标记阶段;
2.并发标记阶段;
3.重新标记阶段;
4.并发清除阶段;
(涉及STW的阶段主要是:初始标记 和 重新标记)

1.初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
2.并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
3.重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World”的发生,但也远比并发标记阶段的时间短。
4.并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。(标记清除算法)由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
周志明老师 JVM第三本书籍
CMS收集器细节分析
1.尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是尽可能地缩短暂停时间。
2.由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作线程,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS 清理垃圾时 并发清除时 用户线程与GC线程同时运行
3.CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
CMS 收集器 在什么时候触发回收垃圾呢?

1.其他收集器 堆内存空间 不足时 触发 GC回收。
2.CMS 收集器 清理垃圾时 提前根据堆阈值使用(70) 提前清理堆内存垃圾
并发收集器 用户线程与GC线程同时运行
CMS 优缺点

优点:
1.并发标记(用户线程与GC线程同时运行)
2.低延迟;(暂停用户线程时间变得更加短)
缺点:
1.会产生碎片化的问题(并发清除 GC和用户线程同时执行 标记清除算法)
2.CMS收集器对CPU资源非常敏感 在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。上下文
CMS 四个阶段:
初始阶段 暂停所有用户线程
并发阶段 唤醒所有用户线程
修正阶段 暂停所有用户线程
并发清除阶段 唤醒所有用户线程

3.CMS收集器无法处理浮动垃圾,并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记(第一阶段没有扫描到该GCRoot),最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS(老年代) —并行收集器(parnew)
CMS 参数设置

1.-XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。
2.-XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。 JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
4.-XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。CMS 并发清除-

4.-XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理。
5.-XX:ParallelCMSThreads:设置CMS的线程数量。
CMS默认启动的线程数是 (ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行收集器的线程数,可以当做是 CPU 最大支持的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
新生代 parnew 收集器----cms
相关参数测试:
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -Xms100m -Xmx100m
相关代码:
byte[] bytes = new byte[90 * 102481024];

CMS 收集器版本优化

1.JDK9中开始不推荐使用CMS收集器;使用g1(整堆)
2.JDK14 删除CMS垃圾回收器(JEP363)移除了CMS垃圾收集器
JDK开始官方推荐学习 G1收集器
CMS—
CMS收集器常见面试题
CMS回收是在什么时候触发

提前回收 根据 用户设置 使用堆内存阈值

为什么 CMS 不采用标记-压缩算法而是标记清除算法?

因为在并发清除时,用户线程和GC线程是同时在执行 没有发生stw问题 而使用标记整理算法时,会发生移动对象,对象引用内存地址,所以不能够使用标记整理算法而是标记清除算法(不会移动对象)
CMS 收集器如何处理浮动垃圾问题?
GC与用户线程同时运行 并发标记、并发清除

什么是浮动垃圾:CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

1.可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生
2.放到下一次GC清理。

CMS回收停顿用户线程了几次,为什么要停顿用户线程两次?

两次:
1.初始标记;
2.重新修正;
重新修正暂停用户线程的时间比 初始标记初始标记 长
为什么配置了CMS GC收集器,却触发了Full GC?

1.大对象分配时,年轻代放不下,直接去老年代,结果老年代也放不下。
2.CMS GC失败(concurrent mode failure导致)
3.老年代碎片化的问题严重—标记整理算法
G1收集器

在线模拟JVM垃圾回收动画效率图

Github: https://github.com/visualizit/vpoc

G1收集器的引入

1.CMS收集器回收 老年代 需要 配合parnew 组合回收堆内存;
2.CMS采用标记清除算法回收容易发生大量碎片化问题,
如果碎片化问题严重,可能会导致触发full gc 采用标记整理算法。

G1收集器
什么是G1
G1收集器(Garbage-First Garbage Collector) 整堆收集。
G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为默认GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。
1.因为G1是一个并行/并发回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、幸存者0(S0)区,幸存者(S1)1区,老年代等。
2.由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First) 。
3.G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
4.G1收集器使用的是 整体上使用标记整理 两个Region 之间 标记复制算法。
G1收集器发展历程
1.2004发表:G1的第一篇paper
2.2012年才在jdk1.7u4中可用
3.2017 年jdk9中将G1变成默认的垃圾收集器以替代CMS。
G1收集器分区划分
1.使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。2048MB 每个独立区间1
2.可以通过-XXG1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
3.虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。通过Region的动态分配方式实现逻辑_上的连续。
一个region(分区)只能属于一个角色,有可能为eden区、S区、老年代等, E表示为Eden区、S区表示为S1,S0区,老年代O区 空白的表示为未使用的分配的内存。
H区存放巨型对象

为什么G1收集器需要设计巨型对象

  1. 在G1收集器中也有一个新的内存区域,称作为:Humongous (H)区(巨型对象),主要存放一些比较大的对象,一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。
    2.在以前收集器中,如果是一个大对象是直接放入到老年代中,而触发老年代GC不是很频繁,万一该大对象不是非常频繁的使用,则会非常浪费我们堆内存,为了解决这个问题在G1收集器专门弄一个H区存放巨型对象,如果一个H区装不下的情况下,则会寻找连续H区存储,如果还没有足够的空间,有可能会引发FULLGC.
    G1收集器参数设置

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
G1收集器回收的细节

G1收集器回收过程分为三个环节:
1.年轻代GC(Young GC )
2.新生代和并发标记过程( Concurrent Marking
3.混合回收(Mixed GC )----
4.Full GC
young gc(新生代) 一> young gc + concurrent mark(新生代+并发标) 一> Mixed GC(混合回收)顺序,进行垃圾回收。

1.应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程; G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

2.当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。—CMS

3.标记完成马上开始混合回收过程。对于一个混合回收期, G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就称为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
G1收集器跨代引用
G1收集器Rset问题(记忆集)

1.概念:
在垃圾收集过程中,会存在一种现象,即跨代引用,在G1中,又叫跨Region引用。如果是年轻代指向老年代的引用我们不用关心,因为即使Minor GC把年轻代的对象清理掉了,程序依然能正常运行,而且随着引用链的断掉,无法被标记到的老年代对象会被后续的Major GC回收
2.如果是老年代指向年轻代的引用,那这个引用在Minor GC阶段是不能被回收掉的,那如何解决这个问题呢?
3.最合理的实现方式自然是记录哪些Region中的老年代的对象有指向年轻代的引用。GC时扫描这些Region就行了。这就是RSet存在的意义。RSet本质上是一种哈希表,Key是Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。

4.现代JVM,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。

5.对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。

G1两种回收策略

G1 Young GC(新生代GC)

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

G1 Mix GC(混合回收)

G1收集器优缺点
1.并行与并发
并行:G1收集器在回收时,可以实现多个GC线程同时执行 利用CPU多核利用率,但是会让用户线程暂停 触发stw机制。 堆6个GB JDK8环境开启G1 jdk9默认使用
并发:多个GC与用户线程用时执行,用户线程不会阻塞。
2.分代收集原理
G1收集器,也会分为新生代、老年代, 新生代eden、S0或者S1区域,但是不要求整个eden、S0或者S1区域具有连续性。
与之前的收集器不同,它可以收集新生代也可以收集老年代。
3.空间整合
之前我们所学习的CMS收集器采用标记清除算法,容易产生碎片化的问题且空间不连续性,而G1收集器划分成n多个不同的采用标记压缩算法,没有产生碎片化的问题。分配大对象的时候,避免FullGC的问题,所以如果堆内存空间比较大,使用G1收集器更加有优势。
4.可预测的停顿时间模型 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
由于G1收集器采用分区模型,所以G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
缺点:
小内存的情况下使用cms收集器,大内存的情况下可以使用G1收集器。G1收集器6GB以上
G1收集器核心配置参数
JDK9已经默认开启了G1收集器,如果在JDK8开启G1收集器。需要配置
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB 到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 也就是G1收集器最小堆内存应该是2GB以上,最大堆内存64GB
-XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标 ,默认值是200ms
-XX:ParallelGCThread 设置垃圾回收线程数 最大设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:+UseG1GC 设置开启G1收集器
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -verbose:gc

4.jvm入门到精通相关推荐

  1. JVM从入门到精通(尚硅谷宋红康)

    不动笔墨不读书,先把书读厚,再把书读薄是我学习方式. 所以等理解了再整理一次笔记,目前笔记和视频一一对应. 笔记连载中 <尚硅谷2020最新版宋红康JVM> 第1章:JVM与Java体系结 ...

  2. java从入门到精通_想要开始学java?你要的java从入门到精通布列如下!

    java从入门到精通,让我来告诉你! 毫无疑问,java是当下最火的编程语言之一.对于许多未曾涉足计算机编程的领域「小白」来说,深入地掌握java看似是一件十分困难的事.其实,只要掌握了科学的学习方法 ...

  3. MAT入门到精通(二)

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 阅读本文大概需要6分钟. 上一篇文章MAT入门到精通(一)介绍了MAT的使用场景和基本概念,这篇文 ...

  4. 内存泄露从入门到精通三部曲之常见原因与用户实践

    2019独角兽企业重金招聘Python工程师标准>>> 内存泄露从入门到精通三部曲之常见原因与用户实践 腾讯Bugly特约作者: 姚潮生 常见原因 1.集合类 集合类如果仅仅有添加元 ...

  5. Java学习从入门到精通-旧版

    为什么80%的码农都做不了架构师?>>>    Java学习从入门到精通-旧版 http://tech.ccidnet.com/art/3737/20051017/465333_1. ...

  6. spark从入门到精通spark内存管理详解- 堆内堆外内存管理

    前言 Spark作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解Spark内存管理的基本原理,有助于更好地开发Spark应用程序和进行性能调优.本文将详细介绍两部 ...

  7. java书籍_学习Java最好的10本书,从入门到精通

    在当代,学习Java等编程课程的主要方式是视频资源,如果你想学,在网上五分钟之内就可以找到一堆学习视频,瞬间将你的硬盘填满.但是这些课程质量良莠不齐,对于小白来说很难辨别好坏. 但是书籍不同,书籍都是 ...

  8. Java学习从入门到精通

    Java Learning Path (一).工具篇 一. JDK (Java Development Kit) JDK是整个Java的核心,包括了Java运行环境(Java Runtime Envi ...

  9. SpringBoot入门到精通_第5篇 _SpringBoot Actuator监控

    接上一篇:SpringBoot入门到精通_第4篇 _开发三板斧 https://blog.csdn.net/weixin_40816738/article/details/101097161 文章目录 ...

  10. @aspect注解类不生效_Spring Boot从入门到精通(三)常用注解含义及用法分析总结...

    Spring Boot是目前非常流行的框架,而注解是Spring Boot的核心功能,接下来主要说一说Spring Boot常用注解的含义以及部分注解在项目中的使用方法. @RestControlle ...

最新文章

  1. while read line 用法详细介绍
  2. 一次kvm嵌套虚拟化踩坑经历
  3. 程序猿提升自己水平的方法
  4. SecureRandom生成随机数
  5. SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“.
  6. C语言Dialogbox添加图片,c# dll c 类_dialogboxparam_msg结构
  7. 《那些年啊,那些事——一个程序员的奋斗史》九
  8. sonarqube8.9的pdf插件安装
  9. faker 无敌了,专注于制作假数据
  10. windows系统统不支持mysql_Windows系统下MySQL无法启动的万能解决方法
  11. slack 开源系统_Slack团队聊天的5种开源替代品
  12. Commvault发布横向扩展一体机 矛头对准Rubrik和Cohesity
  13. Windows同一局域网如何文件共享
  14. Python快速入门(八)面向对象1:类、对象和封装
  15. 51单片机 播放青花瓷(源码)
  16. 全系列极路由刷不死uboot(breed)教程
  17. 把三层交换机当成普通交换机来用
  18. android 音量按键,Android 音量键的监听
  19. 香港汇丰取消个人账户最低存款要求及月费
  20. 数字化门店转型| 舞蹈室管理系统| 门店小程序开发教程

热门文章

  1. 【c++入门(2)】完全背包
  2. APP微信小程序测试一览表(常规用例)
  3. oracle优化之driving_site
  4. MATLAB零基础入门教程视频课程
  5. 爬取搜狗微信文章笔记2
  6. php初级程序员的自我评价,程序员的自我评价【程序员的简历自我评价】
  7. centos系统加入windows域
  8. 计算机日志查询域用户登录记录,Windows域控制器身份验证登录日志记录和取证...
  9. 【gflags】【gflags实践】【gflags的学习使用记录】
  10. DevOps案例研究|中华有为-解构华为软开云DevOps实践