竹笋

首页 » 问答 » 问答 » 满满的一整篇,全是JVM核心知识点C
TUhjnbcbe - 2023/3/24 20:22:00

头图

CSDN下载自视觉中国

作者

sowhat责编

张文

想要提高程序员自身的内功心法无非就是数据结构跟算法+操作系统+计网+底层,而所有的Java代码都是在JVM上运行的,了解了JVM好处就是:

写出更好更健壮的代码。提高Java的性能,排除问题。面试必问,要对知识有一定对深度。

简述JVM内存模型

从宏观上来说JVM内存区域分为三部分线程共享区域、线程私有区域、直接内存区域。

1.1、线程共享区域

堆区

堆区Heap是JVM中最大的一块内存区域,基本上所有的对象实例都是在堆上分配空间。堆区细分为年轻代和老年代,其中年轻代又分为Eden、S0、S1三个部分,他们默认的比例是8:1:1的大小。

方法区:

在《Java虚拟机规范》中只是规定了有方法区这么个概念跟它的作用。HotSpot在JDK8之前搞了个永久代把这个概念实现了。用来主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。PermGen(永久代)中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且为PermGen分配多大的空间因为存储上述多种数据很难确定大小。因此官方在JDK8剔除移除永久代。官方解释移除永久代:

ThisispartoftheJRockitandHotspotconvergenceeffort.JRockitcustomersdonotneedtoconfigurethepermanentgeneration(sinceJRockitdoesnothaveapermanentgeneration)andareaccustomedtonotconfiguringthepermanentgeneration.即:移除永久代是为融合HotSpotJVM与JRockitVM而做出的努力,因为JRockit没有永久代,不需要配置永久代。元空间:

在Java中用永久代来存储类信息,常量,静态变量等数据不是好办法,因为这样很容易造成内存溢出。同时对永久代的性能调优也很困难,因此在JDK8中把永久代去除了,引入了元空间metaspace,原先的class、field等变量放入到metaspace。

总结:

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。

1.2、直接内存区域

直接内存:

一般使用Native函数操作C++代码来实现直接分配堆外内存,不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。这块内存不受Java堆空间大小的限制,但是受本机总内存大小限制所以也会出现OOM异常。分配空间后避免了在Java堆区跟Native堆中来回复制数据,可以有效提高读写效率,但它的创建、销毁却比普通Buffer慢。

PS:如果使用了NIO,本地内存区域会被频繁的使用,此时jvm内存≈方法区+堆+栈+直接内存

1.3、线程私有区域

程序计数器、虚拟机栈、本地方法栈跟线程的声明周期是一样的。

程序计数器

课堂上比如你正在看小说《诛仙》,看到章节时,老师喊你回答问题,这个时候你肯定要先应付老师的问题,回答完毕后继续接着看,这个时候你可以用书签也可以凭借记忆记住自己在看的位置,通过这样实现继续阅读。

落实到代码运行时候同样道理,程序计数器用于记录当前线程下虚拟机正在执行的字节码的指令地址。它具有如下特性:

线程私有:多线程情况下,在同一时刻所以为了让线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。没有规定OutOfMemoryError:程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。执行Native方法时值为空:Native方法大多是通过C实现,并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。虚拟机栈

方法的出入栈:调用的方法会被打包成栈桢,一个栈桢至少需要包含一个局部变量表、操作数栈、桢数据区、动态链接。

动态链接:

当栈帧内部包含一个指向运行时常量池引用前提下,类加载时候会进行符号引用到直接引用的解析跟链接替换。

局部变量表:

局部变量表是栈帧重要组中部分之一。他主要保存函数的参数以及局部的变量信息。局部变量表中的变量作用域是当前调用的函数。函数调用结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。

操作数栈:保存着Java虚拟机执行过程中数据

方法返回  :执行static代码块进行初始化,如果存在父类,先对父类进行初始化。

使用

类加载完毕后紧接着就是为对象分配内存空间和初始化了:

为对象分配合适大小的内存空间为实例变量赋默认值设置对象的头信息,对象hash码、GC分代年龄、元数据信息等执行构造函数(init)初始化。卸载

