再看 JVM(1)

那些年翻来覆去折腾 JVM

这不是我第一次学习 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 发展历程


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,咱们固然要学习 JVM 的3大组成部分了:类加载器运行时数据区执行引擎

可是咱们以前都是每一个点学每一个点的,从没有站在 JVM 整体的角度上串起来看,这就是此次我要展现给你们的,从整体上看,从整体上理解,其实每一个点都是相互关联的

1. JVM 和硬件紧密相联,站在全局的角度去理解 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 10istore_1 这些你们认识吗,看着是否是和汇编多少优势像啊,这就是 JVM 本身设计的,专属于本身的指令集。因此说 JVM 更像是一个虚拟的计算机,只要有一个硬件设备,上面安装有一个内核,JVM 就能顺利的运行,甚至不须要完整的操做系统支持

什么是虚拟机:就是一套用来执行特定虚拟指令的软件。好比要在 mac 上跑 wins 就须要一个虚拟机,要不 mac 怎么认识 X86 指令呢...

2. JVM 已经超脱 java 了

若是说 java 是跨平台的语言:

那么 JVM 就是跨语言的平台:

愈来愈多的语言选择运行在 JVM 环境上,无论这个语言怎么写的,主要该语言的编译器把代码最终编译成 .class 标准字节码文件,那么就都能在 JVM 上运行。像上图中的,这些永远均可以在 JVM 上运行

JVM 已经变成一个生态了,这不能不让咱们去思考,我以为你们看到这里都思考一下是有好处的,感慨下,这就是一种趋势

整体来讲JVM就一句话:从软件层面屏蔽不一样操做系统在底层硬件个指令上的区别,也包括顶层的高级语言

3. 理解学习 JVM 的好处

这是 java 程序的结构,JVM 提供最底层运行支持,使用 java 提供的 API 开发了不少框架,咱们使用这些框架开发出最终的服务,app等

JVM 是最终承载咱们代码的地方,你的服务运行的好很差,卡不卡不都看 JVM 的反馈嘛。单从性能优化的角度看,咱们都得对最底层的知识体系有足够了解

懂得 JVM 内存结构,工做机制,是设计高扩展性应用和优化性能的基础,阻碍程序运行的永远是咱们对硬件使用的效率,对硬件使用效率的高低决定了咱们程序执行的效率

下面这些问题我想你们都会遇到吧:

  • 线上系统忽然卡死,OOM
  • 内存抖动
  • 线上 GC 问题,无从下手
  • 新项目上线,JVM 参数配置一脸懵逼
  • 面试 JVM 直接被拍晕在地上,JVM 如何调优,如何解决 GC,OOM

JVM 玩不转别想吧上面这些搞顺溜了...

就算是否是后台服务的,你搞 android 或者其余就没有 内存抖动 的问题啦,不可能的,只要你语言用的 java 或者跑在 JVM 上,这 JVM 都是你逃不过去的

了解 JVM

知道了这些点以后,有助于咱们理解后面 JVM 的内容

1. 再次理解什么是JVM

这是摘抄过来的一句话,不用再解释了,你们仔细揣摩

虚拟机的概念是相对于物理机而言的,这两种机器都有执行代码的能力。
物理机的执行引擎是直接创建在硬件处理器、物理寄存器、指令集和操做系统层面的
而虚拟机的执行引擎是本身实现的,所以能够自定义指令集和执行引擎的结构体系
并且能够执行那些不能被硬件直接支持的指令

在不一样的“虚拟机”实现里面,执行引擎在执行JAVA代码的时候有两种方式:
1. 解析实行(经过解释器执行)
2. 和编译执行(经过即时编译器编译成本地代码执行)
复制代码

2. Java进程之间以及跟JVM关系

java程序是跑在JVM上的,严格来说,是跑在JVM实例上的,一个JVM实例其实就是JVM跑起来的进程,两者合起来称之为一个JAVA进程

各个JVM实例之间是相互隔离的

  • 一个进程能够拥有多个线程
  • 一个程序能够有多个进程(屡次执行,也能够没有进程,不执行)
  • 一台机器上能够有多个JVM实例(也能够没有JVM实例)
  • 进程是指一段正在执行的程序
  • 线程是程序执行的最小单位
  • 经过屡次执行一个程序能够有多个进程,经过调用一个进程能够有多个程序

程序运行时,会首先创建一个JVM实例----------因此说,JVM实例是多个的,每一个运行的程序对应一个JVM实例。每一个java程序都运行在一个单独的JVM实例上,(new建立实例,存放在堆空间),因此说一个java程序的多个线程,共享堆内存

