目录
程序计数器 Program Counter Register:
一、JVM
JVM总述
1、类加载子系统
2、运行时数据区
3、字节码执行引擎
具体工作流程,
1、java文件编译成class文件
2、通过类加载器加载到方法区。
3、线程调用方法的时候,会创建一个栈帧,读取方法区的字节码执行指令,执行指令的时候,会把执行的位置记录在程序计数器中,如果创建对象,会在堆内存中创建,方法执行完,这个栈帧就会出栈。
运行数据区域
可以划分为:虚拟机栈、程序计数寄存器、本地方法栈、Java堆、方法区域(运行常量池)
JVM内存空间详细介绍_文晓武的博客-优快云博客_jvm内存
方法区域 MethodArea(元空间)
1、方法区域是一个JVM实例中的所有线程共享的,当启动一个JVM实例时,方法区域被创建。
2、它用于存放运行常量池、有关域和方法的信息、静态变量、类和方法的字节码。不同的JVM实现方式在实现方法区域的时候会有所区别。
Oracle的HotSpot称之为永久区域(Permanent Area)或者永久代(Permanent Generation)。
程序计数器 Program Counter Register:
1、虚拟机栈也是每个线程单独拥有,线程启动时创建。这个计数器存放当前正在被执行的字节码指令(JVM指令)的地址。
2、每次由字节码引擎修改。
虚拟机栈 VM Stack:
1、虚拟机栈也是每个线程单独拥有,线程启动时创建。
2、这个栈中存放着一系列的栈帧(Stack Frame),JVM只能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,可以调用printStackTrace等方法来查看栈的情况。栈的示意图如下:
堆(Heap)
堆中存放的是程序创建的对象或者实例。这个区域对JVM的性能影响很大。垃圾回收机制处理的正是这一块内存区域。Java堆中还可以细分为:新生代和老年代;新生代还细分为Eden空间、From Survivor空间、To Survivor空间。
本地方法栈
当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就根据本地方法的语言类型建立相应的栈。
补充,javap命令
javap -n XXX.class 可以看到代码的指令码
二、类的加载
1、类加载步骤
加载-验证-准备-解析-初始化
加载-Loading:在硬盘上查找并通过IO读如字节码文件,使用到才会加载,例如main(),后者new等等,会在内存中生成一个类对象,作为方法区这个类的各种数据的访问入口。
验证-Verifying:检查载入的类文件是否符合Java规范和虚拟机规范。
准备-Preparing:为这个类分配所需要的内存,确定这个类的属性、方法等所需的数据结构。(类的静态变量,并赋予默认值)(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)
解析-Resolving:将该类常量池中的符号引用都改变为直接引用。这个就是静态链接过程(动态链接是指运行期间将符号引用转间接引用的过程)
初始化-Initialing:初始化类的局部变量,为静态域赋值,同时执行静态初始化块。
补充:思考下面加载关系
类的静态代码块、new的对象会加载,如果有父类,会先调用父类的静态代码
调用构造方法是,一定会先走父类默认构造方法。
package com.example.demo.test;
/**
* @author 10450
* @description 类加载调用关系
* @date 2022/8/23 14:27
*/
public class MyLoader{
static {
System.out.println("MyLoader类静态");
}
public static void main(String[] args) {
MyNULL myNULL = null;
MyChild child = new MyChild("你好");
}
}
class MyChild extends MyParent {
static {
System.out.println("MyChild类静态");
}
MyChild(){
System.out.println("MyChild默认构造方法");
}
MyChild(String name){
System.out.println("MyChild带参数构造方法");
}
}
class MyParent {
static {
System.out.println("MyParent类静态");
}
MyParent(){
System.out.println("MyParent默认构造方法");
}
}
class MyNULL{
static {
System.out.println("MyNULL类静态");
}
MyNULL(){
System.out.println("MyNULL默认构造方法");
}
}
2、类加载器层级结构
C++语言会调用一个类:Launcher.class ,会初始化ExtClassLoader和 AppClassLoader
注意,他们不是父类子类关系,而是父类加载器和子类加载器关系!
3、自定义类加载器
代码包含,自定义类加载器、打破双亲机制
package com.example.demo.test;
import sun.misc.Launcher;
import sun.misc.MetaIndex;
import sun.misc.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
/**
* @author 10450
* @description 自定义类加载
* @date 2022/8/24 15:24
*/
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath){
this.classPath=classPath;
}
private byte[] loadByte(String name)throws Exception{
name = name.replaceAll("\\.","/");
FileInputStream in = new FileInputStream(classPath+"/"+name+".class");
int len = in.available();
byte[] data = new byte[len];
in.read(data);
in.close();
return data;
}
/**
* 自定义类加载,重写findClass
* @param name
* @return
* @throws ClassNotFoundException
*/
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
byte[] data= loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 打破双亲委派机制,
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
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);
long t0 = System.nanoTime();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if(name.startsWith("com.example.demo.test")){
c = findClass(name);
}else{
c = this.getParent().loadClass(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;
}
}
}
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("D:/");
Class clazz = myClassLoader.loadClass("com.example.demo.test.MySout");
Object object = clazz.newInstance();
Method method = clazz.getDeclaredMethod("printin",null);
method.invoke(object,null);
System.out.println(clazz.getClassLoader().getClass().getName());
// MyClassLoader myClassLoader1 = new MyClassLoader("E:/");
// Class clazz1 = myClassLoader1.loadClass("com.example.demo.test.MySout");
// Object object1 = clazz1.newInstance();
// Method method1 = clazz1.getDeclaredMethod("printin",null);
// method1.invoke(object1,null);
// System.out.println(clazz1.getClassLoader().getClass().getName());
}
}
4、双亲委派
源码:Launcher.class loadClass | findClass
双亲委派模型优缺点
优点:
1、避免重复加载的。
2、沙箱安全机制:sercurityException Prohibited package name :自己重写JDK中同名类是不可能会加载
缺点:
加载过程比较长,顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类
5、打破双亲委派模式
即:重写loadClass,不要向上加载,自己没有就自己findClass
注意,自定义的加载器用到核心的包,沙箱安全机制不允许,只能向上,需要做判断
6、Tomcat打破双亲委派机制
例如,tomcat 部署两个项目,A项目是spring3 / B项目是spring4
Tomcat双亲是加载公共jar包,会有每个war的加载器。不公用
三、对象创建与内存分配
.class文件进入JVM有个加载过程。
分配内存-----》初始化-----》设置对象头-----》init方法(真正的赋值,调用构造方法)
1、分配内存
1、逃逸分析:即一个对象只在栈帧中使用(只在当前方法用),那放在线程栈中。JDK1.7后默认开启
2、大对象,不同垃圾收集器,JVM参数不一样。
对象是否被回收
一般是可达性分析算法:将GC Roots对象作为起点,从这些节点开会时向下搜索引用的对象,找到对象标记为非垃圾对象,其余为垃圾对象。
GC Roots 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等
2、设置对象头
Klass Pointer 类型指针,C++写的
分代年龄 4bit 即0-15所以最大年龄代是15
默认对象的指针压缩
四、JVM调优
JVM调优的本质是减少STW的时间,不管是减少minor GC 还是 full GC(主要)
在发生GC时,会停止用户的线程,用户会感知卡顿,所以要减少发生GC。
JVM调优发生在堆中。
例如,需要新建多少对象,一个对象多少字段,估算下多少M,再放大下。
配比:eden : s0:s1 = 8:1:1
1、JVM参数
参考某个测试环境配置:
java -jar
-XX:MetaspaceSize=4096m
-XX:MaxMetaspaceSize=4096m
-Xms4096m 初始堆大小
-Xmx4096m 最大堆大小
-Xmn1024m 年轻代大小
-Xss256k
-XX:SurvivorRatio=8 Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
#禁用显示的调用”System.gc()”
-XX:+DisableExplicitGC
#开启jmx远程监控
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.port=3000
-Dcom.sun.management.jmxremote.authenticate=false
-XX:+UseConcMarkSweepGC 开启CMS收集器的方式
-XX:+CMSParallelRemarkEnabled 在重新标记的时候多线程执行,降低STW
-XX:CMSlnitiatingOccupanyFraction=70 这两个参数配套使用,
-XX:+UseCMSInitiatingOccupancyOnly 指示只有在 old generation 在使用了初始化的比例后concurrent collector 启动收集
-XX:LargePageSizeInBytes=128m 指定Java heap的分页页面大小
-XX:+UseFastAccessorMethods get,set 方法转成本地代码(对于jvm来说是冗余代码,jvm将进行优化)
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
方法区元空间:直接内存
-XX:MetaspaceSize:最小元空间 默认21M会自动调整 推荐设置
-XX:MaxMetaspaceSize:最大元空间 默认21M会自动调整 推荐设置
-Xms:最小堆内存
-Xmx:最大堆内存 不要超过32G,指针压缩会失效,统一用64位存储,太浪费
-Xmn:新生代大小
-Xss:栈大小 一个线程的栈大小,默认是1M 推荐设置
-XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:为3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5
-XX:MaxTenuringThreshold 不设置,默认会自动调整,最大年龄代是15,
但是动态计算公式:(可以看下源码):
-XX:PretenureSizeThreshold=1M
-XX:+UseConcMarkSweepGC:设置并发收集器
Java堆的代际结构你怎么理解
新生代——Java 程序创建的所有新对象都放入堆的新生代部分。
垃圾收集在新一代上运行并删除所有短期对象。
新一代部分进一步分为两个部分。伊甸园空间和幸存者空间。
伊甸园空间- 所有新对象都被放入伊甸园空间。当伊甸园空间填满时,垃圾收集运行并删除所有没有引用的对象。所有仍然有引用的对象都被提升到幸存者空间。
幸存者空间- 如果不再引用对象,则在新生部分运行的每个垃圾收集都会从幸存者空间中删除对象。如果该对象仍被引用,它将增加该对象的年龄。在增量达到一定数量后,通常从 15 开始,具体取决于 JVM 实现,该对象将被提升到老年代部分。
老一代- 在新生代部分的幸存者部分幸存下来的对象被提升到老一代部分。老一代部分比新一代大得多。一个单独的垃圾收集过程,也称为 FullGC,发生在老年代部分。
PermGen - JVM 使用 PermGen 来存储关于类的元数据
2、内存分配
1、指针碰撞
2、空闲列表
3、如果并发时候,CAS+重试机制 或者TLAP
五、垃圾收集器
常见的垃圾收集器有串行垃圾回收器(Serial)、并行垃圾回收器(Parallel)、并发清除回收器(CMS)、G1回收器
1、垃圾收集算法
分代收集理论:不同代不通算法
标记复制算法
年轻代会用,浪费空间
标记-清楚算法
标记-整理算法
三色标记法
增量更新是写后屏障
SATB是写前屏障
CMS | G1 | ZGC |
写屏障+增量更新 | 写屏障+SATB | 读屏障 |
为什么G1用SATB?CMS使用增量更新?
记忆集与卡表(remeberSet)
记忆集remeberSet:跨代引用,在新生代中有个内存空间放老年的引用。扫描的时候一起扫
2、常用垃圾收集器
4G 以下 | 4-8G | 8G以上 | 几百G以上 |
parallel | ParNew+CMS | G1 | ZGC |
JDK8推荐 | JDK9 | JDK11以上 |
JDK8默认用Parallel 和 parallet Old作为新生代和老年代收集器
缺点是,并行STW
CMS(老年代收集器)
CMS: Concurrent Mark Sweep 真正并发,标记清楚算法
问题:
1、为什么要分这么阶段,与parallel区别?尤其是大内存,STW时间会比较久,适合CMS
因为减少STW停顿时间,并发标记耗时比较长,CMS并发标记并不停止用户线程
2、标记清楚算法,会产生大量垃圾碎片怎么办?
有个参数可以设置,进行整理UseCMSCompactAtFullCollection 与CMSFullGCsBeforeCompaction
3、什么是并发标记失败?
会整个并发STW,使用Sefial Old 单线程专心收集。也可以用参数调整
CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中,
第一种条件,UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。 CMSFullGCsBeforeCompaction代表次数,几次full GC 后做碎片整理。
第二种条件是用户调用了System.gc(),而且DisableExplicitGC没有开启。
第三种条件是young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
2. -XX:CMSInitiatingOccupancyFraction=70 和-XX:+UseCMSInitiatingOccupancyOnly
这两个设置一般配合使用,一般用于『降低CMS GC频率或者增加频率、减少GC时长』的需求
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.
3. -XX:+CMSScavengeBeforeRemark
在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销-----一般CMS的GC耗时 80%都在remark阶段
G1收集器
JDK9推荐使用,针对大内存机器,是用复制算法。
老年代、年轻代没有物理隔阂,切分2048(默认)格子。
默认年轻代占比5%,不能超过60%
Humongous,超过Region(格子)50%,认为是大对象,放入Humongous中
筛选回收:
可以设置最大停顿时间(默认200ms最好不要动),做部分回收。通过优先选择回收代价最大的Region,进行排序回收。
G1的参数几乎不要调优。
大内存高并发的场景很适合G1:
可以通过停顿时间,改为100ms,将eden区调到10G-30G
ZGC收集器(-XX:+UseZGC)
JDK11以上Linux,JDK14 windows
支持TB级内存,停顿时间到10ms~~~牛逼
未来的GC基础~~~
五、JVM调优
1、常用命令
jmap
jinfo
jstat:
jstack
GC日志
2、案例一
1、第一步先找出Java进程ID
ps -ef | grep java 获取pid
2、ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid
第二步找出该进程内最耗费CPU的线程
例如: top -Hp pid
3、将线程pid转十六进制
printf
"%x\n"
21742 得到
54ee
4、打出堆栈信息
jstack 21711 | grep 54ee
root@ubuntu:/# jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]
就可以查到对应代码
3、案例二:
jmap用来查看堆内存使用状况,一般结合jhat使用。
1)打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息
jmap -permstat pid
2)查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况。比如下面的例子:
jmap -heap pid
root@ubuntu:/# jmap -heap 21711
Attaching to process ID 21711, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.10-b01
using thread-local object allocation.
Parallel GC with 4 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2067791872 (1972.0MB)
NewSize = 1310720 (1.25MB)
MaxNewSize = 17592186044415 MB
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 21757952 (20.75MB)
MaxPermSize = 85983232 (82.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 6422528 (6.125MB)
used = 5445552 (5.1932830810546875MB)
free = 976976 (0.9317169189453125MB)
84.78829520089286% used
From Space:
capacity = 131072 (0.125MB)
used = 98304 (0.09375MB)
free = 32768 (0.03125MB)
75.0% used
To Space:
capacity = 131072 (0.125MB)
used = 0 (0.0MB)
free = 131072 (0.125MB)
0.0% used
PS Old Generation
capacity = 35258368 (33.625MB)
used = 4119544 (3.9287033081054688MB)
free = 31138824 (29.69629669189453MB)
11.683876009235595% used
PS Perm Generation
capacity = 52428800 (50.0MB)
used = 26075168 (24.867218017578125MB)
free = 26353632 (25.132781982421875MB)
49.73443603515625% used
....
3、使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图,如果带上live则只统计活对象,如下:
调优案例:
垃圾收集器知识点总结-MaxTenuringThreshold_六道木_的博客-优快云博客_maxtenuringthreshold最大值
问题:
1、JVM调优的本质是减少SWT的时间。
2、能否堆JVM调优,让其几乎不发生Full GC?
可以。
3、OOM内存泄漏