在之前的文章 一步步解析java执行内幕 中,比较详细分析了java代码是如何一步一步在jvm中执行的,然而设计的的jvm核心技术点,并未做深入分析,本篇文章将重点分析jvm,涉及到的内容包括jvm内存模型,类加载器,GC回收算法,GC回收器,整体偏向于理论。

本篇文章不适合初学者,适合具有3年以上开发经验的技术人员,欢迎大家一起交流分享,文章若有不足之处,欢迎读者朋友们指出,先感谢。

一  明确jdk,jre和jvm之间关系

下图为官网关于jdk,jre和jvm的架构图,从该架构图,很容易看出三者之间关系:

(1)jdk包含jre,而jre又包含jvm

(2)jdk主要用于开发环境,jre主要用于发布环境,当然,发布环境用jdk也没问题,仅仅是性能可能会有点影响,jdk与jre关系有点类似程序debug版本和release版本之间关系

(3)从文件大小来说,jdk比jre大。从图中可以看出,jdk比jre多了一层工具包,如常用的javac,java命令等

二   类加载器

关于jvm类加载器,可概括为如下图:

1.为什么要有类加载器?

(1)将字节码文件加载到运行时数据区。.java源码通过Javac命令编译后形成的字节码文件(.class),通过类加载器加载进入jvm中的。

(2)确定字节码文件在运行时数据区的唯一性。相同的字节码文件,通过不同的类加载器,就形成不同的文件,因此字节码文件在运行时数据区的唯一性是由字节码文件和加载它的类加载器共同决定的

2.类加载器的种类

从种类上来划分,类加载器主要划分为四大类

(1)启动类加载器 (根类加载器Bootstrap ClassLoader):该类加载器位于类加载器的最顶层,主要加载jre核心相关jar包,如 /jre/lib/rt.jar