总的来讲,操做系统的执行单元是进程,每个JVM实例就是一个进程,而在该实例上运行的主程序是一个主线程(能够当作一个轻量级的进程),该程序下还存在不少线程

还有一个 JVM 实例对应一个 Runtime 对象,咱们能够从该 Runtime 对象中获取一些参数,好比堆内存的初始值和最大值

3. java 也是起源自小程序

有意思的是,java 最先是为了在 IE3 浏览器中执行 java applets,原来早先 java 也是小程序出身,可是谁让后来 java 火了呢...

4. Taobao JVM

阿里很NB,本身基于 OpenJDK 深度定制了本身的 alibabaJDK,而且定制了本身的 Taobao JVM,很厉害的

其特色:

  1. 提出了 GCIH 技术,把生命周期较长的对象放到堆外了,提升了 GC 效率,下降了 GC 频率
  2. GCIH 中的对象能够在多个 JVM 实例中相互共享
  3. 使用 crc32 指令下降 JNI 开销
  4. 针对大数据场景的 ZenGC

缺点是高度依赖 Intel cpu,目前在天猫,淘宝上应用,全面替代 Oracle 官方 JVM

5. JVM 和线程

线程是一个程序里的运行单元,JVM 容许一个应用有多个线程并行执行

在 Hotspot 虚拟机中,每一个线程都与操做系统的本地线程直接映射。当一个 java 线程准备好执行以后,一个操做系统的本地线程也会同时被建立。java 线程终止后,本地线程也会被回收

操做修通负责把全部线程安排哦调度到任何一个可用的 CPU 上去执行,一旦本地线程初始化完成,就会调用 java 线程中的 run()

一个 JVM 实例里有不少后台线程:

  • 虚拟机线程: 这种线程的操做是须要JVM达到安全点才会出现,这种线程的执行类型包括"stop-the-wrold"的垃圾收集,线程栈回收,线程挂起,偏向锁撤销
  • 周期任务线程: 这种线程是时间周期事件的体现,好比中断
  • GC线程
  • 编译线程: 把字节码编译成本地代码
  • 信号调度线程: 这种线程接收信号并发送给JVM

JVM 命令和运行参数调整

IDE 配置 JVM 参数

只须要在 VM options 里面写设置便可,好比:

-XX:MetaspaceSize=100m
复制代码

MetaspaceSize 是方法区的大小,这样写就行,想改哪一个就用对应的英文单词好了

JVM 参数简写问题

后面你们会看到诸如:-Xms 这样的JVM参数,一看就知道是简写,其实 -Xms = -XX:InitialHeapSize,你们知道就行,对照着就知道了,别2个都碰到了不知道啥意思

jps 命令

能够查看进程信息

  • 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
复制代码

jinfo 命令

能够打印出想看的JVM参数信息,想看那个参数后面跟英文单词和进程ID就行啦

// 打印信息,74290 是进程ID,能够用上面 jps -l 命令查看
➜  ~ jinfo -flag MetaspaceSize 74290
// JVM 配置
-XX:MetaspaceSize=21807104
复制代码

PrintGCDetails 打印堆栈信息

-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

堆内存JVM参数

  • -XX:UseTLAB - TLAB 线程专属空间大小
  • Xmx - 堆内存最大值,默认=物理内存的 1/4
  • Xms500m - 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/10
  • Xmn - 新生代最大值,通常不动,通常都用比例,这个写了比例就不算数了
  • -XX:+UseAdaptiveSizePolicy - 自适应内存分配策略,-号是取消设置,+号是采用设置,这个其实不起做用的...
  • jinfo -flag NewRatio 进程ID - 打印新生代老年代比例
  • jinfo -flag SurvivorRatio 进程ID - 打印新生代内比例

栈内存JVM参数

  • -Xss - 栈内存值,只有这个一个参数,能够理解为最大值
  • -Xss900k - VM options 设置写法

JVM 生命周期

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

JVM 总体结构

这里咱们以 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个大的步奏:

  • 加载
    • 引导类加载器
    • 扩展类加载器
    • 系统类加载器
  • 连接
    • 验证验证
    • 准备
    • 解析
  • 初始化

类加载器系统和方法区紧密相联,毕竟类加载出来的东西是放在方法区的,可是这其中仍是又不少讲头的。字节码、常量池、运行时常量池、符号引用转直接引用我都放在后面方法区那部分了,你们像了解请转到后面那里

1. 加载

