jvm全景图

 

JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机,是一种用于计算设备的规范

语法糖

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家Peter J. Landin发明的一个技术术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用,虽然Java中有很多语法糖,但是Java虚拟机并不支持这些语法糖,所以这些语法糖在编译阶段就会被还原成简单的基础语法结构,这样才能被虚拟机识别,这个过程就是解语法糖。虽然Java中有很多语法糖,但是Java虚拟机并不支持这些语法糖,所以这些语法糖在编译阶段就会被还原成简单的基础语法结构,这样才能被虚拟机识别,这个过程就是解语法糖

如switch支持枚举及字符串、泛型、条件编译、断言、可变参数、自动装箱/拆箱、枚举、内部类、增强for循环、try-with-resources语句、lambda表达式等。还有JDK 10中的局部变量类型推断、JDK 13中的文本块(Text Blocks),其实本质上都是语法糖。关于Java中的语法糖,Hollis大神写过很多文章深入的介绍过他们的原理,如《不了解这12个语法糖,别说你会Java》、《我反编译了Java 10的本地变量类型推断》等。

举例说明: Java中的swith自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii码是整型)以及int。

各版本jdk的演变

jdk1.6及之前,有永久代(Permanent generation),静态变量存放在永久代。

jdk1.7,字符串常量池、静态变量移出永久代,存放在堆中。

jdk1.8及之后,去除了永久代,由本地内存的元空间Metaspace取代

为什么去掉永久代:

  • 永久代在jvm中,合适的大小难以确定(元空间分配在本地内存,无需考虑大小)
  • 对永久代调优很困难

类的加载过程(生命周期)

1、加载:查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

    • 通过一个类的全限定名来获取其定义的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

2、验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:

    • 文件格式的验证:验证字节流是否符合Class文件格式的规范,例如是否以0xCAFEBABE开头,版本号是否合理
    • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。例如是否有父类,继承了final类?非抽象类实现了所有的抽象方法
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    • 符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

3、准备:正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配

    • 类变量(static )和 全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的值。
    • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过。
    • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
    • 局部变量不需初始化。

4、解析:虚拟机将常量池内的符号引用替换为直接引用的过程

5、**初始化:**类加载最后阶段,若该类具有超类,则对其进行初始化。只有当对类的主动使用的时候才会导致类的初始化,类的主动使用 包括以下六种:

    • 创建类的实例,也就是new的方式
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射
    • 初始化某个类的子类,则其父类也会被初始化
    • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

6、**使用:**使用类进行业务操作。

7、**卸载:**结束生命周期

    • 执行System.exit()方法
    • 程序正常执行结束
    • 由于操作系统出现错误而导致Java虚拟机进程终止

类加载器

打印类加载如下,双亲委托优势:

优势:

  • 避免类的重复加载
  • 使得java中的类随着他的类加载器一起具备了一种带有优先级的层次关系,会保证系统的类不会受到恶意的攻击
  • 保护程序安全,防止核心API被随意篡改
/**
 *ClassLoader加载过程用的是 模板方法模式 设计模式
 * 1.Bootstrap类加载器(启动类加载器)为顶级的classloader加载路径为<JAVA_HOME>\lib目录中jar文件/charset.jar等核心类,C++实现。
 * 它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,按照文件名识别加载jar包的,如rt.jar,
 * 如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,
 * Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
 *
 * 2.Ex扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,
 * 由Java语言实现的,是Launcher的静态内部类tension类加载器(扩展类加载器)负责加载<JAVA_HOME>\lib\ext目录中的jar文件或者由-Djava.ext.dirs指定。
 * 开发者可以直接使用标准扩展类加载器
 *
 * 3.App类加载器(系统类加载器)负责加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径,
 * 如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。
 *
 * 4.Custom为(自定义类加载器),由用户自己实现。
 * 
 * JVM是按需动态加载,采用双亲委派机制,自底部往上检查该类是否加载(图中4->3->2->1的顺序),如果没有classloader加载过该类则会自上而下(1->2->3->4)再去查找加载class,直到找到该class并加载到内存中
 * 采用双亲委派机制的好处是为了安全,(安全)避免外部加载进来的类和内部classloader链中产生冲突,做恶意破坏,如果有同名的类被加载进来JVM首先会判断是否有这样一个类已经加载过,如果已经加载则不会再加载一次该类(效率)
 */
 public class 打破双亲委托的自定义解密Classloader {
    public static void main(String[] args) throws Exception {
        System.out.println(打破双亲委托的自定义解密Classloader.class.getClassLoader());
        System.out.println(打破双亲委托的自定义解密Classloader.class.getClassLoader().getClass().getClassLoader());
        System.out.println(打破双亲委托的自定义解密Classloader.class.getClassLoader().getParent());
        System.out.println(打破双亲委托的自定义解密Classloader.class.getClassLoader().getParent().getParent());
        /**
         *
         * 打印结果如下:
         * sun.misc.Launcher$AppClassLoader@14dad5dc
         * null
         * sun.misc.Launcher$ExtClassLoader@28a418fc
         * null --》虽然是虚拟机自身的一部分,但是Bootstrap ClassLoader为C++语言,所以这里打印不出来
        */
    }
}

