这不是我第一次学习 JVM 的知识了,从开始学习 java 语法开始,老师就告诉咱们堆啊、栈啊的,那会真是不理解啊,狗捉耗子多管闲事,知道怎么写代码不就好了嘛~java
后来逐渐的知道了,java 内存分配的意思,不了解关于 java 内存的部分,你都不知道你的变量何时就不是你想要的那个值了android
所以专门去看了 java 的内存分配
,为了性能优化又去看了GC 垃圾回收
,这其中反复看了好几回web
此次应该是我第5次看 JVM 的内容,也是第2次写 JVM 的博客,上次那篇已经做废了。之前即使学习 内存分配
和 GC 垃圾回收
那也是单独的看,从没有站在 JVM 整体设计的角度一块儿看思考,此次站在 JVM 整体设计的角度
,我发现了更多的知识点,好比:类的加载机制
,也发展其实诸如这些其实都是紧密相互关联的,只要咱们能理解 JVM 设计的初衷,理解这些其实也没有多大难度了,单个内容理解起来有的真的挺费劲的面试
web 上的博文基本都是说 JVM 单个知识点的,随着 java 版本的变迁,其中有太多的错误,让咱们理解起来既费劲,也搞不明白数据库
JVM 的书却是有基本不错的,可是阅读门槛比较高,强读不少都理解不了bootstrap
这里我推荐B站尚硅谷的JVM视频,讲的很是好,不光有理论,还有严谨的推导过程,使用转用工具一步步验证,而不是胡说白咧、胡讲,不少内容也是引用自学习JVM的经典书籍:《深刻理解 JVM 虚拟机》
小程序
这一个视频+这本书,学习 JVM 的不二法宝,你们不用再找其余资料了,你想知道的,你想不到的这里都有,尤为是视频,即使小白都能看的懂,感谢尚硅谷。更难能难得的是该视频里有不少分析工具和思路,这点是很是值得学习的东西,甚至比JVM自己更值得学习后端
另外在学习过程当中,有一些连带的点很重要,面试文的不少的,可是不太适合放在本文的,本文也不能写的太长了,就另开了JVM面试的文章,由于关联性很大,但愿你们都去看看,能对JVM的理解更上一个台阶数组
须要自定义类加载器的请看这里的内容:浏览器
java 最重要的3个虚拟机:hotspot,JRockit,J9 IBM的
10年 Oracle 收购了 REA 以后,致力于融合 hotspot 和 JRockit 这2个知名的虚拟机,可是2者之间差别太大,hotspot 能够借鉴的比较少,这个成果在 JDK 8 中得以体现,JDK 8的虚拟机虽然还叫 hotspot,但这个 hotspot 是大量借鉴 JRockit 技术以后的成果了,不可同日而语
JDK 11时,革命性的垃圾回收器 ZGC 出来了,目前 ZGC 仍是实验性的,可是实验来看性能远超 G1,虽然 G1 垃圾回收器仍是主流,可是将来必定会被 ZGC 替代
JDK 11 开始,Oracle 每3个版本发布一个长期稳定支持版本,其余版本都支持半年,而且更新内容有限,只有大版本才有大的变化。可是也是从11开始,Oracle 每次都发布2个版本,一个免费 OpenJDK,一个商业收费 OracleJDK
因此 JDK8 是目前使用最多的版本,也是目前咱们学习的基准,另外阿里巴巴有本身的虚拟机 Taobao JVM,这里不得不赞一个,阿里真是国内互联网的基石啊
学习 JVM,咱们固然要学习 JVM 的3大组成部分了:类加载器
、运行时数据区
、执行引擎
可是咱们以前都是每一个点学每一个点的,从没有站在 JVM 整体的角度上串起来看,这就是此次我要展现给你们的,从整体上看,从整体上理解,其实每一个点都是相互关联的
任何代码都是跑在硬件上的,咱们以前学习 JVM 的内容都是学习的 API,从没有考虑硬件上的内容,其实咱们如果把 JVM 和硬件上的关联搞清楚,不少晦涩难懂的知识点迎刃而解。好比多线程,难倒不是由于内存的缘由而设计的吗,难倒 java 的多线程不是 JVM 决定、管理的嘛,归根结底,多线程就是内存、字节码、指令的运做
java 相比 c 多了什么,多的就是 JVM,就是今天咱们研究的东西。java 里咱们只要关系逻辑代码怎么写就好了,内存分配不用咱们管,内存回收不用咱们管,和操做系统的交互不用咱们管。经典的 Thread 就是 JVM 代咱们去和操做系统内核交互
C++ 须要咱们本身分配内存,本身回收,你 C++ 要是技术很好,内存可使用的很是高效,也不会出现涌余。但要是你技术不高的话,内存可能会很是混乱,从语言发展的角度说自动管理也是大的趋势
有人说:JVM
已是一个独立的虚拟的计算机了,一台新的机器只要安转了 java 运行环境,立马就行跑 java 代码,可是 C 行吗... 在C 里面咱们要本身操做内存,要本身和操做系统交互
这就是为何 JVM 在如今愈来愈受欢迎的原理,封装了底层操做,让咱们专心于逻辑,这点也是高级语法发展的趋势,就算不是 JVM,也会有本身的 VM,让代码愈来愈简单
不光如此,JVM 不只仅是对开发者屏蔽了硬件和操做系统层面的操做,JVM 更是有本身的指令系统:字节码
,就是这么个东西,咱们知道 CPU 硬件实际执行的是 010101 这样的二进制指令代码。而 JVM 有本身的指令代码,就是编译完成的 .class
里面的内容
这里是一个反编译出来的方法,你们看看用本身码是怎么写的
public void speak();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: return
LineNumberTable:
line 12: 0
line 13: 3
line 15: 6
复制代码
bipush 10
、istore_1
这些你们认识吗,看着是否是和汇编多少优势像啊,这就是 JVM 本身设计的,专属于本身的指令集。因此说 JVM 更像是一个虚拟的计算机,只要有一个硬件设备,上面安装有一个内核,JVM 就能顺利的运行,甚至不须要完整的操做系统支持
什么是虚拟机:就是一套用来执行特定虚拟指令的软件。好比要在 mac 上跑 wins 就须要一个虚拟机,要不 mac 怎么认识 X86 指令呢...
若是说 java 是跨平台的语言:
那么 JVM 就是跨语言的平台:
愈来愈多的语言选择运行在 JVM 环境上,无论这个语言怎么写的,主要该语言的编译器把代码最终编译成 .class
标准字节码文件,那么就都能在 JVM 上运行。像上图中的,这些永远均可以在 JVM 上运行
JVM 已经变成一个生态了,这不能不让咱们去思考,我以为你们看到这里都思考一下是有好处的,感慨下,这就是一种趋势
整体来讲JVM就一句话:从软件层面屏蔽不一样操做系统在底层硬件个指令上的区别,也包括顶层的高级语言
这是 java 程序的结构,JVM 提供最底层运行支持,使用 java 提供的 API 开发了不少框架,咱们使用这些框架开发出最终的服务,app等
JVM 是最终承载咱们代码的地方,你的服务运行的好很差,卡不卡不都看 JVM 的反馈嘛。单从性能优化的角度看,咱们都得对最底层的知识体系有足够了解
懂得 JVM 内存结构,工做机制,是设计高扩展性应用和优化性能的基础,阻碍程序运行的永远是咱们对硬件使用的效率,对硬件使用效率的高低决定了咱们程序执行的效率
下面这些问题我想你们都会遇到吧:
线上系统忽然卡死,OOM
内存抖动
线上 GC 问题,无从下手
新项目上线,JVM 参数配置一脸懵逼
面试 JVM 直接被拍晕在地上,JVM 如何调优,如何解决 GC,OOM
JVM 玩不转别想吧上面这些搞顺溜了...
就算是否是后台服务的,你搞 android 或者其余就没有 内存抖动
的问题啦,不可能的,只要你语言用的 java 或者跑在 JVM 上,这 JVM 都是你逃不过去的
知道了这些点以后,有助于咱们理解后面 JVM 的内容
这是摘抄过来的一句话,不用再解释了,你们仔细揣摩
虚拟机的概念是相对于物理机而言的,这两种机器都有执行代码的能力。
物理机的执行引擎是直接创建在硬件处理器、物理寄存器、指令集和操做系统层面的
而虚拟机的执行引擎是本身实现的,所以能够自定义指令集和执行引擎的结构体系
并且能够执行那些不能被硬件直接支持的指令
在不一样的“虚拟机”实现里面,执行引擎在执行JAVA代码的时候有两种方式:
1. 解析实行(经过解释器执行)
2. 和编译执行(经过即时编译器编译成本地代码执行)
复制代码
java程序是跑在JVM上的,严格来说,是跑在JVM实例上的,一个JVM实例其实就是JVM跑起来的进程,两者合起来称之为一个JAVA进程
各个JVM实例之间是相互隔离的
一个进程能够拥有多个线程
一个程序能够有多个进程(屡次执行,也能够没有进程,不执行)
一台机器上能够有多个JVM实例(也能够没有JVM实例)
进程是指一段正在执行的程序
线程是程序执行的最小单位
经过屡次执行一个程序能够有多个进程,经过调用一个进程能够有多个程序
程序运行时,会首先创建一个JVM实例----------因此说,JVM实例是多个的,每一个运行的程序对应一个JVM实例。每一个java程序都运行在一个单独的JVM实例上,(new建立实例,存放在堆空间),因此说一个java程序的多个线程,共享堆内存
总的来讲,操做系统的执行单元是进程,每个JVM实例就是一个进程,而在该实例上运行的主程序是一个主线程(能够当作一个轻量级的进程),该程序下还存在不少线程
还有一个 JVM 实例对应一个 Runtime 对象,咱们能够从该 Runtime 对象中获取一些参数,好比堆内存的初始值和最大值
有意思的是,java 最先是为了在 IE3 浏览器中执行 java applets,原来早先 java 也是小程序出身,可是谁让后来 java 火了呢...
阿里很NB,本身基于 OpenJDK 深度定制了本身的 alibabaJDK,而且定制了本身的 Taobao JVM,很厉害的
其特色:
缺点是高度依赖 Intel cpu,目前在天猫,淘宝上应用,全面替代 Oracle 官方 JVM
线程是一个程序里的运行单元,JVM 容许一个应用有多个线程并行执行
在 Hotspot 虚拟机中,每一个线程都与操做系统的本地线程直接映射。当一个 java 线程准备好执行以后,一个操做系统的本地线程也会同时被建立。java 线程终止后,本地线程也会被回收
操做修通负责把全部线程安排哦调度到任何一个可用的 CPU 上去执行,一旦本地线程初始化完成,就会调用 java 线程中的 run()
一个 JVM 实例里有不少后台线程:
虚拟机线程:
这种线程的操做是须要JVM达到安全点才会出现,这种线程的执行类型包括"stop-the-wrold"的垃圾收集,线程栈回收,线程挂起,偏向锁撤销周期任务线程:
这种线程是时间周期事件的体现,好比中断GC线程
编译线程:
把字节码编译成本地代码信号调度线程:
这种线程接收信号并发送给JVM只须要在 VM options
里面写设置便可,好比:
-XX:MetaspaceSize=100m
复制代码
MetaspaceSize 是方法区的大小,这样写就行,想改哪一个就用对应的英文单词好了
后面你们会看到诸如:-Xms
这样的JVM参数,一看就知道是简写,其实 -Xms = -XX:InitialHeapSize
,你们知道就行,对照着就知道了,别2个都碰到了不知道啥意思
能够查看进程信息
jps:
打印全部进程➜ ~ jps
71187 Jps
70867 GradleDaemon
70814
复制代码
jps -l:
输出完整package整路径,android 进程也能打印出来,可是仅限于本身安装的 app➜ ~ jps -l
70867 org.gradle.launcher.daemon.bootstrap.GradleDaemon
71193 sun.tools.jps.Jps
70814
复制代码
能够打印出想看的JVM参数信息,想看那个参数后面跟英文单词和进程ID就行啦
// 打印信息,74290 是进程ID,能够用上面 jps -l 命令查看
➜ ~ jinfo -flag MetaspaceSize 74290
// JVM 配置
-XX:MetaspaceSize=21807104
复制代码
-XX:+PrintGCDetails
VM options 配置项,能够在日志里面把堆栈信息打印出来,挺有用的
// 堆内存
Heap
// 年轻代
PSYoungGen total 38400K, used 4663K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 14% used [0x0000000795580000,0x0000000795a0dc88,0x0000000797600000)
from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
to space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
// 老年代
ParOldGen total 87552K, used 61440K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 70% used [0x0000000740000000,0x0000000743c00010,0x0000000745580000)
// 元空间
Metaspace used 3387K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
复制代码
基本就4个参数:
MetaspaceSize
- 方法区大小MaxMetaspaceSize
- 方法区最大值,这个数代码里是无限,但实际上不能超过物理内存最大值MinMetaspaceFreeRatio
- 在GC以后,最小的Metaspace剩余空间容量的百分比MaxMetaspaceFreeRatio
...-XX:MetaspaceSize=100m
- VM options 设置写法MaxMetaspaceSize
通常咱们不动,这个默认是无穷大数,咱们通常都会把 MetaspaceSize
调大一点,避免由于 MetaspaceSize 太小形成的 FullGC
-XX:UseTLAB
- TLAB 线程专属空间大小Xmx
- 堆内存最大值,默认=物理内存的 1/4Xms500m
- VM options 这么写-XX:NewRatio=2
- 新生代老年代比例,2的意思是新生代是1占总数的1/3,老年代代是2占总数的2/3,通常咱们不改这个参数,由于新生代小了,意味这GC回收频率就要高了-XX:SurvivorRatio=8
- Eden、S0、S1 的比例,8的意思 Eden 是8占总数的8/10,S0是1占总数的1/10,S1是1占总数的1/10Xmn
- 新生代最大值,通常不动,通常都用比例,这个写了比例就不算数了-XX:+UseAdaptiveSizePolicy
- 自适应内存分配策略,-号
是取消设置,+号
是采用设置,这个其实不起做用的...jinfo -flag NewRatio 进程ID
- 打印新生代老年代比例jinfo -flag SurvivorRatio 进程ID
- 打印新生代内比例-Xss
- 栈内存值,只有这个一个参数,能够理解为最大值-Xss900k
- VM options 设置写法JVM 也是有生命周期的,上文说到咱们能够把进程当作一个JVM实例
JVM 生命周期:
启动:
JVM启动时会按照其配置要求,申请一块内存,并根据JVM规范和实现将内存划分为几个区域。而后建立出引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String,java.lang.Object等等。而后加载咱们写有main方法入口的那个类,执行 main 函数运行:
就是执行 main 函数,何时 main 函数结束了,JVM 也就完结了退出:
退出护着异常退出,用户线程彻底退出了,jvm示例结束生命周期// 通常 java 里面咱们退出进程就是这2个方法
// System.exit 其实也是调的 Runtime,咱们跟进去看看
System.exit(0);
Runtime.getRuntime().exit(0);
------------------------------------------------------------------
// System.exit(0)
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}
// Runtime.getRuntime().exit(0)
public void exit(int status) {
// Make sure we don't try this several times
synchronized(this) {
if (!shuttingDown) {
shuttingDown = true;
........
// Get out of here finally...
nativeExit(status);
}
}
}
// nativeExit 最终是一个本地方法
private static native void nativeExit(int code);
复制代码
可能你们对 Runtime
不熟悉,Runtime 是什么呢,就是整个运行时数据区
Runtime
了
这里咱们以 JDK 为准,以 Hotspot 虚拟机为主
图片来源于:鲁班学院-子牙老师
JVM 的3大组成部分:类加载器
、运行时数据区
、执行引擎
下文我会按照 JVM 最佳学习顺序来逐个介绍:类加载器->方法区->内存结构->GC->执行引擎
在上图里你们能够看的很清楚了,这是我能找到的 JVM 最准确、全面的一张结构图了,你们之后以这个为准吧
运行时数据区
这个一贯是你们理解的重点,这里有一点其实不少人搞不清楚
线程独有的区域包括:虚拟机栈、本地方法栈、程序计数器
这3个,这3个区域是线程间不可见的,只有本身所在线程能够访问
更详细一点的是这张图:
有句话这么说的:栈管运行,堆管存储
,因此堆内存很大,天然要放在物流内存中,也就是内存条里
类加载器
这个很重要的,好多黑科技都有使用到类加载器手动new对象,咱们要对这块有清晰的了解才行,虽然 android 有本身的 DexClassLoader,可是也是以 java 的类加载器位基础的,学了不吃亏
执行引擎
包括3部分:解释器,JIT 即时编译器,GC 垃圾回收器
3部分组成
java 默认是逐行解释的,运行时,运行到那行字节码了,解释器就去执行该行本身码,字节码怎么执行呢,很简单,没一个字节码指令对应一个 C++ 的方法,JVM 总体都是用 C++ 写的,因此最终字节码都是转换成 C++ 代码去执行
从 OpenJDK cpp 里能够找到执行器的源码,java_executor.cpp 就是
很清楚吧,switch、case,每一个字节码指令都对应一个或者多个 C 的方法
IT 即时编译器
是 ART 虚拟机新加入的特性,也是目前 VM 发展的趋势,不少 VM 也加入了 JIT 这个特性,JIT 干的事就是记录并判断热点代码,正常流程解释器解释本身码要吧相关参数带入到相应的C方法路面去执行,会C方法进一步翻译成汇编语言才能给CPU硬件去执行
JIT 就是把热点代码提早编译成汇编代码,能够直接运行,比解释器省了一部操做,这样能够提升CPU执行效率
JIT 对于热点代码编译成机器码以后是缓存在方法区的
堆内存:
java heap space,满了会抛出OOM异常元空间:
Metaspace ,满了同样会抛出OOM异常栈空间:
Satck,满了同样也会抛出OOM异常无论咱们在 IDE 里面代码写的如何飞起,编译以后也仅仅是冷冷的一个class文件,躺在硬盘里。可是电脑最终是要运行的,靠的是内存。类加载干的就是读取硬盘里面的class文件,转换成能够在内存中,提供给计算机运行、执行程序的类信息(DNA元数据模板)
类信息保存在方法区里面,方法区在本地内存,可是在JVM的堆内存也会跟着生成一个class对象,这个就是咱们反射用到的 class 对象,详细后面会说
整体来讲class文件从硬盘加载到内存并能够运行,要经历3个大的步奏:
类加载器系统和方法区紧密相联,毕竟类加载出来的东西是放在方法区的,可是这其中仍是又不少讲头的。字节码、常量池、运行时常量池、符号引用转直接引用我都放在后面方法区那部分了,你们像了解请转到后面那里
经过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构对象,并在内存中生成对应的Java.lang.class对象
加载类的方式其实不少,你们必定要清楚,黑科技都是借助这个的:
从本地系统直接加载
网络获取,场景:web Applet
从 zip、jar、war 等压缩包中读取
运行时动态生成,好比:动态代理技术
由其余文件生成,好比:JSP 应用
从数据库中获取 class 文件
从加密文件中获取
你们必定要清除啊,android 的黑科技那个没用带这个呢...
连接里面3个小的步奏:验证、准备、解析
校验:
检查导入类或接口的二进制数据的正确性:(文件格式验证,元数据验证,字节码验证,符号引用验证)准备:
给类的静态变量分配并初始化存储空间,既然都分配属性的内存空间了,那么确定就有对象了,因此加载那一步在方法区建立出运行时数据结构对象确定是没错的,要不到这里解释不了。我曾经对什么时候生成方法区数据对象产生怀疑解析:
将常量池中的符号引用转成直接引用类加载时的初始化方法可不是默认的构造方法啊,构造方法是位对象服务的,类的初始化方法是位类服务的
JVM编译器在编译时会收集类静态变量赋值操做和静态代码块的操做,把这2者合并成一个方法:clinit()
,注意这个方法是C++的,对咱们不可见。若类没有静态属性,也没有静态代码块那就就没有这个 clinit() 啦
clinit() 方法中代码执行的顺序是按照代码书写顺序来的,谁写在前面,谁就在前面的
好比:
public class Max {
static {
age = 100;
}
public static int age = 10;
}
复制代码
这个 age 最后赋值结果是10,谁让static声明在后呢,那age确定就是10了,具体的分析请看:JVM 面试题【中级】,这里写的很清楚,答案都在字节码里
另外 clinit()
只会执行一次,也就是在类首次被加载的时候执行,因此JVM对于clinit()方法是加锁的。这个锁是谁呢,就是该类方法区类元信息在JVM堆内存中的映射class对象,class也是一个对象,也有本身的对象锁,这里用的就是这个锁,该锁最多见的应用就是经典的双判断单例写法了
另外还要注意,静态代码块里面不要写死循环,要不其余线程在同时加载该类型时会一直阻塞在 clinit 的
好比:
public class Dog {
static {
while (true) {
}
}
}
public class Max {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
Dog dog = new Dog();
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
复制代码
对于 t1,t2 来讲,无论谁先加载 Dog 类,都会让对方一直阻塞在 Dog 的初始化函数这里没发往下执行,因此 static{} 静态代码块里咱们不要写耗时操做
clinit() 函数在执行时会优先执行父类的 clinit() 函数
java 类是咱们何时用,何时才加载,这一点你们最好内心有数,有时候有些 BUG 就是由于类虽然加载了可是初始化方法没有自行,你认为在那个时刻类确定会初始化,但实际上她没有
类的使用能够分主动使用、被动使用,主动使用时会初始化该类,被动使用时不会初始化该类。类加载的3部中,初始化方法不是必须顺着加载、连接这2部执行完后执行的,而是能够自由决定何时用
类主动使用的状况:
上面说过类加载能够分3分个大的步奏,在代码上全靠系统提供的类加载来完成,不光涉及java部分,更是涉及C++部分
ClassLoader
是全部类加载的抽象基类,最终返回堆内存种的class对象
public abstract class ClassLoader{
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve){
......
}
}
复制代码
每一个 ClassLoader 都有本身的本身的上一级 ClassLoader,也就是父 ClassLoader,这里在双亲委派机制时会说
实际上系统类加载器有3种,分别加载不一样范围的类:
BootStrapClassLoader:
引导类加载器,用C++语言写的,它是在Java虚拟机启动后初始化的,能够理解为JVM的一部分。加载java系统核心类库,加载JDK目录下:/jre/librt.jar、resources.jar、charsets.jar
的类,为了安全起见,BootStrapClassLoader 只加载包名以:java、javax、sun
开头的了类。BootStrapClassLoader 彻底由JVM本身控制,咱们不只控制不了,甚至都不可见,在JAVA层面即使拿到 BootStrapClassLoader 的实例,对咱们来讲也是一个null。BootStrapClassLoader 没有父加载器,BootStrapClassLoader是C++实现的,不可能再走一个java的父类了EtxClassLoader:
扩展类加载器,是java级别的了,是咱们能够获取的到的了。注意其父加载器是引导类加载器,加载JDK:/jre/lib/ext
中的类(扩展目录),AppClassLoader:
系统类加载器,父加载器是扩展类加载器,加载全部非java核心类库,简单的说就是否是java官方写的代码,好比咱们本身,第三方开源类库都是由她加载。引导类加载器加载目录:
// 系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
// 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@27716f4
// 引导类加载器,java.lang.string 已是java核心类库范围了
ClassLoader bootStrapClassLoader = String.class.getClassLoader();
System.out.println(bootStrapClassLoader);//null
复制代码
获取类加载器的几种方式:
// 经过class获取类加载
ClassLoader classLoader1 = new Dog().getClass().getClassLoader();
ClassLoader classLoader2 = Class.forName("com.bbb.xxx").getClassLoader();
// 经过线程上下文获取类加载,拿到的是系统类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// 直接获取系统类加载器的单例
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
复制代码
ExtClassLoader 和 AppClassLoader 都是在 Launcher 中初始化的,并将 AppClassLoader 设置为线程上下文类加载器。 ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader ,而最终的父类则为 ClassLoader
Launcher public Launcher() {
ExtClassLoader localExtClassLoader;
try {
// 扩展类加载器
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader", localIOException1);
}
try {
// 应用类加载器
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader", localIOException2);
}
// 设置AppClassLoader为线程上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// ...
static class ExtClassLoader extends java.net.URLClassLoader static class AppClassLoader extends java.net.URLClassLoader } 复制代码
固然类加载器也能够自定义的
通常这几种状况下会考虑自定义类加载器:
防止源码泄露
- 对字节码加密,类加载时解密,预防反编译篡改扩展加载源
- 插件化,热修复修改类的加载方法
隔离加载类
- 中间件,中间件和应用模块是隔离的,把类加载到不一样环境当中,相互之间不冲突,防止不一样依赖之间包名类名相同的类的冲突咱们能够选择继承 ClassLoader 类,重写 findClass() 方法返回目标 class 对象,实际上须要咱们本身实现IO流,从磁盘加载class文件到内存生成对应的 bute[] 字节数组,以前咱们要是对字节码加密了,那么这个过程咱们能够进行解密操做,而后使用使用 ClassLoader 自带的 defineClass() 方法把字节数组转换成 class 对象并返回
class MyClassload extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = getCustomeClass(name);
Class<?> aClass = defineClass(name, bytes, 0, bytes.length);
return aClass;
}
public byte[] getCustomeClass(String name) {
return null;
}
}
复制代码
还有更多我就不详细写了,有须要的请自行 google
咱们先回国头来再看一遍 ClassLoader 的设计:
public abstract class ClassLoader{
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve){
......
}
}
复制代码
ClassLoader 对象设计有 parent 父加载器,你们看着像不像链表。链表的next指向下一个,ClassLoader parent 这里上一层级
类加载加载机制中默认不会直接由本身加载,会先用本身的父加载器 parent 去加载,父加载器加载不到再本身加载
JVM 3级类加载器,每一级都有本身能加载类的范围,类加载器一级一级提交给父加载器去加载,每一级类加载在碰到本身能加载的类时,没加载过的会去加载,加载过的会返回已经加载的class对象给下一级
看看 ClassLoader.loadClass() 方法代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
复制代码
这个就叫作:双亲委派机制
,为啥叫双亲,由于系统类加载器上面就2级类加载器
java 核心类库有访问权限限制,类加载器在发现容许加载范围以外的类加载的加载请求以后,会直接报错的。这个判断通常都是用包名来判断的,好比你本身搞了一个 String 类,包名仍是 java.lang,那引导类加载器在处理这个加载请求时会直接报错,这种报错机制就叫作沙箱安全机制
沙箱安全机制有个典型例子:360沙箱隔离,好比U盘程序只在360认为隔离出来的沙箱内运行,以保护沙箱外的系统不受可能的病毒污染
双亲委派机制的目的是为了保证安全,防止核心 API 被篡改
方法区这个名称是 JVM 规范对管理 class 类信息的内存区域的称谓,你们能够当作是一个接口,声明出方法来了,可是具体怎么实现还得看具体的实现类不是
JDK1.6 以前 hotspot 虚拟机关于方法区的实现叫永久带,和堆内存同样都在JVM实例进程内,OOM的问题比较严重,GC也会频繁扫描这个区域,性能比较低
JDK1.8 hotspot 虚拟机换了个方法区的实现叫元空间,把类信息从JVM实例内存区域移出来,放到本地内存 native memory 中去了,这样 OOM 风险小多了,不再怕加载的类太多爆 OOM 了,GC 扫描的频率也下降了
JVM 各部分之间联系很紧密,方法区承载类加载器加载、解析到内存中的字节码文件,记录类的元信息,包括:
类的信息:
类名,报名,访问限制符,父类,实现的接口,注解字段信息:
也叫域信息,是类中全部的成员变量方法信息:
方法的名字,参数,访问限制符,还包括方法自己须要执行的字节码类加载器的引用:
方法区的类信息中会记录加载该类的类加载器,一样类加载器也会记录本身加载了哪些类class 引用:
这里是指堆内存的 class 对象引用常量池:
其实都是字符串和编译时就能肯定的数据,好比 final int 的值,编译的时候就能肯定时多少,由于 final 的 int 是没有机会变化的,不要和运行时常量池混了,这里的常量池实际上是为了减小字节码文件体积,尽可能复用可能会重复的字符串,以后解析时会把这些字符串即符号引用转换成对应的对象引用,好比父类啊,属性类型啊,这些都会再解析时把对应的类加载出来,这样字符串就变成了类引用了JIT 即时编译器编译事后的代码缓存
或者这张图,classFile 就是编译以后的class文件,它的数据结构就是这样的,单词不难,你们一看就知道咋回事了,就像一个Bean数据对象同样,记录一个类里面的都有啥,难点很差理解的是 constant_pool,这个下面会仔细说一下的
这是代中文对照的图:
堆、栈、元空间的相互关系:
其实咱们从反编译下字节码就知道怎么回事了,字节码文件会加载到方法区,也就是数据储存结构有些变化,可是东西仍是字节码里面的东西
public class Max {
public static int staticIntValue = 100;
static {
staticIntValue = 300;
}
public final int finalIntValue = 3;
public int intValue = 1;
public static void main(String[] args) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void speak() {
int a = 10;
int b = 20;
int c = a + b;
}
}
复制代码
反编译下:java -v -p Max.class > Max.txt
,加 -p
是由于私有属性不加这个不先是,最后 > Max.txt
是把反编译出来的字节码写入到txt文件中,这样方便看
Classfile /Users/zbzbgo/Desktop/Max.class
// 字节码参数
Last modified 2020-6-20; size 746 bytes
MD5 checksum 5c6bccb4965bf8e6408c8e3ef8bca862
Compiled from "max.java"
// 包名+类名
public class com.bloodcrown.bw.Max
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER // 访问限定符
// 常量池,里面其实都是字符串,须要解析时加载
Constant pool:
#1 = Methodref #11.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #10.#31 // com/bloodcrown/bw/Max.finalIntValue:I
#3 = Fieldref #10.#32 // com/bloodcrown/bw/Max.intValue:I
#4 = Long 100000l
#6 = Methodref #33.#34 // java/lang/Thread.sleep:(J)V
#7 = Class #35 // java/lang/InterruptedException
#8 = Methodref #7.#36 // java/lang/InterruptedException.printStackTrace:()V
#9 = Fieldref #10.#37 // com/bloodcrown/bw/Max.staticIntValue:I
#10 = Class #38 // com/bloodcrown/bw/Max
#11 = Class #39 // java/lang/Object
#12 = Utf8 staticIntValue
#13 = Utf8 I
#14 = Utf8 finalIntValue
#15 = Utf8 ConstantValue
#16 = Integer 3
#17 = Utf8 intValue
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 StackMapTable
#25 = Class #35 // java/lang/InterruptedException
#26 = Utf8 speak
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 max.java
#30 = NameAndType #18:#19 // "<init>":()V
#31 = NameAndType #14:#13 // finalIntValue:I
#32 = NameAndType #17:#13 // intValue:I
#33 = Class #40 // java/lang/Thread
#34 = NameAndType #41:#42 // sleep:(J)V
#35 = Utf8 java/lang/InterruptedException
#36 = NameAndType #43:#19 // printStackTrace:()V
#37 = NameAndType #12:#13 // staticIntValue:I
#38 = Utf8 com/bloodcrown/bw/Max
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Thread
#41 = Utf8 sleep
#42 = Utf8 (J)V
#43 = Utf8 printStackTrace
{
// 成员变量信息
public static int staticIntValue;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public final int finalIntValue;
descriptor: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 3
public int intValue;
descriptor: I
flags: ACC_PUBLIC
// 默认的构造方法
public com.bloodcrown.bw.Max();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_3
6: putfield #2 // Field finalIntValue:I
9: aload_0
10: iconst_1
11: putfield #3 // Field intValue:I
14: return
LineNumberTable:
line 8: 0
line 17: 4
line 19: 9
// 方法信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc2_w #4 // long 100000l
3: invokestatic #6 // Method java/lang/Thread.sleep:(J)V
6: goto 14
9: astore_1
10: aload_1
11: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
14: return
Exception table:
from to target type
0 6 9 Class java/lang/InterruptedException
LineNumberTable:
line 23: 0
line 26: 6
line 24: 9
line 25: 10
line 27: 14
StackMapTable: number_of_entries = 2
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
public void speak();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 30: 0
line 31: 3
line 32: 6
line 33: 10
// 类的初始化方法 clinit(C++) 方法,编译时自动生成的。注意不是默认的构造函数,静态代码块和静态属性赋值,写代码时谁写在前面,谁的赋值就在前面,注意有前后顺序
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #9 // Field staticIntValue:I
5: sipush 300
8: putstatic #9 // Field staticIntValue:I
11: return
LineNumberTable:
line 10: 0
line 13: 5
line 14: 11
}
SourceFile: "max.java"
复制代码
你们看个意思,方法区储存的类信息其实和字节码差不了对少
图里的字符串常量池不方法区里,JVM 规范虽然是这样的,可是具体的虚拟机实现都会有变化的,具体看实际
存储位置:
方法区数据结构:
Map<Classload,Map<String,Class>>
这样的数据结构同样。系统类加载器是 static 的,每一个进程的系统类加载器是都是不一样的对象,对应的方法区空间也不同,因此他们之间加载的类信息是不能共享的,比如A进程加载Dog的1.3版本,B进程加载Dog的1.0版本,这并不影响进程A和B之间的独立运行classload 和方法区class相互记录:
方法区默认大小是20.75M
,android 上是20.79M
,最大值是一个无限大的数,可是其实是物理内存的上限,超过这个上限同样也会 OOM
jinfo -flag MetaspaceSize 74290
命令能够查看方法区大小-XX:MetaspaceSize=100m
在 VM options 设置方法区大小,方法区的最大值通常不动,咱们调节的都是方法区一上来的默认大小方法区咱们能够设置固定大小,也能够设定动态调整,默认是动态调整的,一旦方法区满了就会触发 Full GC,GC 会去回收方法区中不被用到的 class 类信息,何时 class 类信息不被用到呢。就是加载 class 类信息的 classload 销毁了,那么这个这个 classload 加载的全部的 class 类信息都无用了,能够被回收了
Full GC 要是发现仍是不能方法区内存需求,就会扩大方法区的内存大小,可是一次确定不会增长不少,估计就是几M 的事。这里就有个问题了,要是咱们一上来设置的太小,咱们加载的类又不少,那会方法区就会频繁的触发 Full GC,这是一个能够优化的点
常量池这东西咱们应该清楚的,即使网上的资料,看那些文字描述基本看不懂,可是这不是咱们不去理解的理由,方法区和类加载机制是紧密联系的,因此方法区的一切咱们都应该知道
常量池这块挺复杂的:
classfile 里面的叫常量池
方法区里面的叫运行时常量池
他俩之间的关系:
字节码文件 classfile 被 classload 加载到内存以后,字节码文件中的常量池就变成运行时常量池了
必定要搞清楚他俩是什么,我一开始看这里的时候头疼啊,一会常量池,一会运行时常量池,我都怀疑网上的文章是否是写错了,去看《深刻理解java虚拟机》这本书又写的不连贯,写的莫名其妙,看着描述的文字不少,但就是没说明白这是啥
其实他俩的关系就是一句话:文件里的字节码常量池加载到内存以后就是运行时常量池了。学习他俩其实把字节码的常量池搞明白就好了,剩下那个天然就懂了
先看看常量池的字节码吧,用的是前面反编译出来的字节码
Constant pool:
#1 = Methodref #11.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #10.#31 // com/bloodcrown/bw/Max.finalIntValue:I
#3 = Fieldref #10.#32 // com/bloodcrown/bw/Max.intValue:I
#4 = Long 100000l
#6 = Methodref #33.#34 // java/lang/Thread.sleep:(J)V
#7 = Class #35 // java/lang/InterruptedException
#8 = Methodref #7.#36 // java/lang/InterruptedException.printStackTrace:()V
#9 = Fieldref #10.#37 // com/bloodcrown/bw/Max.staticIntValue:I
#10 = Class #38 // com/bloodcrown/bw/Max
#11 = Class #39 // java/lang/Object
#12 = Utf8 staticIntValue
#13 = Utf8 I
#14 = Utf8 finalIntValue
#15 = Utf8 ConstantValue
#16 = Integer 3
#17 = Utf8 intValue
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 StackMapTable
#25 = Class #35 // java/lang/InterruptedException
#26 = Utf8 speak
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 max.java
#30 = NameAndType #18:#19 // "<init>":()V
#31 = NameAndType #14:#13 // finalIntValue:I
#32 = NameAndType #17:#13 // intValue:I
#33 = Class #40 // java/lang/Thread
#34 = NameAndType #41:#42 // sleep:(J)V
#35 = Utf8 java/lang/InterruptedException
#36 = NameAndType #43:#19 // printStackTrace:()V
#37 = NameAndType #12:#13 // staticIntValue:I
#38 = Utf8 com/bloodcrown/bw/Max
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Thread
#41 = Utf8 sleep
#42 = Utf8 (J)V
#43 = Utf8 printStackTrace
复制代码
你们看看这常量池的字节码感受像啥,像不像list列表,有int索引,数据一行一行很规则
没错,常量池本质就是一张表,虚拟机根据字节码指令来这张表找到和这个类关联的类、方法名、参数类型、字面量等信息
你们注意啊,常量池里面存的都是字符串,为啥?class文件不是字符串能是什么,就算加载到内存里对应的仍是字符串,只有类加载器根据这么字符串信息把这个类加载出来,这些字符串才有意思
因此像:java/lang/Object、java/lang/Thread、com/bloodcrown/bw/Max.intValue
这些,咱们知道其实他们是什么,是类的类型,接口,方法,包名等等,常量池中的这些字符串就叫作符号引用
可是单单字符串咱们是无法用的,必需要类加载器把这些字符串描述的类都正式在内存中加载出来才有意义。这个过程在类加载机制中的解析环节,会把常量池中这些字符串转换成加载事后的class类型信息在方法区中的地址,这个地址叫作:直接引用
从常量池->运行时常量池=从符号引用->直接引用,说白了就是把字节码中描述信息的字符串都正式加载成出来,生成对应的类、接口、注解等等可使用的信息
总结一下,常量池的内容包括:
数值量:
好比 int=10 中的 10字符串
类引用
字段引用
方法引用
那为何要涉及一个常量池出来呢,既然都是字符串,咱们写在用的地方不就行了嘛~距网上的解释,JVM 官方是考虑到有的字符串会重复被使用,为了尽量减小class文件体积。另外一个考虑是,每一个类里面其实都涉及其余类,若是不用字符串代替class自己涉及到的其余的类型信息,那么就要把这些涉及到的类型信息都写在同一个class文件里,那么这回形成灾难性的后果,class文件大到难以接收,文件结构也会变得不可预期,大量的class文件中都会有重复信息,甚至涉及到不一样类型的版本,这样就无法搞了
前文说过,方法区是 JVM 规范的称为,只是一种建议规范,而且尚未作强制限制。具体设计成什么样,还得看看方法区的具体实现,永久带和元空间就是方法区的具体实现,区别很大
永久带这东西只有 hotspot 虚拟机再 JDK1.6 以前才有,其余虚拟机像 JRockit、J9 人家压根就不用,而是用本身的实现:元空间
永久代:
设计在JVM内存中,和堆内存连续的一块内存中,储存类元信息、字符串常理池、静态数据,由于有JVM虚拟机单个实例的内存限制,永久带会较多概率触发 FullGC,而且垃圾回收的效率、性能还低,类加载的多还会出现 OOM,尤为是后台程序加载的模块多了
元空间:
设计在本地内存 native memory,没有了JVM虚拟机内存限制,OOM 基本就杜绝了,FullGC 触发的概率较低。类元信息随着方法区中的迁移,改在本地内存中保存,字符串常量池和静态数据则保存在堆内存中
JDK 1.6 以前方法区采用永久带,JDK1.8 开始,方法区换用元空间,JDK1.7 在其中起过分
方法区不是没有GC的,只是规范没强制有,具体看方法区实现的心情了,固然元空间确定是有的
你们须要知道方法区不足会引发 GC,而这个 GC 是 FullGC,性能消耗很大。方法区GC回收的其实就是运行时常量池里的东西
类元信息的回收条件很是苛刻,必须同时知足下面全部条件:
该类 类型的全部实例都被回收了
加载该类的类加载器已经被回收了
该类对应的在堆内存中的class映射对象,没有被任何地方引用
蛋疼不,第三条有点说到的地方,咱们反射时但是大量会用到class的,因此反射可能会形成类元信息的内存泄露
正是由于方法区回收的条件众多且必须一一知足又和堆内存息息相关,因此才会触发最重量家的 FullGC,把堆内存总体过一遍。回收的内容又没有堆内存那样多,可能有的人以为这点内存其实不必回收,可是之前Sun公司由于方法区没有GC回收问题而引发过很多重量级bug,因此方法区的回收是一件必须的事情,可是又是一件费力不讨好,还性能消耗大的事,因此在后端开发时,方法区初始值通常都尽可能设置的大一些,为了就是减小方法区GC
大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP,及其 OSGI 这类频繁自定义类加载器的场景中,一般都是须要 JVM 具有类型卸载的能力,以保证不会对方法区形成多大的压力
这是从别人那里看过来的,想了想应该是正确的
一个称之为类数据共享(CDS)的特性自HotspotJVM 5.0开始被引进。在安装JVM期间,安装器加载一系列的Java核心类(如rt.jar)到一个通过映射过的内存区进行共享存档。CDS减小了加载这些类的时间从而提高了JVM的启动速度,同时容许这些类在不一样的JVM实例之间共享。这大大减小了内存碎片
说完类加载器和方法区我就能够来看对象是怎么在堆内存存储的了
就是这个样子:
对象在堆内存中的存储结构:
对象头
对象实体
对齐方式
其中详细的指针看下图:
这是一道常问的面试题,咱们只考虑通常对象,对象实体是空的,Mark Word 占64位8个字节
类型指针默认是8个字节的,可是在开启指针压缩时会变成4个字节,JDK8 是默认开启的
参考对齐方式,因此一个空对象默认占用内存大小是12个字节,算上对齐方法的化是16个字节
学习JVM的话,oop-klass 模型永远是一个绕不过去话题。咱们都知道 HotSpot VM 几乎能够说是纯 C++ 语言编写的 Java 虚拟机,那么 Java 的对象模型和 C++ 的对象模型之间究竟有什么关系呢?这个问题简单回答就是 oop-kclass 二分对象模型
具体来讲:HotSpot 虚拟机采用 Klass-OOP
模型来存储 java 对象,OOP(Ordinary Object Pointer)指的是普通对象指针,是 java 部分的。而 Klass 用来描述对象实例的具体类型,是 C++ 的。oop 在堆内存中,就是对象头,Kclass 是方法区类元数据的数据模型
至于为啥要搞这一个东西呢,我从网上找来的,这个应该是解释的最正确的了吧
事实上HotSpot底层究竟怎么表示一个Java对象这个问题归根结底就是C++怎么表述一个Java对象。有一个朴素的实现方案就是将每个Java对象都影射为一个对等的C++对象,然而这么作确实是太朴素了,它有一个严重的弊端就是若是这样作的话那么就不得不为每个Java对象都保存一份VTable(虚函数表),由于C++的多态实现就是为每个对象都保留一份VTable。这是很浪费空间的,因此HotSpot设计者采用了oop-class二分模型来表述一个Java对象。其中这里的oop表示Ordianry Object Pointer(普通对象指针,注意可不是object-oriented programming),它用来表示对象的实例信息,看起来像个指针其实是藏在指针里的对象。而 klass 则包含 元数据和方法信息,用来描述 Java 类
那么为什么要设计这样一个一分为二的对象模型呢?这是由于HotSopt JVM的设计者不想让每一个对象中都含有一个vtable(虚函数表),因此就把对象模型拆成klass和oop,其中oop中不含有任何虚函数,而klass就含有虚函数表,能够进行method dispatch。这个模型实际上是参照的 Strongtalk VM 底层的对象模型
总结来讲:就是对象数据在 java 和 C++ 2种语言直接联系(我的理解)
还记的对象的对象头码,Mark Word + Kclass Word,oop-klass 模型的 oop 就是这个对象头。对象的建立这一步中就专门有生成分配设置对象头这一步
oop 的具体类型有:instanceOopDesc(实例对象)/arrayOopDesc(数组对象)
,oop 是 方法区 C++ 类元信息在 java 数据,模型中的映射
instanceOopDesc 继承自 oopDesc,看看 oopDesc 的源码:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
...
}
复制代码
_mark 是对象头里的 Mark World,_klass 和 _compressed_klass 都是方法区 Kclass 对象的引用地址,_compressed_klass 是开启指针压缩以后的内存地址
instanceKlass 是类元信息的具体数据模型,具体不解释了
class InstanceKlass: public Klass {
...
enum ClassState {
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
...
}
复制代码
Java 虚拟机是如何经过栈帧中的对象引用找到对应的对象实例的,看图:
通常介绍 oop-Kclass 的到这里就结束了,可是这里还有一个重要角色,也是不少人搞不明白的,就是 CLASS
对象,咱们知道每一个类在堆内存中都有一个对应的class对象,尤为是反射的时候
// 这个class就是咱们要说的东西
Class<Dog> dogClass = Dog.class;
复制代码
具体来讲仍是由于 C++ 和 java 语言的差别,Kclass 对象在方法区内,而方法区又不在堆内存而是在 Native Memory 本地内存中,Native Memory 不容许咱们直接访问,因此为了便于衔接JVM内存结构,因此搞了一个句柄出来,对 JVM堆内存中的 class 对象就是 Kclass 对象的句柄,class 并能够访问 Kclass ,加之反射咱们须要帮助JVM解析类的结构,因此就有了堆内存里的class对象
class对象是何时生成的呢,是和 Kclass 对象一块儿生成的,类加载机制在加载的时候在方法区生成一个 Kclass 对象,那么就会在类加载器所属的 JVM 堆内存中同步生成一个 class 对象
Class 自己也是一个 java 类型,其中基本都是 Native 方法
Class{
public native Field getDeclaredField(String name) throws NoSuchFieldException;
private native Field[] getPublicDeclaredFields();
}
复制代码
值得注意的是,class 没有直接指向 Kclass,而是 Kclass 内部指向了 class,咱们需找 class 的路线是:oop(对象头)->kclass(方法区)->class(堆内存)
网上有句话:栈是运行时结构,堆是储存结构。栈是管程序如何执行,怎么处理数据的。这句话基本道尽了 java 栈内存的做用,栈就是管运行的
栈内存同程序计数器,本地方法栈一块儿是线程私有的,有一个线程 new 出来,就会开辟一块栈内存出来
栈内存采用栈
数据结构,其内部保存的是一个一个栈帧,一个栈帧对应一个java方法。栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器,栈内存有OOM,可是没有GC的需求,空间不够了直接OOM
栈内存的做用就是主管程序的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回,生命周期和线程一致
JVM 容许栈内存的大小是固定的或者是动态调整的,这个看你具体设置的JVM 参数了
JDK8 时 栈默认大小1M,最小160K,小于 160K直接报错
还有一个参数,栈深度,就是方法调用链的深度,用递归来举例 10086,就是递归10086此调用本身这个方法了
栈内存不够用了也是会 OOM 的:
栈内存是的固定话,会抛 stackOverFlowError
栈内存是动态的话,会抛 outOfMemoryError
JVM 参数:
Xss:
栈内存值-Xss256k
记住栈内存只有这么一个 JVM 参数,当最大值用就行啦
默认状况下咱们搞一个 OOM 看看:
public class Max {
public static int index = 0;
public static void main(String[] args) {
index++;
System.out.println(index);
main(args);
}
}
复制代码
能够看到抛出的哪一个异常,这对于JVM调优是很是重要的,另外能够看到栈深度大概是1000左右,默认1M的栈大小,这么一算的话一个栈帧大概要占200个字节左右,因此写递归时必定要注意,稍不注意栈内存就溢出了,别堆内存没事,栈到先顶不住了...
打印栈内存大小: XX:+PrintFlagsFinal -version | grep ThreadStack
➜ ~ java -XX:+PrintFlagsFinal -version | grep ThreadStack
intx CompilerThreadStackSize = 0 {pd product}
intx ThreadStackSize = 1024 {pd product}
intx VMThreadStackSize = 1024 {pd product}
复制代码
特别须要注意的是,GC 垃圾回收机制不会涉及到栈内存,栈内存相对于堆内存是不可见的,咱们常说的 GC 只会对堆内存起做用,甚至方法区都有本身专门的垃圾回收器
栈内存是 stack 栈这种先入后出的数据结构,每个栈帧表明的是一个方法,方法执行时所须要,所产生的数据都包含在栈帧中,栈帧就能够当作方法执行在内存中的样子
其实这几句话我都不想写的,能看我这篇文章的,栈和堆基本都知道
一个栈帧表明一个方法,方法在运行过程当中会有传入的参数,本身建立的属性,执行结果等等的东西,因此栈帧的结构比较复杂,由于要分门别类储存这些东西:
局部变量表
操做数栈
动态连接
运行时常量池中方法的引用返回地址
附加信息
先说 动态连接
这个东西,方法的字节码存在哪?固然是方法区里面啦,一个类的方法确定会有不少地方都用到啊,不能每个地方都保存一份方法运行的字节码啊,那内存可就搂不住啦,因此栈帧里面必须有一个属性要能在方法运行之时把方法的字节码拿到交给执行引擎去执行,动态连接干的就是指向方法区Kclass对象运行时常量池中该方法的引用
动态连接这里有个点,静态连接和动态连接,涉及黑科技的东西你们要知道:
静态连接:
当一个字节码被装载进JVM内部时,若是被调用的目标方法在编译期可知,且运行期保持不变时,在这种状况下将调用方法的符号引用转换位直接引用,这个成为静态连接。就是一旦编译就不变了动态连接:
若是被调用的目标方法在编译期没法肯定下来,只能在程序运行时将调用方法的符号引用转换位直接引用,因为这种引用转换过程具有动态性,所以被成为动态连接。好比一个方法接受一个接口对象类型的参数,对于方法来讲,编译时怎么知道会具体传什么类型的实现类进来,这个就是动态绑定返回地址,附加信息这2个没必要说,相比你们都能猜的出,重点是局部变量表和操做数栈
局部变量表
其实也好理解,她就是一个数组,保存方法接收到的参数和在方法运行时生成的对象的引用,对象自己仍是保存在堆内存里面的,在方法运行的时刻会拷贝赋值一份到线程所属的工做内存中,栈帧所属的栈内存也在工做内存中。局部变量表的基本单位是 slot,一个slot占4个字节,8个字节的基本数据类型占2个slot,引用类型的指针占2个slot。局部变量表是栈帧中占据内存空间最大的部分,一个对象引用就要占8个字节出去
操做数栈
在方法运行过程当中,根据字节码指令,往栈中写入活提取数据,push 入栈、pop 出栈,保存方法运行过程当中产生的中间数据和临时数据,是彻底为了方法执行服务的,其保存的值没有最终的实际意义,是位字节码指令执行服务的。好比让 Dog a、Dog b 交换对象实例的操做,中间产生的tem这个临时变量的引用,就会存储在操做数栈里
操做数栈的字节码分析按理说淂写一遍的,要不就是不到位,可是这里我就真的不想写了,B站上讲JVM的都会把这块说的倒背如流,留给你们当个思考题吧,不能什么都靠别人不是,本身动手丰衣足食...
这个问题绝对会难倒绝大部分人的,由于我就是 ︿( ̄︶ ̄)︿
栈内存中值得咱们深刻思考的是栈和工做内存的关系以及执行状况,JMM 不了解的请看:JMM和底层实现原理
JMM 是什么:MM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工做方式
。JMM规定每一个线程都有本身的工做内存,工做内存是什么:工做内存是寄存器和高速缓存的抽象
。说栈就是工做内存绝对是错误的,栈虽然默认是1M的,很小,但每一个进程容许建立的现场数最多能够达5000个,栈要是不设置小一点,内存装的下嘛~,栈是运行在线程所属工做内存的,可是会随着硬件吃紧缓存到内存中
栈表明的是方法的执行,须要快速高效,而内存相对于CPU内部的寄存器和缓存来讲要慢上百倍,这是不能接收的,因此每一个线程才会在cpu缓存上有本身的一块空间也就是工做内存。可是cpu缓存很小,桌面CPU AMD R3600X L3才32M,一个线程就要占1M走,那线程多了cpu也搂不住啊。移动cpu 高通家的晓龙865 3级缓存在4M,明显不够用
因此线程在获取cpu时间片时,其工做内存必定会在cpu L3中运行,一旦失去cpu时间片仍是会保存在cpu缓存中的,可是一旦cpu缓存不够用了,cpu缓存会执行清理工做,这时会把失去cpu时间片的线程工做内存移至内存中缓存,用时再加载会cpu缓存中,因此cpu缓存越大多线程性能越好,线程切换更少
还有另一个佐证:栈帧中的局部变量表也是垃圾回收机制重要的根节点。可见垃圾收集器是能访问到栈内存的,栈内存要是常驻cpu告诉缓存中的话,垃圾回收器是访问不到的
这里我迷糊了很久,搞清这个问题我是花了不少功夫的...
写到这里我终于找到了明确的答案:
因为操做数是存储在内存中的,所以会频繁的执行内存读/写操做,必然会影响执行速度。纬二路解决这个问题,Hotspot 虚拟机的设计者们提出了栈顶缓存技术,讲栈顶元素也就是立刻要执行的栈帧(方法)缓存在cpu寄存器中,以此下降对内存的读写次数,提高执行引擎的效率
结合这段话,我是咱们能够猜想下,栈顶的栈帧放入cpu缓存的优先级确定是:L1->L2->L3 的吗,我估计即使线程失去cpu时间片,可能还会缓存该线程下一个栈帧到L3中,估计会结合java的锁升级机制,经过锁能够知道哪一个线程将来执行机会大
仍是有必要说一下,找到一段经典解析:
当某个线程调用一个本地方法时,她就进入了一个全新的而且再也不受虚拟机限制的世界,他和虚拟机拥有一样的权限
- 本地方法经过本地方法接口来访问虚拟机内存的运行时数据区
- 能够直接使用cpu中的寄存器
- 能够直接从本地内存的堆中分配任意数量的内存
并非全部的JVM都支持本地方法栈,java 虚拟机规范并无明确规定本地方法栈使用的语言,具体实现等。若是JVM产品不打算直接native方法,能够没有本地方法站的
hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一了
栈内存使执行 java 方法的,本地方法栈是执行 C/C++ 方法的,知道这么多就行啦 o( ̄ヘ ̄o#) 等你研究 C++ 时能够再深刻理解