经过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构对象,并在内存中生成对应的Java.lang.class对象

加载类的方式其实不少,你们必定要清楚,黑科技都是借助这个的:

  • 从本地系统直接加载
  • 网络获取,场景:web Applet
  • 从 zip、jar、war 等压缩包中读取
  • 运行时动态生成,好比:动态代理技术
  • 由其余文件生成,好比:JSP 应用
  • 从数据库中获取 class 文件
  • 从加密文件中获取

你们必定要清除啊,android 的黑科技那个没用带这个呢...

2. 连接

连接里面3个小的步奏:验证、准备、解析

  • 校验: 检查导入类或接口的二进制数据的正确性:(文件格式验证,元数据验证,字节码验证,符号引用验证)
  • 准备: 给类的静态变量分配并初始化存储空间,既然都分配属性的内存空间了,那么确定就有对象了,因此加载那一步在方法区建立出运行时数据结构对象确定是没错的,要不到这里解释不了。我曾经对什么时候生成方法区数据对象产生怀疑
  • 解析: 将常量池中的符号引用转成直接引用

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() 函数

4. 初始化方法执行时机

java 类是咱们何时用,何时才加载,这一点你们最好内心有数,有时候有些 BUG 就是由于类虽然加载了可是初始化方法没有自行,你认为在那个时刻类确定会初始化,但实际上她没有

类的使用能够分主动使用、被动使用,主动使用时会初始化该类,被动使用时不会初始化该类。类加载的3部中,初始化方法不是必须顺着加载、连接这2部执行完后执行的,而是能够自由决定何时用

类主动使用的状况:

  • 经过new关键字、反射、clone、反序列化机制实例化对象
  • 调用类的静态方法时
  • 使用类的静态字段或对其赋值时
  • 经过反射调用类的方法时
  • 初始化该类的子类时(初始化子类前其父类必须已经被初始化)
  • JVM启动时被标记为启动类的类(简单理解为具备main方法的类)

5. 类加载器介绍

上面说过类加载能够分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 } 复制代码

6. 自定义类加载器

固然类加载器也能够自定义的

通常这几种状况下会考虑自定义类加载器:

  • 防止源码泄露 - 对字节码加密,类加载时解密,预防反编译篡改
  • 扩展加载源 - 插件化,热修复
  • 修改类的加载方法
  • 隔离加载类- 中间件,中间件和应用模块是隔离的,把类加载到不一样环境当中,相互之间不冲突,防止不一样依赖之间包名类名相同的类的冲突

咱们能够选择继承 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

7. 双亲委派机制

咱们先回国头来再看一遍 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 被篡改

方法区

1. 基本介绍

方法区这个名称是 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,这个下面会仔细说一下的

这是代中文对照的图:

堆、栈、元空间的相互关系:

2. 从字节码入手

其实咱们从反编译下字节码就知道怎么回事了,字节码文件会加载到方法区,也就是数据储存结构有些变化,可是东西仍是字节码里面的东西

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"
复制代码

你们看个意思,方法区储存的类信息其实和字节码差不了对少

3. 方法区的储存结构

图里的字符串常量池不方法区里,JVM 规范虽然是这样的,可是具体的虚拟机实现都会有变化的,具体看实际

  • 存储位置:
    方法区不在用户进程的内存中,而是在本地内存 native memory 中,这样的好处是加载过多的类也不会形成堆内存的OOM了
  • 方法区数据结构:
    操做系统中,能够有多个 JVM 实例,看着每一个JVM都有本身的方法区。但实际上在 native memory 内存中,方法区只有一块。每一个类加载器在方法区均可以申请一块本身的空间,类加载器相互之间不能访问,每一个类加载器本身的空间内,给每个类信息都分配一块空间,就像 Map<Classload,Map<String,Class>> 这样的数据结构同样。系统类加载器是 static 的,每一个进程的系统类加载器是都是不一样的对象,对应的方法区空间也不同,因此他们之间加载的类信息是不能共享的,比如A进程加载Dog的1.3版本,B进程加载Dog的1.0版本,这并不影响进程A和B之间的独立运行
  • classload 和方法区class相互记录:
    方法区里的class类信息对象会记录本身是哪一个类加载器加载的,类加载器同样会记录本身加载过哪些类信息

4. 方法区的 OOM

方法区默认大小是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,这是一个能够优化的点

5. 理解什么是常量池

常量池这东西咱们应该清楚的,即使网上的资料,看那些文字描述基本看不懂,可是这不是咱们不去理解的理由,方法区和类加载机制是紧密联系的,因此方法区的一切咱们都应该知道