最终没啥说等,就是通过GC算法回收对象了。

6.2、对象占据字节

关于对象头问题在Synchronized一文中已经详细写过了,一个对象头包含三部分对象头(MarkWord、classPointer)、实例数据InstanceData、对齐Padding,想看内存详细占用情况IDEA调用jol-core包即可。

问题一:newObject()占多少字节

markword8字节+classpointer4字节(默认用calssPointer压缩)+padding4字节=16字节如果没开启classpointer压缩:markword8字节+classpointer8字节=16字节问题二:User(intid,Stringname)Useru=newUser(1,李四)

markword8字节+开启classPointer压缩后classpointer4字节+instancedataint4字节+开启普通对象指针压缩后String4字节+padding4=24字节

6.3、对象访问方式

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

直接指针:reference中存储的直接就是对象地址。最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

SunHotSpot使用直接指针访问方式进行对象访问的。

对象一定创建在堆上吗

结论:不一定,看对象经过了逃逸分析后发现该变量只是用到方法区时,则JVM会自动优化,在栈上创建该对象。

7.1、逃逸分析

逃逸分析(EscapeAnalysis)简单来讲就是:JavaHotspot虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存。

7.2、标量替换

标量替换:JVM通过逃逸分析确定该对象不会被外部访问。那就通过将该对象标量替换分解在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

标量:不可被进一步分解的量,而JAVA的基本数据类型就是标量

聚合量:在JAVA中对象就是可以被进一步分解的聚合量。

7.3、栈上分配

JVM对象分配在堆中,当对象没有被引用时,依靠GC进行回收内存,如果对象数量较多会给GC带来较大压力,也间接影响了应用的性能。

为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。那就通过将该对象标量替换分解在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

7.4、同步消除

同步消除是java虚拟机提供的一种优化技术。

通过逃逸分析,可以确定一个对象是否会被其他线程进行访问,如果对象没有出现线程逃逸,那该对象的读写就不会存在资源的竞争,不存在资源的竞争,则可以消除对该对象的同步锁。比如方法体内调用StringBuffer。

逃逸分析结论:虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

如果对象经过层层分析后发现无法进行逃逸分析优化则反而耗时了,因此慎用。

类加载器

在连接阶段一般是无法干预的,大部分干预类加载阶段,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,类加载时候重要三个方法:

loadClass():加载目标类的入口,它首先会查找当前ClassLoader以及它的双亲里面是否已经加载了目标类,找到直接返回。findClass():如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。defineClass():拿到这个字节码之后再调用defineClass()方法将字节码转换成Class对象。8.1、双亲委派机制

定义:当某个类加载器需要加载某个.class文件时,首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

作用:

可以防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。保证核心.class不能被篡改,通过委托方式,不会去篡改核心.class。类加载器:

BootstrapClassLoader(启动类加载器):c++编写,加载java核心库java.*,JAVA_HOME/libExtClassLoader(标准扩展类加载器):java编写的加载扩展库,JAVA_HOME/lib/extAppClassLoader(系统类加载器):加载程序所在的目录,如user.dir所在的位置的ClassPathCustomClassLoader(用户自定义类加载器):用户自定义的类加载器,可加载指定路径的class文件8.2、关于加载机制

双亲委派机制只是Java类加载的一种常见模式,还有别的加载机制哦,比如Tomcat总是先尝试去加载某个类,如果找不到再用上一级的加载器,跟双亲加载器顺序正好相反。再比如当使用第三方框架JDBC跟具体实现的时候,反而会引发错误,因为JDK自带的JDBC接口由启动类加载,而第三方实现接口由应用类加载。这样相互之间是不认识的,因此JDK引入了SPI机制,线程上下文加载器来实现加载(跟Dubbo的SPI不一样哦)。

OOM、CPU%

系统性能分析常用指令:

9.1、OOM

为啥OOM?:发生OOM简单来说可总结为两个原因:

分配给JVM的内存不够用。分配内存够用,但代码写的不好,多余的内存没有释放,导致内存不够用。三种类型OOM

堆内存溢出:此种情况最常见Javaheapspace。一般是先通过内存映像工具对Dump出来的堆转储快照,然后辨别到底是内存泄漏还是内存溢出。

