JVM优化

本文详细介绍了Java编码的执行流程,包括JVM加载过程、JIT编译、类装载器的工作原理、懒加载机制、双亲委派原则以及JVM内存结构,重点讲解了JDK、JRE、JVM实现、类装载器加载策略和G1垃圾回收器的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java编码执行流程图

在这里插入图片描述
JVM加载模式

默认混合模式
-Xint 设置成纯解释模式(一句句解释)
-Xcomp 设置成纯编译模式(Java代码编译成机器码放到内存中)

JVM加载过程

a.java
->javac(前端编译器,javac属于其中一种)
->a.class
和java类库
->classloader->
Java解释器(一行行解释并运行)
或即时编译器JIT(Just In Time,属于后端编译器)

JIT可以将一个方法(热点代码,多次被调用的方法)中所有的字节码编译成机器码,放在缓存中,然后需要的时候拿出来直接用		   
判断热点代码的方式是:热点探测
			   
JDK:jre+development kit
JRE:jvm+core lib
JVM

JVM实现

hotspot1.8以后收费,开源版openJDK
jrockit,被oracle收购,合并于hotspot
taobaoJVM

class文件

magic文件类型    minor version小版本	major version主版本
ca fe ba be     20 20               20 34
constant_pool_count常量池个数
20 10  
constant_pool为constant_pool_count-1的表
access_flags 类的修饰符
this_class 当前类指向常量池
super_class 父类指向常量池

javap或者jclasslib插件

javap -v命令或jclasslib插件用于反汇编 Java 字节码并显示更详细信息的命令,它可以显示与指定类相关的字节码指令、常量池、方法、字段和其他类信息

general_information通用信息,大版本,小版本,常量池个数,this classsuper class等
constant_pool:格式为常量池个数-1
	void () --> <init>()V  表示没有返回值的构造方法
	String toString() --> ()Ljava/lang/String  表示toString()方法返回值为String
methods:包含一些属性name(方法名)、descriptor、access flags(修饰符)
	code子属性:具体的方法实现,例如this-->aload_0,object()-->invokespecial<java/lang/Object.<init>> return
	aload_0是一个java指令,一共有256个指令

类装载器把一个类装入JVM步骤

类加载过程(虚拟机将描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型)

1,加载loading:查找和导入Class文件
2,链接linking:把类的二进制数据合并到JRE中
2.1,校验verification:检查载入Class文件数据的正确性,比如cafe babe检查加载到的文件格式
2.2,准备preparation:给类的静态成员变量分配存储空间,赋默认值
2.3,解析resolution:将方法、类、属性等符号符号引用转成直接引用;常量池中将各自符号引用解析为指针、偏移量等内存地址的直接引用
3,初始化init:对类的静态成员变量,静态代码块static {},执行初始化操作,静态成员变量赋值为初始值

3,初始化init示例
class C {
    public static int j = 8;//先执行C.c=9,因为init阶段会执行静态成员变量,在构造方法中+1
    public static C c = new C();
    //public static int j = 8;//后执行C.c=8,因为因为init阶段会执行静态成员变量,j初始化时复制为8
    private C(){
        j++;
    }
}
3,初始化init示例,对类的静态成员变量,静态代码块static {},执行初始化操作,静态成员变量赋值为初始值
public static void main(String[] args) {
    C c;
    //P p = new P();//new对象时会加载自己对象和父类对象,所以会执行各自static块
    System.out.println(C.i);
    //System.out.println(C.j);
}
public static class C{
    final static int i = 9;//打印final值时不需要加载类
    static int j = 8;//打印非final时需要加载类
    static {//加载类时执行static{}
        System.out.println("static{C}");
    }
}
public static class P extends C{
    static {
        System.out.println("static{P}");
    }
}

JVM懒加载loading

JVM没有规定什么时候加载,一般是什么时候使用这个class才会什么时候加载,但是JVM规定了什么时候必须初始化(装载、链接、初始化),只要加载之后,那么肯定是要进行初始化的

new getstatic putstatic invokestatic指令,访问final变量除外
java.lang.reflect对类进行反射调用时
初始化子类的时候,父类首先初始化
虚拟机启动时,被执行的主类必须初始化
动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

类加载器

类加载器remark
Bootstrap类加载器将<java_home>/lib目录下核心类库rt.jar charset.jar等加载到内存,c++实现,没有父类
Extention类加载器父类为null,加载<java_home>/lib/ext/目录下扩展jar包类库
Appliation类加载器父类为Extention类加载器,加载classpath指定内容
Custom类加载器父类为Appliation类加载器,自定义类加载器继承ClassLoader类重写findClass()方法,内部调用loadClass()方法

CustomClassLoader ->Appliation ->Extention -> Bootstrap 子加载器到父加载器

双亲委派机制(class加载过程)

双亲委派机制详解
自底向上检查该类是否已经被加载到自身类加载器cache中(每种类加载器都有一个list维护着已经加载到哪些类),如果都没有,自顶向下进行判断该类是否应该自己加载如果是就加载,如果不是再委派子加载器加载。
请添加图片描述
请添加图片描述

双亲委派机制好处

