深⼊理解java虚拟机(周志明)JVM个⼈总结
JIT:即时编译器,把class中的字节码翻译成CPU上可以直接执⾏的⼆进制指令。新的JIT不仅是编译,可以分析字节码是否可以优化,它可以将那些经常执⾏的字节码⽚段(热点代码)进⾏缓存。
java虚拟机规范 周志明
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是⼀种⽤于计算设备的规范,它是⼀个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语⾔的⼀个⾮常重要的特点就是与平台的⽆关性。⽽使⽤Java虚拟机是实现这⼀特点的关键。⼀般的⾼级语⾔如果要在不同的平台上运⾏,⾄少需要编译成不同的⽬标代码。⽽引⼊Java语⾔虚拟机后,Java语⾔在不同平台上运⾏时不需要重新编译。Java语⾔使⽤Java虚拟机屏蔽了与具体平台相关的信息,使得Java语⾔编译程序只需⽣成在Java虚拟机上运⾏的⽬标代码(字节码),就可以在多种平台上不加修改地运⾏。Java虚拟机在执⾏字节码时,把字节码解释成具体平台上的机器指令执⾏。这就是Java的能够⼀次编译,到处运⾏ 的原因。
* 执⾏引擎处于JVM的核⼼位置,在Java虚拟机规范中,它的⾏为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执⾏字节码遇到指令时,它的实现应该做什么,但对于怎么做却⾔之甚少。Java虚拟机⽀持⼤约248个字节码。每个字节码执⾏⼀种基本的CPU运算,例如,把⼀个整数加
到寄存器,⼦程序转移等。Java指令集相当于Java程序的汇编语⾔。Java指令集中的指令包含⼀个单字节的操作符,⽤于指定要执⾏的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由⼀个单字节的操作符构成。
* java的字节码是由javac所编译的,
Java中,字节码是CPU构架(JVM)的具有可移植性的机器语⾔
Java中,字节码是CPU构架(JVM)的具有可移植性的机器语⾔第⼀章 ⾛近java
* 因为程序员把内存控制的权⼒交给了java虚拟机,编码的时候享⾃动内存管理的诸多优势。
* 提供了⼀个相对安全的内存管理和访问机制,避免了绝⼤部分的内存泄漏和指针越界问题
* 但是也是会出现内存泄漏。
* Jdk进化史
* jdk 1.1 jdbc jar⽂件格式 jdk javabeans 语法的内部类 反射
* 1.2 java分为三个⽅向 j2ee(企业) j2se(桌⾯开发) j2me(⼿机移动终端)
* collections集合 math TimerAPI
* 1.3 类库
* 1.4 正则表达式 异常链 nio xml 等
* 1.5 ⾃动装箱 泛型 动态注解 枚举 可变长参数 遍历(foreach) concurrent 并发包
* 1.6 锁 垃圾收集 类加载 算法
* 普通对象指针压缩功能 (-XX:+ userCompressedOops)不建议开启 jvm⾃动管理开启
* 开启压缩指针会增加执⾏代码质量,java 堆 指向java堆内对象的指针都会被压缩
* 1.7
* 1.8 lambda 表达式 map
* 第⼆章
* java 内存区域与内存溢出异常
* ⽅法区和堆 线程共享
* 剩下的线程隔离
* 程序计数器(program counter register)只占⽤了⼀块⽐较⼩的内存空间{可以忽略不计}
* 可以看作是当前线程所执⾏的字节码⽂件(class)的⾏号指⽰器。在虚拟机的世界中,字节码解释器就是通过改变计数器的值来选取下⼀条执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复都需要这玩意来实现的
* 多线程是通过线程轮流切换,并分配处理器执⾏时间的⽅式来实现的。
* 1个处理器执⾏⼀个线程 多核同时多个
* 每条线程都需要有⼀个独⽴的程序计数器。各条线程之间计数器互不影响独存储。线程私有的内存。
java虚拟机栈
* 每个⽅法执⾏都会创建⼀个栈帧,⽤于存放局部变量表,操作栈,动态链接,⽅法出⼝等。每个⽅法从被调⽤,直到被执⾏完。对应着⼀个栈帧在虚拟机中从⼊栈到出栈的过程。
* 会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度⼤于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展⽆法申请到⾜够的内存空间时候,抛出OutOfMemoneyError
* 每当⼀个java⽅法被执⾏时都会在虚拟机中新创建⼀个栈帧,⽅法调⽤结束后即被销毁。
* 局部变量表中的变量作⽤域是当前调⽤的函数。函数调⽤结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。
* 栈帧存储空间为虚拟机栈,每⼀个栈帧都有⾃⼰的局部变量表、操作数栈和指向当前⽅法所属的类引⽤。
* 当然⽅法调⽤其他的⽅法新的栈帧就会创建且控制权交给新的栈帧
* ⽽ JVM 的字节码指令是这样的:
* iconst_1 //把整数 1 压⼊操作数栈
* iconst_2 //把整数 2 压⼊操作数栈
* iadd //栈顶的两个数相加后出栈,结果⼊栈时间正则表达式java
* 局部变量表所需内存空间在编译期间完成分配当进⼊⼀个⽅法时,这个⽅法需要在栈帧中分配多⼤的局部变量空间是完全确定的,在⽅法运⾏期间不会改变局部变量表的⼤⼩。
* 局部变量区被组织为以⼀个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存⼊数组前要被转换成int值,⽽long和 double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第⼀项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。
* 单位slot 8个数据类型基本上都是占⽤⼀个slot long duble 占⽤2个加引⽤类型的数据指向⼀条虚拟机指令的操作码引⽤指针或者对象句柄
* 虚拟机规范 boolean 虚拟机中int代替 boolean 数组 oracle 中为byte 数组
* true 为1 false 为0
* 局部变量表使⽤索引来进⾏访问⾸个局部变量的索引值为0
* 操作数栈是后进先出的栈
* 本地⽅法栈
* 什么是Native Method
* 简单地讲,⼀个Native Method就是⼀个java调⽤⾮java代码的接⼝。⼀个Native Method是这样⼀个java的⽅法:该⽅法的实现由⾮java语⾔实现,⽐如C。这个特征并⾮java所特有,很多其它的编程语⾔都有这⼀机制
* 与java环交互:
* 有时java应⽤需要与java外⾯的环境交互。这是本地⽅法存在的主要原因,你可以想想java需要与⼀些底层系统如操作系统或某些硬件交换信息时的情况。本地⽅法正是这样⼀种交流机制:它为我们提供了⼀个⾮常简洁的接⼝,⽽且我们⽆需去了解java应⽤之外的繁琐的细节。
* 堆
* 是虚拟机中最⼤的⼀块共享区域 在虚拟机启动的时候创建 它存储了⾃动内存管理系统 (gc垃圾收集器) 虚拟机实现者根据系统的实际需要来选择⾃动内存管理技术
* 所有类实例和数组分配内存的区域
* 基本上采⽤分代收集算法
* ⽅法区
* 各个线程共享的运⾏区域
* 存储了每个类的结构信息运⾏时常量池字段⽅法数据构造函数普通⽅法的字节码内存还有特殊⽅法
* oom
* ⽅法区并不等于永久代
* hotspot 把gc 分代收集扩展到⽅法区使⽤永久代来实现⽅法区跟堆⼀样管理内存
* 运⾏时常量池是⽅法区的⼀部分具备动态性可以编译时候产⽣ class ⽂件常量池内容运⾏区间产⽣新的
* 虚拟机指令不依赖类接⼝类实例数组的布局⽽是依赖常量池表中符号信息
* 在HotSpot虚拟机中,⽤永久代来实现⽅法区,将GC分代收集扩展⾄⽅法区,但是这样容易遇到内存溢出的问题。
* JDK1.7中,字符串常量池native()
* JDK1.8撤销永久代,引⼊元空间。
* 直接内存(堆外内存)并不是虚拟机运⾏时数据区的⼀部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加⼊了NIO(New Input/Output)类,引⼊了⼀种基于通道(Channel)与缓冲区(Buffer)的I/O ⽅式,它可以使⽤native 函数库直接分配堆外内存,然后通脱⼀个存储在Java堆中的DirectByteBuffe r 对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了在Java堆和Native堆中来回复制数据。
* 好处:这样做有两⽅⾯的好处:
* 减少GC管理内存:由于GCIH会从Old区“切出”⼀块, 因此导致GC管理区域变⼩, 可以明显降低GC⼯作量, 提⾼GC效率, 降低Full GC STW时间(且由于这部分内存仍属于堆, 因此其访问⽅式/速度不变- 不必付出序列化/反序列化的开销).
* GCIH内容进程间共享:由于这部分区域不再是JVM运⾏时数据的⼀部分, 因此GCIH内的对象可供对个JVM实例所共享(如⼀台Server跑多个MR-Job可共享同⼀份Cache数据), 这样⼀台Server也就可以跑更多的VM实例.
* 3、堆外内存的好处是:
* (1)可以扩展⾄更⼤的内存空间。⽐如超过1TB甚⾄⽐主存还⼤的空间;
* (2)理论上能减少GC暂停时间;
* (3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;
* (4)它的持久化存储可以⽀持快速重启,同时还能够在测试环境中重现⽣产数据
* 本机直接内存的分配不会受到Java 堆⼤⼩的限制,受到本机总内存⼤⼩限制
* 配置虚拟机参数时,不要忽略直接内存防⽌出现OutOfMemoryError异常
* Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有⾃⼰的⼯作内存,线程的⼯作内存中保存了被该线程所使⽤到的变量(这些变量是从主内存中拷贝⽽来)。线程对变量的所有操作(读取,赋值)都必须在⼯作内存中进⾏。不同线程之间也⽆法直接访问对⽅⼯作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
* 基于此种内存模型,便产⽣了多线程编程中的数据“脏读”等问题。
volatile变量是⼀种稍弱的同步机制在访问volatile变量时不会执⾏加锁操作,因此也就不会使执⾏线程阻塞,因此volatile变量是⼀种⽐synchronized关键字更轻量级的同步机制。读取快 修改慢
* 1.volatile保证可见性
* 1)保证了不同线程对这个变量进⾏操作时的可见性,即⼀个线程修改了某个变量的值,这新值对其他线程来说是⽴即可见的。
* 2)禁⽌进⾏指令重排序。
* 编译出来的只有⼀条字节码指令,也不意味执⾏这条指令就是⼀个原⼦操作 ⼀条字节码指令在解释
* 执⾏时,解释器将要运⾏许多⾏代码才能实现。
* 什么是指令重排?
* 指令重排是指JVM在编译Java代码的时候,或者CPU在执⾏JVM字节码的时候,对现有的指令顺序进⾏重新排序。
* 指令重排的⽬的是为了在不改变程序执⾏结果的前提下,优化程序的运⾏效率。需要注意的是,这⾥所说的不改变执⾏结果,指的是不改变单线程下的程序执⾏结果。
* 如何使⽤volatile呢
* 运算结果并不依赖变量的当前值,后者能够确保只有单⼀的线程修改变量的值
⾮原⼦操作加锁 ++ 不能保证原⼦性需要加synchronized 或者lock
第七章虚拟机类加载机制
* Java源代码被编译成class字节码,最终需要加载到虚拟机中才能运⾏。整个⽣命周期包括:加载、验证、准备、解析、初始化、使⽤和卸载7个阶段。
*
* 加载验证准备初始化卸载5个阶段的顺序是确定的
* 加载
* 1、通过⼀个类的全限定名获取描述此类的⼆进制字节流;
* 2、将这个字节流所代表的静态存储结构保存为⽅法区的运⾏时数据结构;
* 3、在java堆中⽣成⼀个代表这个类的java.lang.Class对象,作为访问⽅法区的⼊⼝;
* 类加载器
* 虚拟机设计团队把加载动作放到JVM外部实现,以便让应⽤程序决定如何获取所需的类,实现这个动作的代码称为¡°类加载器¡±,JVM提供了3种类加载器:* 1、启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib ⽬录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按⽂件名识别,如rt.jar)的类。
* 2、扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext ⽬录中的,或通过dirs系统变量指定路径中的类库。
* 3、应⽤程序类加载器(Application ClassLoader):负责加载⽤户路径(classpath)上的类库。
* JVM基于上述类加载器,通过双亲委派模型进⾏类的加载,当然我们也可以通过继承java.lang.ClassLoader实现⾃定义的类加载器。
* 双亲委派模型⼯作过程:当⼀个类加载器收到类加载任务,优先交给其⽗类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当⽗类加载器⽆法完成加载任务时,才会尝试执⾏加载任务。
* 双亲委派模型有什么好处?⽐如位于rt.jar包中的类java.lang.Object,⽆论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进⾏加载,确保了Obj ect类在各种加载器环境中都是同⼀个类。
* 验证
* 为了确保Class⽂件符合当前虚拟机要求,需要对其字节流数据进⾏验证,主要包括格式验证、元数据验证、字节码验证和符号引⽤验证。
* 格式验证:验证字节流是否符合class⽂件格式的规范,并且能被当前虚拟机处理,如是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池是否有不⽀持的常量类型等。只有经过格式验证的字节流,才会存储到⽅法区的数据结构,剩余3个验证都基于⽅法区的数据进⾏。
* 元数据验证:对字节码描述的数据进⾏语义分析,以保证符合Java语⾔规范,如是否继承了final修饰的类、是否实现了⽗类的抽象⽅法、是否覆盖了⽗类的fin al⽅法或final字段等。
* 字节码验证:对类的⽅法体进⾏分析,确保在⽅法运⾏时不会有危害虚拟机的事件发⽣,如保证操作数栈的数据类型和指令代码序列的匹配、保证跳转指令的正确性、保证类型转换的有效性等。
* 符号引⽤验证:为了确保后续的解析动作能够正常执⾏,对符号引⽤进⾏验证,如通过字符串描述的全限定名是都能到对应的类、在指定类中是否存在符合⽅法的字段描述符等。
准备准备阶段是正式为类变量分配内存并设置类变量初始值得阶段,这些变量所使⽤的内存都讲在⽅法区中进⾏分配。这时候进⾏内存分配的仅包括类变量(被static修饰的变量),⽽不包括实例变量,实例变量将会在对象实例化时随着对象⼀起分配在Java堆中。
* 在准备阶段,为类变量(static修饰)在⽅法区中分配内存并设置初始值。
* private static int var = 100;
* 准备阶段完成后,var 值为0,⽽不是100。在初始化阶段,才会把100赋值给val,但是有个特殊情况:
* private static final int VAL= 100;
* 在编译阶段会为VAL⽣成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。
* 初始化
* 初始化阶段是执⾏类构造器⽅法的过程,⽅法由类变量的赋值动作和静态语句块按照在源⽂件出现的顺序合并⽽成,该合并操作由编译器完成。
* 开始执⾏java代码(或者说字节码)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论