内存泄漏

通过工具查看泄漏对象到GCRoots的引用链。找到泄漏的对象是通过怎么样的路径与GCRoots相关联的导致垃圾回收机制无法将其回收,最终比较准确地定位泄漏代码的位置。

不存在泄漏

就是内存中的对象确实必须存活着,那么此时就需要通过虚拟机的堆参数,从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。

虚拟机栈和本地方法栈溢出

在HotSpot虚拟机上不区分虚拟机栈和本地方法栈,因此栈容量只能由**-Xss**参数设定。在Java虚拟机规范中描述了两种异常:

StackOverflowError:线程请求的栈深度超过了虚拟机所允许的最大深度,就会抛出该异常。

OutOfMemoryError:虚拟机在拓展栈的时候无法申请到足够的空间,就会抛出该异常。

单线程环境下无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法继续分配的时候,虚拟机抛出的都是StackOverflowError异常。

多线程环境下为每个线程的栈分配的内存越大,每个线程获得空间大则可建立的线程数减少了反而越容易产生OOM异常,因此,一般通过减少最大堆和减少栈容量来换取更多的线程数量。

永久代溢出:

PermGenspace即方法区溢出了。方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。当前的一些主流框架,如Spring、Hibernate,对于类进行增强的时候都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成Class可以加载入内存,这样的情况下可能会造成方法区的OOM异常。

OOM查看指令

通过命令查看对应的进程号:比如:jps或者ps-ef

grep需要的任务输入命令查看gc情况命令:jstat-gcutil进程号刷新的毫秒数展示的记录数,比如:jstat-gcutil010(查看进程号,每隔1秒获取下,展示10条记录)查看具体占用情况:命令:jmap-histo进程号

more(默认展示到控制台)比如:jmap-histo

more查看具体的classname,是否有开发人员的类,也可以输出到具体文件分析

9.3CPU%

线上应用导致CPU占用%,出现这样问题一般情况下是代码进入了死循环,分析步骤如下:

找出对应服务进程id:用ps-ef

grep运行的服务名字,直接top命令也可以看到各个进程CPU使用情况。查询目标进程下所有线程的运行情况:top-Hppid,-H表示以线程的维度展示,默认以进程维度展示。对目标线程进行10进制到16进制转换:printf‘%x\n’线程pid用jstack进程id

grep16进制线程id找到线程信息,具体分析:jstack进程ID

grep-A进制线程id

GC调优

一般项目加个xms和xmx参数就够了。在没有全面监控、收集性能数据之前,调优就是瞎调。

出现了问题先看自身代码或者参数是否合理,毕竟不是谁都能写JVM底层代码的。一般要减少创建对象的数量,减少使用全局变量和大对象,GC优化是到最后不得已才采用的手段。日常分析GC情况优化代码比优化GC参数要多得多。一般如下情况不用调优的:

minorGC单次耗时50ms,频率10秒以上。说明年轻代OK。FullGC单次耗时1秒,频率10分钟以上,说明年老代OK。GC调优目的:GC时间够少,GC次数够少。

调优建议:

-Xms5m设置JVM初始堆为5M,-Xmx5m设置JVM最大堆为5M。-Xms跟-Xmx值一样时可以避免每次垃圾回收完成后JVM重新分配内存。-Xmn2g:设置年轻代大小为2G,一般默认为整个堆区的1/3~1/4。-Xss每个线程栈空间设置。-XX:SurvivorRatio,设置年轻代中Eden区与Survivor区的比值,默认=8,比值为8:1:1。-XX:+HeapDumpOnOutOfMemoryError当JVM发生OOM时,自动生成DUMP文件。-XX:PretenureSizeThreshold当创建的对象超过指定大小时,直接把对象分配在老年代。-XX:MaxTenuringThreshold设定对象在Survivor区最大年龄阈值,超过阈值转移到老年代,默认15。开启GC日志对性能影响很小且能帮助我们定位问题,-XX:+PrintGCTimeStamps-XX:+PrintGCDetails-Xloggc:gc.log

1
查看完整版本: 满满的一整篇,全是JVM核心知识点C