常量池这块挺复杂的:

  • 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文件中都会有重复信息,甚至涉及到不一样类型的版本,这样就无法搞了

6. JDK1.8 方法区变化

前文说过,方法区是 JVM 规范的称为,只是一种建议规范,而且尚未作强制限制。具体设计成什么样,还得看看方法区的具体实现,永久带和元空间就是方法区的具体实现,区别很大

永久带这东西只有 hotspot 虚拟机再 JDK1.6 以前才有,其余虚拟机像 JRockit、J9 人家压根就不用,而是用本身的实现:元空间

永久代:设计在JVM内存中,和堆内存连续的一块内存中,储存类元信息、字符串常理池、静态数据,由于有JVM虚拟机单个实例的内存限制,永久带会较多概率触发 FullGC,而且垃圾回收的效率、性能还低,类加载的多还会出现 OOM,尤为是后台程序加载的模块多了

元空间:设计在本地内存 native memory,没有了JVM虚拟机内存限制,OOM 基本就杜绝了,FullGC 触发的概率较低。类元信息随着方法区中的迁移,改在本地内存中保存,字符串常量池和静态数据则保存在堆内存中

JDK 1.6 以前方法区采用永久带,JDK1.8 开始,方法区换用元空间,JDK1.7 在其中起过分

7. 方法区的GC

方法区不是没有GC的,只是规范没强制有,具体看方法区实现的心情了,固然元空间确定是有的

你们须要知道方法区不足会引发 GC,而这个 GC 是 FullGC,性能消耗很大。方法区GC回收的其实就是运行时常量池里的东西

类元信息的回收条件很是苛刻,必须同时知足下面全部条件:

  • 该类 类型的全部实例都被回收了
  • 加载该类的类加载器已经被回收了
  • 该类对应的在堆内存中的class映射对象,没有被任何地方引用

蛋疼不,第三条有点说到的地方,咱们反射时但是大量会用到class的,因此反射可能会形成类元信息的内存泄露

正是由于方法区回收的条件众多且必须一一知足又和堆内存息息相关,因此才会触发最重量家的 FullGC,把堆内存总体过一遍。回收的内容又没有堆内存那样多,可能有的人以为这点内存其实不必回收,可是之前Sun公司由于方法区没有GC回收问题而引发过很多重量级bug,因此方法区的回收是一件必须的事情,可是又是一件费力不讨好,还性能消耗大的事,因此在后端开发时,方法区初始值通常都尽可能设置的大一些,为了就是减小方法区GC

大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP,及其 OSGI 这类频繁自定义类加载器的场景中,一般都是须要 JVM 具有类型卸载的能力,以保证不会对方法区形成多大的压力

8. 补充

这是从别人那里看过来的,想了想应该是正确的

一个称之为类数据共享(CDS)的特性自HotspotJVM 5.0开始被引进。在安装JVM期间,安装器加载一系列的Java核心类(如rt.jar)到一个通过映射过的内存区进行共享存档。CDS减小了加载这些类的时间从而提高了JVM的启动速度,同时容许这些类在不一样的JVM实例之间共享。这大大减小了内存碎片

对象在堆内存中的储存结构

1. 储存结构

说完类加载器和方法区我就能够来看对象是怎么在堆内存存储的了

就是这个样子:

对象在堆内存中的存储结构:

  • 对象头
    • Mark Word:也称运行时元数据,一个64位的数字,使用位运算分段保存对象的一些信息,包括:哈希值(对象内存的首地址),GC分代年龄,锁状态,偏向线程ID,偏向时间搓
    • Kclass Word 类型指针:指向该类在方法区对应的类元数据地址(KClass对象)
    • 数组长度:若是这是数组对象的话,惠济路数组的长度
  • 对象实体
    • 注意会先储存父类的属性,内存占用相同的属性会放在一块儿
  • 对齐方式
    • 64位系统JVM默认对8字节对齐,简单说就是必须被8整除,不能被8整除,这里会添加一些大小以实现被8整除

其中详细的指针看下图:

2. 空对象占内存多少

这是一道常问的面试题,咱们只考虑通常对象,对象实体是空的,Mark Word 占64位8个字节

类型指针默认是8个字节的,可是在开启指针压缩时会变成4个字节,JDK8 是默认开启的

参考对齐方式,因此一个空对象默认占用内存大小是12个字节,算上对齐方法的化是16个字节

3. java 中的 oop-klass 模型

学习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++ 时能够再深刻理解

相关文章
相关标签/搜索