学习笔记-JVM原理

本文深入讲解JVM的工作原理,涵盖类加载机制、内存管理、垃圾回收算法及调优技巧等内容,帮助读者理解JVM如何高效运行Java应用。

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

目录

 

一、JVM

JVM总述

运行数据区域

方法区域 MethodArea(元空间)

 程序计数器 Program Counter Register:

 虚拟机栈 VM Stack:

堆(Heap)

本地方法栈

补充,javap命令

 二、类的加载

1、类加载步骤

补充:思考下面加载关系

2、类加载器层级结构

3、自定义类加载器

4、双亲委派

双亲委派模型优缺点

5、打破双亲委派模式

6、Tomcat打破双亲委派机制

三、对象创建与内存分配

1、分配内存

3、设置对象头

四、JVM调优

1、JVM参数

五、垃圾收集器

1、垃圾收集算法

标记复制算法

标记-清楚算法

标记-整理算法

三色标记法

​编辑

记忆集与卡表(remeberSet)

2、常用垃圾收集器

CMS(老年代收集器)

G1收集器

ZGC收集器(-XX:+UseZGC)

JVM调优

调优案例:

问题


一、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是写前屏障

CMSG1ZGC
写屏障+增量更新写屏障+SATB读屏障

为什么G1用SATB?CMS使用增量更新?

记忆集与卡表(remeberSet)

记忆集remeberSet:跨代引用,在新生代中有个内存空间放老年的引用。扫描的时候一起扫

2、常用垃圾收集器

4G 以下4-8G8G以上几百G以上
parallelParNew+CMSG1ZGC
JDK8推荐JDK9JDK11以上

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垃圾回收参数优化 - 知乎

大内存高并发的场景很适合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内存泄漏 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值