一、类的加载
1.1 Java类可以从哪些地方加载
- 本地直接加载
- 网络下载.class文件
- 从zip,jar等归档文件加载
- 从转悠数据库中提取.class文件
- 动态编译
- 从加密文件中获取
1.2 加载类的过程
装载->链接->初始化
链接:验证,准备,解析
1.3 类加载器与双亲委派
1.3.1 Java中的四种类加载器:
-
Bootstrap ClassLoader(启动类加载器):负责加载JVM核心类库(如java.lang包下的类)和其他基础的API,是所有类加载器的祖先类,由JVM自身实现,因此不需要程序员自己实现。
-
Extension ClassLoader(扩展类加载器):负责加载Java的扩展库,如javax等,位于sun.misc.Launcher$ExtClassLoader类中,是由Bootstrap ClassLoader加载的。可以通过系统属性java.ext.dirs设置扩展库的目录。
-
System ClassLoader(系统类加载器):负责加载应用程序classpath下的所有类,是开发者自己编写的代码所需要的类的加载器。系统类加载器可以通过ClassLoader.getSystemClassLoader()方法获得,也可以通过设置系统属性java.class.path来指定。
-
Custom ClassLoader(自定义类加载器):根据自身需要自定义的类加载器。tomcat,jboss会自行实现ClassLoader
1.3.2 双亲委派:
给定一个类的全路径名,从4->1,首先看是否加载过了,加载过了则直接返回;如果到1仍没有被加载,则从1到4依此去尝试加载。如果都没加载,则报错。
1.4 Java的Class字节码
字节码是通过javac编译Java文件生成的字节码文件,通常后缀名是.class。这个文件可以放入Java虚拟机中执行。
字节码由以下模块组成:
1.4.1 魔数
Java 字节码文件的头四个字节称为魔数,它用于识别文件格式,保证文件的正确性和安全性。魔数的值为固定的 0xCAFEBABE。
1.4.2 文件版本
紧接着魔数的四个字节是版本信息,它包含了两个字节的主版本号和两个字节的次版本号,用于标识字节码文件所针对的 Java 运行时环境版本。
1.4.3 常量池
常量池是一个表,它包含了编译器生成的各种字面量和符号引用,例如类和接口名、字段名和方法名、字符串常量等。常量池中的每一项都是一个常量项(Constant),常量项包含了常量的类型和值,常量池中的第一个常量项的下标从 1 开始,而不是从 0 开始。
1.4.4 访问标志
访问标志是一个 16 位的二进制数,它用于描述当前类或接口的访问控制符,例如 public、private、final、abstract 等。
1.4.5 类/父类索引与接口索引集合
类索引、父类索引和接口索引集合(Class Indexes、Superclass Indexes and Interface Index Collections):类索引用于确定当前类在常量池中的位置,父类索引用于确定当前类的父类在常量池中的位置,而接口索引集合用于确定当前类实现的接口在常量池中的位置。
1.4.6 字段表集合
字段表集合用于描述当前类或接口中定义的字段,包括字段的访问标志、名称、描述符等信息。
1.4.7 方法表集合
方法表集合用于描述当前类或接口中定义的方法,包括方法的访问标志、名称、描述符、异常表等信息。
1.4.8 属性表
Java字节码的属性表集合包含了以下内容:
- Code 属性:
包含了方法的字节码指令和异常处理器的信息。 - Constant Value 属性:
包含了一个常量的值,这个常量通常是类、接口、字段或方法的静态 final 属性。 - Deprecated 属性:
标记了一个已经被弃用的类、接口、字段或方法。 - Exceptions 属性:
列出了一个方法可能会抛出的异常。 - Inner Classes 属性:
描述了一个类的内部类。 - Line Number Table 属性:
包含了源代码中的行号和字节码指令之间的对应关系。 - Local Variable Table 属性:
包含了方法中所有局部变量的名称、类型和作用域信息。 - Source File 属性:
包含了源文件的名称。 - Synthetic 属性:
标记了一个类、接口、字段或方法是否是由编译器自动生成的。
二、垃圾回收
2.1 垃圾回收的介绍
垃圾回收Garbage Collector,又称GC,是托管语言的一个重要组成部分。当一个new出来的对象没有被任何一个对象引用时,该对象就被称为垃圾,需要被回收。
2.2 垃圾的定位
目前,定位垃圾主要有两种:
1-)引用计数
在对象中存放引用计数。每当被一个其他对象引用,引用计数就+1。当断开引用时,引用就-1。当引用数为0时,则释放该对象。
在cocos2dx的对象和python中,就是使用的这个垃圾定位方法。
这个方法有好处是可以及时的回收垃圾,并且算法简单,避免了垃圾回收需要各线程停顿的问题(用强一些的算法也可以规避)。
但这种方法也有一个很致命的缺点,就是无法处理循环引用的情况。A对象中有B对象的指针,B对象中有C对象的指针,C对象中又有A对象的指针,但没有任何对象和他们3个关联,就会造成内存泄露。所以这种垃圾回收器的语言,要尽量使用树状的引用结构。
2-)根可达算法
从一些程序中最基础的根出发,逐个扫描各对象。如果扫描到了,则不需要被释放,否则应被释放。
Java和lua就是用的这个算法来标识需要释放的对象。
这种算法可以判断所有的无用对象,但有一个问题,当对象的数量很多时,每扫描一次的耗时会非常长。
2.3 垃圾的回收
垃圾被标记后,如何回收呢?主流的做法有这3种。
1-) Mark-Sweep(标记清除)
Mark-Sweep 算法是一种基础的垃圾回收算法。该算法主要分为两个阶段:标记(Mark)和清除(Sweep)。标记阶段遍历整个对象图,将存活对象进行标记,清除阶段回收未标记的对象。该算法的优点是简单易懂,缺点是回收后的空间存在不连续性,可能会产生内存碎片,导致内存利用率降低。
2-)Copying(拷贝)
Copying 算法是一种基于空间换时间的算法。该算法将内存分为两个区域,每次只使用其中一个区域,当这个区域满了之后,将存活对象复制到另一个区域,然后清空当前使用的区域。该算法的优点是可以避免内存碎片,缺点是需要使用两倍的内存空间,故而难以处理大对象。
3-) Mark-Compact(标记压缩)
Mark-Compact分3个阶段。标记阶段:从根对象开始遍历整个对象图,将所有存活对象标记。整理阶段:遍历堆空间,将存活对象移动到堆的一端,未标记的对象移动到堆的另一端。更新引用:更新所有指向移动后的存活对象的指针。
该算法优点是能够在保证没有内存碎片的情况下对存活对象进行整理,减少了堆空间的浪费。但整理过程需要移动存活对象,可能会导致较长的停顿时间。
2.4 分代模型
上图是JDK所使用的各种垃圾收集器。G1左边都是分代模型,适用于内存没那么大的虚拟机。JDK1.8使用的是Parallel Scavenge和Parallel Old。JDK11用的是G1。本小节我们主要讲JDK1.8的分代模型算法。
new出来的对象(不那么大),首先会进入伊甸区。当进行一次垃圾回收时,会把伊甸区幸存的对象拷贝到某个survivor区。在下一次垃圾回收时,JVM会把那时的伊甸区和刚才的survivor区的对象遍历,把幸存的对象统统拷贝到另一个survivor区。当某个对象总是幸存,数量达到8时,将会被拷贝到老年代的tenured区。
当然,伊甸区还有些细节的优化,比如会为每个线程分配一个小空间,该线程创建的小对象优先放到自己的专属空间。这样做可以避免线程的竞争。
2.5 放入栈中的Java对象
上图展示了Java对象的生命周期。小伙伴们会发现,这里还有一个栈的概念。当一个对象在某个方法中被new出来,如果不是很大的话,该对象是会放入栈中,当方法结束后,该对象会直接弹出。
是否能被放入栈上,要经过2个分析。一个是逃逸分析,一个是标量替换。能满足这两个条件的Java对象才会被放入栈上。
2.6 三色标记算法
对于老年代的垃圾回收,JVM通常使用三色标记算法。改算法主要为了处理多个线程并发处理垃圾回收时,所面对的一系列问题。
三色标记法(Tricolor Marking)是一种标记清除(Mark and Sweep)垃圾回收算法中的一种算法。
在三色标记法中,每个对象都被标记为白色、灰色或黑色。
- 白色表示对象还未被垃圾回收器扫描,也就是还没有被访问过。
- 灰色表示对象已经被扫描过,但是其引用的其他对象还未被扫描。
- 黑色表示对象已经被扫描过,并且其引用的其他对象也已经被扫描过。
垃圾回收器从根对象开始,对所有可达对象进行扫描,可达对象被标记为灰色或黑色。扫描完成后,所有白色对象就可以被回收了。因为所有黑色和灰色对象都是相互可达的,所以它们构成了一个可达图。而所有白色对象就是这个可达图之外的对象。
三色标记法的优点是可以并发执行垃圾回收操作,因为对象的颜色可以在回收过程中动态修改。同时,三色标记法相比其他标记清除算法更加稳定,不容易漏掉未被回收的对象。
三色标记法的缺点是需要保证垃圾回收器对于可达对象的扫描是完整的,如果有对象的引用没有被扫描到,那么这个对象可能会被错误地回收掉,或者造成内存泄漏。因此,需要采用一些技术手段来保证可达性分析的完整性,比如屏障技术等。
2.7 屏障技术
屏障技术是一种在垃圾回收过程中保证可达性分析完整性的技术。具体来说,屏障是在对象引用修改的时候插入的一段代码,用来标记这个引用发生了变化,从而能够对应用的可达性进行分析。
屏障技术分为写屏障和读屏障。
写屏障:当一个对象引用发生变化时,垃圾回收器会在这个引用被修改的时候插入一段代码,这段代码会将对象的颜色从灰色变为黑色,或者将黑色对象的引用标记为灰色。
读屏障:当读取一个对象的引用时,垃圾回收器会插入一段代码,这段代码用来确保被读取的引用所指向的对象仍然存活。如果这个对象被回收了,垃圾回收器会立即抛出一个异常或者进行一些其他的处理。
通过使用屏障技术,垃圾回收器可以在对象引用发生变化的时候保证可达性分析的完整性。但是屏障技术也会带来一定的性能损失,因为每次对象引用的读取和修改都需要插入额外的代码。因此,在实现垃圾回收器时需要权衡性能和可达性分析的完整性。
2.8 G1
G1 是一种面向服务器的垃圾回收器,它使用了分代垃圾回收的思想,同时将堆空间分成多个大小相等的区域(Region),每个区域可以是 Eden 区、Survivor 区或者 Old 区。G1 回收器将回收任务分解成多个小任务,并行地执行,能够在较短的时间内回收大量垃圾对象。
优点:与传统的垃圾回收器相比,G1 能够更好地控制停顿时间,并且能够对内存使用情况进行预测,从而避免了由于内存不足而导致的长时间停顿。
缺点:在垃圾回收过程中会产生大量的内存碎片。
还有个点要说下,G1这个垃圾回收器的目标就是尽量不让程序员做JVM调优,就用默认的就行。我好想在真实项目中用JDK11呀~~~
2.9 ZGC和结语
其实到G1的算法,已经很不好理解了。JDK11默认使用G1。但到JDK15以后,以及SpringBoot3.0的JDK17,则使用的更令人匪夷所思的ZGC,有着令人恐怖的低延时。他是单一代,并发压缩。也就是说,如果跟上时代使用新技术,我之前讲的东西都是没有用的知识。。。
现在Java面试官之所以喜欢问这些问题,真的是在务实的为公司招人么。
三、JVM相关的命令与内存查看
3.1 工程准备与JVM设置
我们这次实验使用芝法酱躺平系列的bailan5工程,就用test这个微服务做测试
修改工程中bin目录下的startup.sh,JVM相关设置如下:
JAVA_OPT="${JAVA_OPT} -Xms512m -Xmx512m -Xss128k -XX:MaxGCPauseMillis=300 -Xlog:gc:../logs/gc.log"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -jar ${BASE_DIR}/target/${SERVER_JAR}"
JAVA_OPT="${JAVA_OPT} --spring.config.additional-location=${CUSTOM_SEARCH_LOCATIONS}"
JAVA_OPT="${JAVA_OPT} --logging.config=${BASE_DIR}/config/logback.xml"
JAVA_OPT="${JAVA_OPT} --server.max-http-header-size=524288"
我们环境用的JDK11,使用的是G1垃圾回收。G1垃圾回收的目的就是让程序员可以尽可以少的去修改JVM,用默认的就好。
我这里仅仅做了几个简单的修改:
首先让最大堆Xms和最小堆Xmx大小都是512M,以免扩容拷贝所带来的性能损失。
其次,虚拟机世界线暂停允许的时间长一些,300毫秒。如果不是做高精度的设备监控,抢票,抢购。其实大多数需求是可以容忍一定的世界线时停的。把这个设高些反倒有利于减少GC次数。
最后改的是线程栈的大小Xss,一般说来,只有在方法体中new的对象(或声明的变量)才会存放在栈中,对象中的对象引用也仅仅是指针,占不了太多空间。这个值默认是1M,完全用不了那么多,改小点可以增加线程吞吐量。
3.2 JVM常用命令
3.2.1 JPS
jps可以用于查看java程序的进程id,在wsl中实验如下:
hataksumo@localhost:~/DOCUMENTS/app/test/bin$ jps
5479 test-1.0.0.jar
7673 Jps
1178 nacos-server.jar
还有个linux的命令,也很常用,在这里说下,就是:
netstate -tulpn
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:6006 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:6004 0.0.0.0:* LISTEN -
这个命令可以查看各端口的占用情况
3.2.2 jinfo
jinfo用于查看JVM的状态
例如:
jinfo 5479 #进程id
3.2.3 jstat
用于查看jvm的内存及GC状态
jstat -gcutil 5479 1000 10
S0:survivor space 0 utilization as a percentage of the space’s current. 幸存者0
s1:survivor space 1 utilization as a percentage of the space’s current. 幸存者1
E: Eden space utilization as a percentage of the space’s current capacity. 伊甸园区
M: Metaspace utilization as a percentage of the space’s current capacity. 元空间
在G1垃圾回收器中,元空间(Metaspace)是指存储类元数据的区域,包括类的名称、方法信息、字段信息等。在Java 8及以前的版本中,元数据存储在永久代(PermGen)中,但是由于PermGen空间容易发生内存泄漏和溢出,所以在Java 8中将元数据存储在了元空间中,通过动态分配内存的方式进行管理。
与PermGen相比,元空间的一个显著特点是:它是在Java堆之外的,因此它不会受到堆大小的限制。它的大小是由-XX:MaxMetaspaceSize参数控制的。另外,元空间的回收和垃圾回收器是相互独立的,因此元空间中的内存会随着类的卸载而释放。
CCS:Compressed class space utilization as a percentage. 压缩类空间利用率,百分比。
YGC: Number of young generation GC events. 年青代的GC时间数量
YGCT:Young generation garbage collection time. 年青一代垃圾收时间
FGC:Number of full GC event. 完整的GC时间的数量
FGCT:Full garbage collection time. 完全垃圾收集时间
GCT:Total garbage collection time. 垃圾回收总时间
用下面这个命令,可以看具体数值
jstat -gc 5479 1000 10
如果把最后一个参数省去,则会不限次数的打印
3.2.4 jstack
用于查看各线程调用的堆栈方法
jstack -| 5479
3.2.5 jmap
用于生成堆的dump文件,供其他软件做分析
比如查看前20个空间占用最大的对象
jmap -histo 5479 | head -n 20
再比如,把所有信息导出到一个文件中
jmap -dump:live,format=b,file=dump.hprof 5479
3.3 第三方辅助工具
实际工作中,我们不可能仅通过JDK提供的那些原始工具做性能分析,更多时候会借助第三方工具。
3.3.1 Arthas
Arthas发音阿尔萨斯,是早年流行的玻璃渣网游知名角色呜喵王的名字。至于他如何使用,可以移步官网先行学习。
首先,下载启动Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
进入Arthas后,可以选择想监控的程序
键入dashboard命令,可以监控当前JVM的统计信息
Arthas还有很多其他命令,大家可以自行在官网上查看
一个比较有用的命令是:
thread --state RUNNABLE
该命令可以显示正在运行的线程的状态:
3.3.1 visualvm
visualvm是一个很方便的虚拟机内存查看工具,可以在官网免费下载
加载从JVM中dump的文件,可以分析其内存使用状况:
点击view all,可以看到我们各个对象的数量和大小,这一定程度上能帮我们定位内存泄露
选择一个对象,点击GC Root,即可发现他的调用栈,帮助我们猜测原因