这种设计有个好处是,如果有人想替换系统级别的类java.lang.String,篡改它的实现,在这种机制下这些系统的类已经被BootstrapClassLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
双亲委派机制,其实指的是由子到父,再由父到子的过程。
主要解决的是类加载的安全问题(比如java.lang.String,如果没有双亲委派,直接由自定义ClassLoader加载了,有了双亲委派会由子到父检查是否已加载这个类,如果没有,再从父到子分别去加载)
优先级,父加载器优先于子加载器
避免重复加载,父加载器加载了的话,子加载器就没有必要再次加载

JVM内存

JVM内存:remark
虚拟机栈每个线程有一个私有的栈,随着线程的创建而创建。每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等
本地方法栈JVM运行Native方法(c/c++)准备的空间
PC寄存器记录当前程序执行到哪一步
存放所有的对象和数组,是JVM所有线程共享的部分,这部分空间可通过GC进行回收,当申请不到空间时会抛出OutOfMemoryError。分为新生代(伊甸园、幸存者区1、幸存者区2)占1/3堆空间、老年代占2/3堆空间
方法区所有线程共享。用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆区分,又叫“非堆”。JDK1.8之前方法区的实现是永久代PermGen(堆内存中),JDK1.8之后分方法区的实现是元空间(不属于堆内存,在本地存储)

方法区?

元空间和永久代,都是JVM规范中方法区的实现

方法区(永久代)方法区(元空间)堆内存
类的信息(类以及超类全限定名、类型标志类或接口、访问修饰符、方法名)类的信息(类以及超类全限定名、类型标志类或接口、访问修饰符、方法名)
方法信息(方法名称、参数列表、返回值)方法信息(方法名称、参数列表、返回值)
字段信息字段信息
code信息(方法执行的字节码指令)code信息(方法执行的字节码指令)
JIT编译之后的代码JIT编译之后的代码
运行时常量池包括:全局唯一的字符串常量池(双引号引起来的字符串值)、一个class类对象一个运行时常量池(final修饰的变量、非final修饰的变量比如long,double,float、引用类型到引用地址)运行时常量池包括:全局唯一的字符串常量池(双引号引起来的字符串值)、一个class类对象一个运行时常量池(final修饰的变量、非final修饰的变量比如long,double,float、引用类型到引用地址)
静态变量(类变量)静态变量(类变量)

永久代、元空间?

元空间和永久代概念?
元空间和永久代类似,都是JVM规范中方法区的实现
永久代JVM虚拟机中一块内存空间,可以设置大小(-XX:PermSize),但是是有上限的,在内存不够时会触发FullGC,也就是和老年代同时垃圾回收
元空间不属于JVM内存,而是使用本地内存,默认是可以无限制使用本地内存,也可以通过参数限制内存使用大小
元空间为什么代替了永久代?
1,运行时常量池存在永久代中,容易出现性能问题和内存溢出。
2,类及方法的元信息比较难确定其大小,永久代大小指定比较困难,太小容易出现永久代溢出,太大容易造成老年代溢出
3,永久代通过FullGC,也就是和老年代同时垃圾回收;替换成元空间之后,简化了垃圾回收,可以在不暂停情况下,并发进行垃圾回收
4,Oracle要合并Hotspot(被Oracle收购)和JRockit代码,JRockit里没有永久代

string声明的字面量数据都放在字符串常量池中
jdk1.6中字符串常量池存放在方法区(永久代中)
jdk1.7及以后字符串常量池存放在堆空间

字符串常量池 intern()

s1.intern();
intern()方法会从字符串常量池中查询s1字符串是否存在,若不存在就会将s1引用地址放入字符串常量池中,若存在就直接返回引用地址。

//创建3个new String,字符串常量池创建1,2
String s1 = new String("1") + new String("2");
//查找字符串常量池有没有12,没有,将对象s1的引用地址保存在字符串常量池
s1.intern();
String s2 ="12";//查找字符串常量池12引用地址,也就是s1的引用地址
System.out.println(s1==s2);//true

//创建一个new String,字符串常量池创建123
String s3 = new String("123");
//查找字符串常量池有没有123,有,返回123的引用地址
s3.intern();
String s4 ="123";//查找字符串常量池123返回地址,也就是123的引用地址
System.out.println(s3==s4);//false

//创建一个String对象,指向1234
String s5 = String.valueOf(1234);
//查找字符串常量池有没有1234,没有,将s5的引用地址保存在字符串常量池并返回
String intern = s5.intern();
String s6 ="1234";//查找字符串常量池1234引用地址,也就是1234的引用地址
System.out.println(s5==s6);//true
System.out.println(s5==intern);//true

堆栈?

栈:方法调用(自动释放)、变量名
堆:new出的对象
在这里插入图片描述

为什么不把基本数据类型放在堆中?

1,栈比堆运算效率快,但是堆空间比栈空间大
2,将复杂的数据类型放在堆中目的是不影响栈运行效率,通过引用的方式去堆中找。
3,基本数据类型占用内存少,放在栈空间中,能够提高效率。

GC垃圾回收