沙箱安全机制

Java安全模型的核心就是java沙箱sandbox,将java代码限定在jvm特定的运行范围中,并严格限制代码对本地系统资源访问,这样来保证代码的有效隔离,防止对本地系统造成破坏。所有java程序运行都可以指定沙箱,可以定制安全策略。java将执行程序分成本地代码和远程代码,本地代码默认可信任,远程不受信任.目前最新安全机制是引入了域domain。jvm会把所有代码加载到不同系统域和应用域,系统域专门负责关键资源交互,而应用域则通过系统域部分代理来对所需资源进行访问。jvm中不同的protected domain,对应不同的权限。

基本组件:字节码检验器、类装载器、存取控制器、安全管理器、安全软件包

    • 字节码校验器 bytecode verifier
      • 确保java类文件遵循java语言规范,帮助程序实现内存保护。并不是所有类都经过字节码校验器,如核心类。
    • 类加载器 class loader
      • 双亲委派机制、安全校验等,防止恶意代码干涉。守护类库边界。
    • 存取控制器 access controller
      • 它可以控制核心API对操作系统的存取权限,控制策略可以有由用户指定。
    • 安全管理器 security manager
      • 它是核心API和系统间的主要接口,实现权限控制,比存取控制器优先级高。
    • 安全软件包 secruity package

java.secruity下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性。包括:安全提供者、消息摘要、数字签名、加密、鉴别等。

一般实使用ava开发服务端程序不涉及安全管理器,开发applet、app会用到。

打开安全管理器:

$ java -Djava.security.manager xxx

$ java -Djava.security.manager -DDjava.security.policy="${policypath}"

因为安全限制条件可以定制,所以还需要提供具体的安全策略文件路径,默认的策略文件路径是 JAVA_HOME/jre/lib/security/java.policy。

IDEA字节码查看jclasslib

jclasslib bytecode viewer主要优点:

1 不需要使用javap指令,使用简单

2 点击字节码指令可以跳转到 java虚拟机规范对应的章节。

可以查看基本信息、常量池、接口、属性、函数等信息。比如我们想了解 putstatic 的含义,可以点击该指令。自动通过浏览器打开虚拟机规范并定位到该指令位置

程序计数器

程序计数器(Program Counter Register)是一个记录着当前线程所执行的字节码的行号指示器.它占用很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致,不会存在内存溢出(OutOfMemoryError)。装上jclasslib插件后通过view菜单栏的show bytecode with jclasslib即可。效果如下图:

 

虚拟机栈

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素,栈的生命周期和线程的生命周期一致,随着线程的启动而创建,随着线程的停止而销毁。当主线程结束后,整个的虚拟机的栈就全部销毁掉。

存放类型:8大基本类型 + 对象的引用 + 实例的方法

栈:线程运行时需要的内存空间。

栈帧:每个方法运行时需要的内存,栈帧是栈的基本单位。每个线程只能有一个活动栈帧,对应当前执行的那个方法

例如:main(),test(),test1()都是一个栈帧,test1()是活动栈帧,也可以说是当前栈帧

通过设置-Xss size 给栈内存分配大小

栈溢出(StackOverflowError)

  • 栈帧过多(递归调用)
  • 栈帧过大(不容易出现)

本地方法栈

本地方法栈也是线程私有

理解java中的逃逸[TODO:待补充实例]

Java在Java SE 6u23以及以后的版本中支持并默认开启了逃逸分析的选项, 开启 -XX:+DoEscapeAnalysis 、关闭 -XX:-DoEscapeAnalysis

逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用。

方法逃逸:例如作为调用参数传递到其他方法中。

线程逃逸:有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量。

使用逃逸分析,JIT编译器可以对代码做如下优化:

同步省略(锁消除)

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除【TODO:待补充实例]

标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

      • 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换,举例说明:

public static void main(String[] args) { testCase2(); } private static void testCase2() { TestCase2 test = new TestCase2(6,6); System.out.println(test.a+test.b); } class TestCase2{ private int a; private int b; }

经过标量替换后,代码变为如下代码:

private static void testCase2() { int a = 6; int b = 6; System.out.println(a+b); }

可以大大减少了堆内存的占用。

栈上分配

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了

逃逸分析并不成熟

关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。虽然这项技术并不十分成熟,但是他也是即时编译器优化技术中一个十分重要的手段

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值