(2)扩展类加载器(Extension ClassLoader):该类加载器位于类加载器层次的第二层,主要加载 jre扩展相关jar包,如/jre/lib/ext/*.jar

(3)应用程序类加载器(Application ClassLoader) App:该类加载器位于类加载器的第三层,主要加载类路径(classpaht)下的相关jar包

(4)用户自定义类加载器(User ClassLoader):该类加载器为用户自定义类加载器,主要加载用户指定的路径下的相关jar包

3.类加载器的机制(双亲委派)

对于字节码的加载,类加载机制为双亲委派,什么叫双亲委派呢?

类加载器获取字节码文件后,不是直接加载,而是将该字节码文件传递给其直接父级类加载器,其直接父加载器又继续传递给其直接父加载器的直接父加载器,依次类推到根父加载器,若根父加载器

能加载,则加载,否则交给其直接孩子加载器加载,直接孩子加载器能加载就加载,若不能,依次类推其直接孩子类加载器,若都不能加载,最后才由用户自定义类加载器加载。

4.jdk 1.8 如何实现类加载器?

如下为jdk 1.8 类加载器的实现,采用递归方式

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

5.破坏双亲委派模型

在某些情况下,由于受加载范围限制,父类加载器无法加载到需要的文件,因此父类加载器需要委托其子类加载器去加载相应的字节码文件。

如在jdk中定义的数据库驱动接口Driver,但该接口的实现却由不同的数据库厂商来实现,这就产生这样一个问题:由启动类(Bootstrap ClassLoader)

执行的DriverManager要加载实现了Driver接口的相关实现类,从而实现统一管理,但Bootstrap ClassLoader只能加载jre/lib下的相应文件,不能加载

由各个厂商实现的Dirver接口相关实现类(Dirver实现类是由Application ClassLoader加载),这时就需要Bootstrap ClassLoader委托其子类加载器加载Driver

来实现,从而破坏了双亲委派模型。

三  类的生命周期

java中的类,在jvm中的生命周期,大概分为五个阶段:

1.加载阶段:获取字节码二进制流,并将静态存储结构转化成方法区的运行时数据结构,且在方法区生成相应的类对象(java.lang.Class对象),作为该类的数据访问入口。

2.连接阶段:该阶段包括三个小阶段,即验证,准备和解析三阶段

(1)验证:确保字节码文件符合虚拟机规范要求,如元数据验证,文件格式验证,字节码验证和符号验证等

(2)准备:为内的静态表里分配内存,并且设置jvm默认值,对于非静态变量,此阶段,不需分配内存。

(3)解析:将常量池内的符号引用转化为直接引用

3.初始化阶段:类对象使用前的一些必要初始化工作

如下引用自一位博友的观点,个人认为解释得很好。

在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

除了 final static 修饰的常量,直接赋值操作以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit > 。初始化的目的是是为标记为

常量值的字段赋值,以及执行< clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

哪些条件会发生类初始化呢?

(1)当虚拟机启动时,初始化用户指定的主类(main函数);

(2)当遇到用于新建目标类实例的 new 指令时,初始化 new 指令的目标类;

(3)当遇到调用静态方法的指令时,初始化该静态方法所在的类;

(4)子类的初始化会触发父类的初始化;

(5)如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

(6)使用反射 API 对某个类进行反射调用时,初始化这个类;

(7)当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

4.使用阶段:jvm中使用对象

5.卸载阶段:将对象从jvm中卸载(unload),哪些条件会使jvm发生类卸载呢?

(1)加载该类的类加载器被回收

(2)该类的所有实例已经被回收

(3)该类对应的java.lang.Class对象没有任何地方被引用

四  jvm内存模型

1.JVM内存模型是怎样的?

如下为JVM内存模型架构图,由于在之前的文章中论述过,这里就不再一 一论述,主要讲解堆区。

在jdk 1.8前,堆区主要分为新生代、老年代和永久代。jdk 1.8后,去掉了永久代,增加了MetaSpace区。这里,主要分享jdk 1.8。

根据jdk1.8,堆区逻辑抽象为三个部分:

(1)新生代:包括Eden区,S0区(也叫from区),S21(也叫TO区)

(2)老年代

(3)Metaspace区

2.新生代和老年代的内存大小是怎样的?

根据官方建议,新生代占三分之一(Eden:S0:S1=8:1:1),老年代占三分之二,因此内存分配图如下:

3.GC回收是怎样进行的?

对象先在Eden区运行,当Eden内存用占用满时,Eden会进行两个操作:回收不用的对象和将未回收对象放入s0区,此时s0区和s1区互唤名称,即s0->s1,s1->s0,Eden区经过一次对象回收后,释放了空间,当Eden下次再满时,执行相同步骤,依次循环执行,当Eden区回收后,剩下的对象超过s0容量,则将出发一次Minor GC,此时将未回收的对象放入老年区,依次循环执行,当Eden区触发Minor GC时,剩余的对象容量大于old区剩余容量时,则old区将触发一次Major GC,此时便会触发一次Full GC。需要注意的是,一般发生Major GC,基本都都会伴随一次Full GC回收,Full GC非常损耗性能,在JVM调优时,要注意。

下图我在生产环境截的一张GC图,监控工具VisualVM

4.垃圾回收算法有哪些?

(1)标记-清除算法

该算法分为2个阶段,即标记阶段和清楚阶段,首先标记所有要回收的对象,然后回收被标记的对象。该算法效率低,且容易产生内存碎片。

a.效率低:需要遍历两次内存,第一次标记,第二次回收被标记对象

b.由于是非连续内存片段,容易产生碎片,当对象过大时,容易发生Full GC

下图为标记-清除算法 回收前和回收后对比示意图

(2)标记-复制算法

该算法解决了“标记-清除”算法效率低和大部分内存碎片问题,它将内存分为大小相等的两块,每次只使用其中一块,当其中一块需要回收时,只需将该快区域还存活的对象复制到另一块,然后再把该块内存一次性清理掉,循环往复。

下图为标记-复制算法回收前和回收收简要示意图

然而,由于年轻代大部分对象驻留时间都非常短,98%的对象都很快被回收,存活的对象非常少,不需要按照内存1:1来划分,而是按照8:1:1来划分,

将2%存活的对象放在s0(from区)即可。

如下为按照Eden:s0:s1 =8:1:1 划分示意图

(3)标记-整理算法

该算法分为两阶段,即标记和整理,首先标记所有存活对象,将这些对象向一端移动,然后直接清理掉端边界以外的内存。由于老年代的对象存活时间比较长,因此适合用该算法。

标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

如下为"标记-整理算法"回收期和回收后示意图

(4)分代收集算法

该算法未目前jvm算法,采用分代思想,模型如下:

5.常见GC回收器有哪些?

(1)SerialGC

SerialGC又叫串行回收器,也是最基础的GC回收器,主要适用于单核cpu,新生代采用复制算法, 老年代采用标记-压缩算法,在运行的过程中需要暂停应用程序,

因此会造成STW问题,在JVM标注参数为:-XX:+UseSerialGC 。

(2)ParallelGC

ParallelGC基于SerialGC,主要解决SerialGC串行问题,改为并行问题,解决多线程问题,但同样会产生STW问题,jvm关键参数:

a.-XX:+UseParNewGC,表示新生代并行(复制算法) 老年代串行(标记-压缩)

b.XX:+UseParallelOldGC,老年代也是并行

(3)CMS GC

CMSGC属于老年代回收器,采用“标记-清除算法”,不会发生STW问题,在jvm中参数设置:

-XX:+UseConcMarkSweepGC,表示老年代使用CMS收集器

(4)Garbage First

Garbage First面向jvm垃圾收集器 ,它满足短时间停顿的同时达到一个高的吞吐量,适用于多核cpu和大内存的服务端,也是jdk9的默认垃圾回收器。

五  总结

本篇文章在之前文章 一步步解析java执行内幕 基础上,深入分析了JVM内存模型,其中重点分析了jdk,jre和jvm关系,jvm类加载器,jvm堆内存划分,GC回收器和GC回收算法等,整体偏向于理论,由于篇幅有限,本篇文章未分析这些技术在JVM实际调优中是如何运用的,将在接下来的文章中与大家分享。

揭秘JAVA JVM内幕的更多相关文章

  1. Spark Tungsten揭秘 Day1 jvm下的性能优化

    Spark Tungsten揭秘 Day1 jvm下的性能优化 今天开始谈下Tungsten,首先我们需要了解下其背后是符合了什么样的规律. jvm对分布式天生支持 整个Spark分布式系统是建立在分 ...

  2. Java 授权内幕--转载

    在信息安全性领域,授权是世界的的中心,因为它是控制个体(即人.进程和计算机)对系统资源的访问权限的过程.直到最近,在 Java 安全体系结构中相关的问题都是“这段运行中的代码的访问权限是什么?” 随着 ...

  3. Java (JVM) Memory Model – Memory Management in Java

    原文地址:http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java Understanding JV ...

  4. JAVA JVM虚拟机选项:Xms Xmx PermSize MaxPermSize 区别

    Xms : 是指设定程序启动时占用内存大小.一般该值设置大的会使程序启动快,但是可能会使本机暂时变慢. Xmx : 是指设定程序运行期间最大可占用的内存大小,如果程序运行需要占用更多的内存,超出这个 ...

  5. Java JVM、JNI、Native Function Interface、Create New Process Native Function API Analysis

    目录 . JAVA JVM . Java JNI: Java Native Interface . Java Create New Process Native Function API Analys ...

  6. Java多线程学习笔记——从Java JVM对多线程数据同步的一些理解

       我们知道在多线程编程中,我们很大的一部分内容是为了解决线程间的资源同步问题和线程间共同协作解决问题.线程间的同步,通俗我们理解为僧多粥少,在粥有限情况下,我们怎么去防止大家有秩序的喝到粥,不至于 ...

  7. Java JVM 多态(动态绑定)

    Java JVM 多态(动态绑定) @author ixenos 摘要:绑定.动态绑定实现多态.多态的缺陷.纯继承与扩展接口.向下转型与RTTI 绑定 将一个方法的调用和一个方法的主体关联起来,称作( ...

  8. Java JVM 内存泄漏--全解析和处理办法 [ 转载 ]

    Java JVM 内存泄露——全解析和处理办法 [转载]   @author 小筐子 @address http://www.jianshu.com/p/bf159a9c391a         JA ...

  9. Java JVM使用哪种编码格式

    Java JVM使用哪种编码格式 A ASCII characters  B Unicode characters C Cp1252 D UTF-8 E GBK F GBK2312 答案:B   在J ...

随机推荐

  1. @loj - 6039@ 「雅礼集训 2017 Day5」珠宝

    目录 @description@ @solution@ @accpeted code@ @details@ @description@ Miranda 准备去市里最有名的珠宝展览会,展览会有可以购买珠 ...

  2. 修改MSSQL的端口地址_TcpPort_数据库安装工具_连载_2

    修改MSSQL的端口地址_TcpPort,可在程序中调用,从而修改TcpPort Use master Go ------------------------------ --1)在注册表中查询 Pi ...

  3. vim改变字体和查看映射的(mapping)命令

    临时修改.通过gvim Command MODE,输入如下命令即可: Linux/Unix: set guifont=Monospace\空格14 注意这里需要对空格使用\进行转义 Windows:  ...

  4. 图解MySQL索引(三)—如何正确使用索引?

    MySQL使用了B+Tree作为底层数据结构,能够实现快速高效的数据查询功能.工作中可怕的是没有建立索引,比这更可怕的是建好了索引又没有使用到.本文将围绕着如何优雅的使用索引,图文并茂地和大家一起探讨 ...

  5. 从一个计算器开始说起——C#中的工厂方法模式

    工厂模式作为很常见的设计模式,在日常工作中出镜率非常高,程序员们一定要掌握它的用法哟,今天跟着老胡一起来看看吧. 举个例子 现在先让我们来看一个例子吧,比如,要开发一个简单的计算器,完成加减功能,通过 ...

  6. Java学习笔记4(多线程)

    多线程 多个程序块同时运行的现象被称作并发执行.多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一条线程,它们会交替执行,彼此间可以进行通信. 进程:在一个操作系统中,每个独立执行的程 ...

  7. Win10搭建VM12.0.1虚拟机,虚拟机网络同宿主机ping不通的解决办法

    准备系统学习Linux系统,在电脑搭建了一个CentOS虚拟机,希望能从宿主机连接至虚拟机. 尝试了很多办法,碰到各种坑,最后这个方法成功了! 分享给大家,希望有所帮助. 一.环境 1.宿主机:Win ...

  8. 谈谈我对 Flutter 未来发展 和 “嵌套地狱” 的浅显看法

    Flutter 未来发展 提到 Flutter 就不得不提到 Fuchsia 系统,这是一个尚未正式发布的操作的系统,引用 Android 和 Chrome 的高级副总裁 Hiroshi Lockhe ...

  9. 下订单更新订单表然后减少库存表中的数据,出现库存超卖,使用数据库和redis坚决库存超卖的问题

    上面的代码更新库存的数据,存在多线程的问题,第一种方法使用synchronized关键字修饰的语句块代码,但是性能较低,并且还是存在问题的 在分布式的场景下,当前库存系统部署在多个tomcat上,即使 ...

  10. 服务扫描-dmitry、nmap、amap和服务识别

    dmitry使用-pb参数可以进行常用端口的banner抓取. 抓取效果: 强大的nmap也可以进行banner抓取,但是需要使用nmap内置的banner.nse脚本: kali中还有一个工具叫am ...