垃圾回收算法特点
mark-sweep标记清除内存碎片
copying拷贝浪费空间
mark-compact标记整理浪费时间

在这里插入图片描述

GC Root有哪些?

1,虚拟机栈中引用的对象
2,方法区中静态属性引用的对象
3,方法区中常量引用的对象
4,本地方法栈中(即一般说的native方法)引用的对象

内存模型

分代模型:
分代模型比例GC算法次数
新生代1/3YGC年轻代垃圾回收coping算法在新生代中年龄增长(PS+PO:15次,CMS:6次)放到老年代中
老年代2/3Full GC老年代垃圾回收mark-sweep标记清除、mark-compact标记整理

新生代

新生代比例
eden伊甸区8/10
survivor1幸存者区11/10
survivor2幸存者区21/10
新生代采用coping算法,
比如先给eden区new10个对象,然后回收9个对象,将剩余的1个放到survivor,这时eden区又为空
然后在eden区new10个对象,再回收时,回收eden区和第一个survivor区,将不需要回收的对象放在第二个survivor区

老年代
随着在新生代中年龄增长(PS+PO:15次,CMS:6次)放到老年代中
Full GC老年代垃圾回收(Full Garbage Collection):当老年代的空间占用达到一定阈值时,JVM 可能会触发 Full GC 来对整个堆(包括新生代和老年代)进行回收。Full GC 会扫描整个堆内存,包括老年代,以释放未被使用的对象占用的内存。
CMS GC(Concurrent Mark-Sweep Garbage Collection):在使用 CMS 垃圾收集器时,老年代的回收通常是通过 CMS 的并发标记清除算法来实现的。这种方式允许在应用程序运行的同时进行老年代的回收操作,以减少停顿时间。
G1 GC(Garbage-First Garbage Collection):G1 垃圾收集器通过划分堆内存为多个区域来管理内存,其中包括老年代。G1 GC 会根据堆内存的使用情况动态地选择哪些区域进行垃圾回收,而不是简单地依赖于年龄或者固定的老年代回收阈值。
在这里插入图片描述

serial单线程+serial old垃圾回收组合

serial:a stop-the-world,copying collector which use a single GC thread
serial old:a stop-the-world,mark-sweep-compact collector what use a single GC thread

内存几十兆时:
serial单线程stw(stop the world)垃圾回收
serial采用copying拷贝算法,
serial old采用mark-sweep标记清除、mark-compact标记压缩

parallel并行 scavenge+parallel old垃圾回收组合

parallel scavenge: a stop-the-world,copying collector which use a multiple GC thread
parallel old: a stop-the-world,mark-sweep-compact collector that use a multiple GC thread
吞吐量大
Java1.8默认PS+PO组合

parallel并行(其实就是多线程同时垃圾回收)
内存几百兆~1G时:
并行多线程
PS+PO组合
JDK1.8默认垃圾回收线程
在需要垃圾回收时,多个线程同时干活
但是不能无限增加垃圾回收线程数量,因为线程切换需要花费时间

CMS+ParNew垃圾回收组合

响应时间短

CMS:concurrent-mark-sweep并发标记清除
内存:20G
Concurrent GC 并发垃圾回收
并发:指GC线程和业务线程同时进行
CMS适用于老年代

三色标记算法

在这里插入图片描述

黑色标记对象A:对象A和子属性都已完成标记
灰色标记对象B:对象B已完成标记,但是子属性没有完成
白色标记对象D:对象D和子属性都没有完成标记

incremental update增量更新

A的子属性是BB的子属性是D时,如果BD之间断开关联,AD之间增加关联时,
需要将黑色标记对象A,改成灰色。这称为CMS解决方案incremental update增量更新

incremental update增量更新bug

但是CMS存在bug,
当垃圾回收线程t1标记完成对象A、子属性A1并且正在标记子属性A2时,
切换至业务线程t2,t2给对象A将子属性A1指向白色对象D,
切换至垃圾回收线程t3,把对象A标记为灰色
切换回垃圾回收线程t1继续标记完子属性A2时,就会把A标记成黑色。
导致对象D漏标
需要在重新标记RemarkSTW,浪费时间

G1

响应时间短
Java1.9默认G1

内存:上百G
垃圾处理时间号称200ms
G1:garbage first首先回收垃圾特别多的区域
是一个老年代和新生代共用的垃圾回收器
G1采用局部收集的回收思想。将Java堆内存划分成多个相同大小的独立region区域。
JVM内存分成不同区域region,物理上不分代,但是分区,逻辑上分代
G1分region回收,优先回收花费时间少,垃圾比例高的region
新老年代比例一般不用手动指定

G1也采用三色标记,但是将incremental update增量更新升级为SATB(snopshot at the begining)

SATB(snopshot at the begining)

SATB(snopshot at the begining)
在起始时做一个快照,当B->D消失时,要把这个引用推到GC的堆栈,保证D还能被GC扫描到,
配合Rset(remember set记忆集,region分区有10~15%区域用于记录哪些分区有引用当前分区),
只用扫描哪些region引用到了D这个region,如果全部不指向D,那么D就是垃圾。
如果有别的对象指向DD就保留下来

JVM 优化经验总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值