
浅谈jvm虚拟机
jvm 是Java Virtual Machine(Java虚拟机)的缩写,java 虚拟机作为一种跨平台的软件是作用于操作系统之上的,那么认识并了解它的底层运行逻辑对于java开发人员来说很有必要!
让我们来看看它一次编译,到处运行的牛叉之处!
废话不多说,先看看jvm的架构图(无论何时脑子里要有这样一张图):
总概
从这副架构图可以看出jvm由类装载器、运行时数据区、执行引擎、本地方法接口还有垃圾回收器构成;其中垃圾回收器作用在整个jvm内存中(主要作用在堆和方法区),所以图中没有具体体现,但是我们要知道这么个东西,后面会聊jvm内存调优主要是在调堆
接下来,我们再结合jvm架构图一个个分析下:
「类加载器
了解类加载器就是了解代码编译执行的机制
1.加载:查找并加载类的二进制数据
2.连接:
验证:保证被加载的类的正确性;
准备:给类静态变量分配内存空间,赋值一个默认的初始值;
解析:把类中的符号引用转换为直接引用
把java编译为class文件的时候,虚拟机并不知道所引用的地址;助记符:符号引用转为真正的直接引用,找到对应的直接地址!
3.初始化:给类的静态变量赋值正确的值;
来看下这行代码的执行顺序:
1 | package jvm; |
了解了这个加载执行顺序后,我们再来看看下面这段代码,请你说说程序是如何输出的?
Demo1代码:
1 | package jvm; |
可以看出,子类继承父类 会优先加载父类的 然后静态块是先于方法和静态变量的.那么常量又是怎样的呢?
Demo2代码:
1 | package jvm; |
Demo3代码:
1 | package jvm; |
理解:根据上面的jvm架构图可以知道类信息,常量,静态变量,编译后运行代码都是存在方法区里的,而常量是存在方法区的常量池中的。
而对于编译期可以“确定值”的常量:例如Demo2中STR2 是存放在Demo2类调用者所在的常量池中的,当STR2放到常量池后Demo2与Parent2类的关系就没有了!
对于编译期“不确定值”的常量:例如Demo3中STR3 是不存在Demo3的调用者的常量池中的,在程序运行期间会主动使用Demo3所在的类,会加载其静态块!
上面的程序Parent2.STR2输出后就退出了Parent2类,因为程序加载Parent2类是直接调用了常量池中关于STR2的引用,无需再加载类的其他信息包括静态块,说白了按我的理解就是常量池和类的其他信息不在一个内存区,程序根据指令扫描的时候无需扫描不相关的内存区!说到池(池?嗯,等等是不是和线程池和队列有关,哈哈。学东西要养成发散思维的习惯,这样就能串起来很多知识点,渐渐形成自己的知识面。)接下来就来扩展一下:
1 | package jvm; |
注⚠️:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean
另外两种浮点数类型的包装类Double Float则没有实现。
另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用常量池,即对象不负责创建和管理大于127的这些类的对象
类加载器 分类
1、java虚拟机自带的加载器
- BootStrap 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)
- Ext 扩展类加载器 (加载一些扩展jar包中的类)
- Sys/App 系统(应用类)加载器 (我们自己编写的类)
2、用户自己定义的加载器
- ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器
Demo4代码:
1 | package jvm; |
「本地接口库
Native方法 JNI : Java Native Interface (Java 本地方法接口)
大家都知道java是从C过度过来的,C可以直接操作硬件
思考一个问题:线程是基于内存的,那么应该是操作硬件 让计算机cpu来划分时间片来“同时”执行多个任务,那么程序是怎么怎么达到底层的呢?java 能直接操作硬件吗?我们来看看线程是怎么启动的,看源码(面向源码编程)
1 | public synchronized void start() { |
native : 只要是带了这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!
这部分知识作为java开发只需要了解即可,如果想去研究也可以看看嵌入式C的相关东西(C语言->编译器—>汇编语言—>驱动->硬件)。
运行时数据区
程序计数器
每个线程都有一个程序计数器,是线程私有的。
程序计数器就是一块十分小的内存空间;几乎可以不计
作用: 看做当前字节码执行的行号指示器
分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!
bipush
将 int、float、String、常量值推送值栈顶
istore
将一个数值从操作数栈存储到局部变量表
iadd
加
imul
乘
方法区
Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!
JDK1.7之前
永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等,这些东西都会放到永久代中;
永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen
JDK1.8之后
彻底将永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空间;
元空间就是方法区在 HotSpot jvm 的实现;
方法区主要是存:类信息,常量,字符串、静态变量、符号引用、方法代码。
元空间和永久代,都是对JVM规范中方法区的实现。
元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!
设置元空间大小:-XX:MetasapceSize10m
堆和栈
堆和栈都是内存中相对独立的一块区域,jvm内存分为5个区域,分别是寄存器、本地方法区、方法区、栈内存、堆内存!
注⚠️:每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器,我们习惯上称呼为程序计数器,并不是什么其他的东西!
栈和堆的区别
1.堆是不连续的,所以分配的内存是在运行期确认的,因此大小不固定;堆对于整个应用程序都是共享、可见的;堆内存存储的是实体;堆内存存放的实体会被垃圾回收机制不定时的回收
2.栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的;栈只对于线程是可见的,所以也是线程私有,他的生命周期和线程相同(所以说,栈里面是一定不会存在垃圾回收的问题);栈内存存储一些基本类型的值,对象的引用,方法等;栈内存的更新速度要快于堆内存(仅次于寄存器),因为局部变量的生命周期很短;栈内存存放的变量生命周期一旦结束就会被释放。
面试题:java中的基本数据类型都是存储在栈中的吗?
面试基本上问到这里都是三连,说说八大基本数据类型?说说堆和栈?接下来就是这个有坑的问题
答:不是
一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
(1)当声明是基本类型的对象时,其变量及变量的引用都在栈中。
(2)当声明的是引用类型(new出来的)时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆内存中的。
二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
(1)当为基本类型时,其变量名及其值放在堆内存中的
(2)当为引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
扩展–>请根据以下代码说说6个对象的在堆栈中是如何分配的?
1 | String str1 = "abc"; |
结合下面的图来理解String 常量存放的位置:
题外话:JVM目前有 SUN公司 HotSpot、BEA公司 JRockit、IBM公司 J9VM
关于堆在垃圾回收器里面介绍!
执行引擎
类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。
字节码必须通过类加载过程加载到JVM后才能够被执行,执行有三种模式:
- 1.解释执行
- 2.JIT(Just-In-Time)编译即执行
- 3.JIT与解释混合执行
来看下执行引擎的简要执行流程:
关于执行引擎这种底层的东西了解即可,说白了就是执行编译后字节码的一套概念模型!执行引擎包含了众多可以操作字节码的执行指令,使程序能够按照一定的顺序执行!关于执行引擎(推荐知乎层层剥开JVM——字节码执行引擎 ,亦可以推荐孤尽老师《码出高效》一书中的第四章“走进JVM”来深入了解)
垃圾回收器
呼,长出了一口气!终于写到和堆相关的了
看堆之前我们先了解下JVM的内存布局:
物理上只有 新生、养老;元空间在本地内存中,不在JVM中!
GC 垃圾回收主要是在 新生区和养老区,又分为 普通的GC 和 Full GC,如果堆满了,就会爆出 OutOfMemory(OOM);
新生区
新生区 就是一个类诞生、成长、消亡的地方!
新生区细分: Eden、s(from to),所有的类Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区(),……. 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;假设项目1年后,整个空间彻彻底底的满了,突然有一天系统 OOM,排除OOM问题,或者重启;
Sun HotSpot 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)
Eden from to
99% 的对象在 Eden 都是临时对象;
养老区
15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC
默认是15次,可以修改!
永久区(Perm)
放一些 JDK 自身携带的 Class、Interface的元数据;
几乎不会被垃圾回收的;
OutOfMemoryError:PermGen
在项目启动的时候永久代不够用了?加载大量的第三方包!
JDK1.6之前: 有永久代、常量池在方法区;
JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;
JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中;
闲聊:方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,但是记住它的别名 非堆
jvm内存调优
来看以下代码:
1 | package jvm; |
输出信息如下:
由计算得出(305664K+699392K)/1024 = 981.5MB
说明新生代和老年代内存相加为整个分配内存大小,也从侧面证明了元空间不存在于jvm中,它存在于本地空间中!
–认识了堆后,我们再来聊聊堆的内存调优,这是每个java开发都需要掌握的!
模拟oom,看看GC详情:
1 | package jvm; |
输出如下:
1 | "C:\Program Files\Java\jdk1.8.0_171\bin\java.exe" -Xmx8m -Xms8m -XX:+PrintGCDetails "-javaagent:D:\idea\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=63475:D:\idea\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_171\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\rt.jar;E:\sCloud\studyDemo\out\production\studyDemo" jvm.HeapDemo1 |
好了,理解这些参数后基本知道怎么调优了。
其实Idea 还有一款插件JProfiler
专门用来分析程序内存占用情况的,用JProfiler
可以dump 内存快照帮助我们快速定位问题。教程百度上有,此处不做说明!
注⚠️:JVM在进行GC时,并非每次都是对三个区域进行扫描的!大部分的时候都是指的新生代!
- 普通GC:只针对新生代 【GC】
- 全局GC:主要是针对老年代,偶尔伴随新生代! 【Full GC】
GC四大算法:
引用计数法
特点:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,则计数器-1,如果为0,则GC可以清理;
缺点:
- 计数器维护麻烦!
- 循环引用无法处理!
注⚠️:JVM 一般不采用这种方式,现在一般使用可达性算法,GC Root…
复制算法
1、一般普通GC 之后,差不多Eden几乎都是空的了!
2、每次存活的对象,都会被从 from 区和 Eden区等复制到 to区,from 和 to 会发生一次交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认值,后面我们会讲解调整),就会进入养老区;
优点:没有标记和清除的过程!效率高!没有内存碎片!
缺点:需要浪费双倍的空间
Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!推荐使用 复制算法
标记清除算法
老年代一般使用这个,但是会和我们后面的整理压缩一起使用!
优点:不需要额外的空间!
缺点:两次扫描,耗时较为严重,会产生内存碎片,不连续!
标记清除压缩
减少了上面标记清除的缺点:没有内存碎片!但是耗时可能也较为严重!
那我们什么时候可以考虑使用这个算法呢?
在我们这个要使用算法的空间中,假设这个空间中很少,不经常发生GC,那么可以考虑使用这个算法!
1 | 总结: |
面试题:
最后再来看看几个面试题:
1.JVM 垃圾回收的时候如何确定垃圾,GC Roots?
- 引用计数法 (每引用一次就加一,为0或-1就会被GC清除)
- 可达性分析算法 (如果从 GC Root 这个对象开始,对于没有关联的对象GC认为不可达的,就会被清理)
2.什么是GC Root?
- 1、虚拟机栈中引用的对象!
- 2、类中静态属性引用的对象
- 3、方法区中的常量
- 4、本地方法栈中 Native 方法引用的对象!
3.jvm 常用参数有哪些,你都用过哪些?
1、标配参数:
-version
,-help
,-showversion
2、X参数:
-Xint
# 解释执行,-Xcomp
# 第一次使用就编译成本地的代码,-Xmixed
# 混合模式(Java默认)3、XX参数:+ 或者 - 某个属性值, + 代表开启某个功能,- 表示关闭了某个功能 例如:
jps -l
,jinfo -flag PrintGCDetail 进程号
# 查看某个运行中的java 程序,-XX:+PrintGCDetails
# 打印GC详情4.XX 参数之key = value值;例如:元空间大小:
-XX:MetaspaceSize=128m
#设置元空间大小,-XX:MaxTenuringThreshold=15
#设置新生代需要经历多少次GC晋升到老年代中的最大阈值(默认值是15,最大也是15)1).
-Xms
初始堆的大小,等价:-XX:InitialHeapSize
2).
-Xmx
最大堆的大小 ,等价:-XX:MaxHeapSize
-XX:+PrintFlagsInitial
查看 java 环境初始默认值;这里面只要显示的值,我们都可以手动赋值,不建议修改,了解即可!=
默认值
:=
就是被修改过的值
java -XX:+PrintCommandLineFlags -version
打印出用户手动选项的 XX 选项
4.你常用的项目,发布后配置过JVM 调优参数吗?
1 | -Xms |
小结:至此JVM的知识,基本已总结完成,推荐书《码出高效》;买了很多书但是一直没看,我新增了栏目“拆书” 有时间会把买的书啃完了谢谢心得体会,或者看到精彩的地方会分享出来,当然不至于技术一类。为能看到这儿的自己点个赞吧,加油!
- Title: 浅谈jvm虚拟机
- Author: viEcho
- Created at : 2021-04-23 19:55:09
- Updated at : 2024-01-18 14:51:01
- Link: https://viecho.github.io/2021/0423/jvm-talk.html
- License: This work is licensed under CC BY-NC-SA 4.0.