JVM深入与项目中应用--(一)JVM理解

参考:

http://www.cnblogs.com/sunada2005/p/3577799.html

http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html


1. JVM/JRE/JDK

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

2. JVM原理

java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。



3. JVM执行程序的过程

1) 加载.class文件 2) 管理并分配内存 3) 执行垃圾收集
JRE(java运行时环境)由JVM构造的java程序的运行环,也是Java程序运行的环境,但是他同时一个操作系统的一个应用程序一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机。操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境:1) 创建JVM装载环境和配置 2) 装载JVM.dll 3) 初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例4) 调用JNIEnv实例装载并处理class类。
 

4. JVM的生命周期

1) JVM实例对应了一个独立运行的java程序它是进程级别 
a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void 
main(String[] args)函数的class都可以作为JVM实例运行的起点 
b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程 
c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
 
2) JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的

5. JVM的体系结构



类装载器(ClassLoader)(用来装载.class文件)
执行引擎(执行字节码,或者执行本地方法)
运行时数据区
(方法区、堆、java栈、PC寄存器、本地方法栈)

6. JVM运行时数据区


第一块:PC寄存器

PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。

PC 指当前指令(或操作码)的地址,本地指令除外。如果当前方法是 native 方法,那么PC 的值为 undefined。所有的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。

第二块:JVM栈

JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。

第三块:堆(Heap)

它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。


(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的

(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配

(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

(4) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。

第四块:方法区域(Method Area)

(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。

(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

第五块:运行时常量池(Runtime Constant Pool)

存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

第六块:本地方法堆栈(Native Method Stacks)

JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

7. JVM垃圾回收

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

(1)对新生代的对象的收集称为minor GC;

(2)对旧生代的对象的收集称为Full GC;

(3)程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)

(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)

(3)弱引用:在GC时一定会被GC回收

(4)虚引用:由于虚引用只是用来得知对象是否被GC


8. JVM参数设置

不管是YGC还是Full GC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。
经验&&规则


年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.


年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.

9. JVM 系统线程

原文链接: jamesdbloom 翻译: ImportNew.com 挖坑的张师傅
译文链接: http://www.importnew.com/17770.html

如果使用 jconsole 或者其它调试器,你会看到很多线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程创建的其他线程一起运行。Hotspot JVM 后台运行的系统线程主要有下面几个:

虚拟机线程(VM thread) 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC 线程 这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程 这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

1)每个运行的线程都包含下面这些组件

程序计数器PC:

栈(Stack)

每个线程拥有自己的栈,栈包含每个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。

Native栈

并非所有的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 一般都会为每个线程创建本地方法栈。如果 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种情况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(一般是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。

栈的限制

栈可以是动态分配也可以固定大小。如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。

栈帧(Frame)

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。

每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用

局部变量数组

局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。

有下面这些局部变量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。

操作数栈

操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。

int i;

被编译成下面的字节码:

1
2
0:    iconst_0    // Push 0 to top of the operand stack
1:    istore_1  

动态链接

每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。

C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。

当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

2)线程间共享

堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。

为了支持垃圾回收机制,堆被分为了下面三个区域:

  • 新生代
    • 经常被分为 Eden 和 Survivor
  • 老年代
  • 永久代

内存管理

对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:

  1. 新的对象和数组被创建并放入老年代。
  2. Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
  3. Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
  4. 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。

非堆内存

非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。

非堆内存包括:

  • 永久代,包括:
    • 方法区
    • 驻留字符串(interned strings)
  • 代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。

即时编译(JIT)

Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。

方法区

方法区存储了每个类的信息,比如:

  • Classloader 引用
  • 运行时常量池
    • 数值型常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 针对每个字段的信息
      • 字段名
      • 类型
      • 修饰符
      • 属性(Attribute)
  • 方法数据
    • 每个方法
      • 方法名
      • 返回值类型
      • 参数类型(按顺序)
      • 修饰符
      • 属性
  • 方法代码
    • 每个方法
      • 字节码
      • 操作数栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
      • 每个异常处理器
      • 开始点
      • 结束点
      • 异常处理代码的程序计数器(PC)偏移量
      • 被捕获的异常类对应的常量池下标

所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。

类文件结构

一个编译后的类文件包含下面的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
     u4            magic;
     u2            minor_version;
     u2            major_version;
     u2            constant_pool_count;
     cp_info        contant_pool[constant_pool_count – 1];
     u2            access_flags;
     u2            this_class;
     u2            super_class;
     u2            interfaces_count;
     u2            interfaces[interfaces_count];
     u2            fields_count;
     field_info        fields[fields_count];
     u2            methods_count;
     method_info        methods[methods_count];
     u2            attributes_count;
     attribute_info    attributes[attributes_count];
}

类加载器

JVM 启动时会用 bootstrap 类加载器加载一个初始化类,然后这个类会在public static void main(String[])调用之前完成链接和初始化。执行这个方法会执行加载、链接、初始化需要的额外类和接口。

加载(Loading)是这样一个过程,找到代表这个类的 class 文件或根据特定的名字找到接口类型,然后读取到一个字节数组中。接着,这些字节会被解析检验它们是否代表一个 Class 对象并包含正确的 major、minor 版本信息。直接父类的类和接口也会被加载进来。这些操作一旦完成,类或者接口对象就从二进制表示中创建出来了。

链接(Linking)是校验类或接口并准备类型和父类父接口的过程。链接过程包含三步:校验(verifying)、准备(preparing)、部分解析(optionally resolving)。



10. JVM项目中应用

针对项目中有JVM优化时,或者涉及内存的项目,通过以下方式测试:
1) Jconsole ,jvisualvm监控项目的内存回收情况(或者其他方式查看机器本身的内存情况);
2)GC的情况,使用如下命令查看回收情况:
jmap
jstack
使用jstat命令查看jvm的GC情况:
jstat -gc 12538 5000
即会每5秒一次显示进程号为12538的java进成的GC情况,

更多参考:http://blog.csdn.net/feihong247/article/details/7874063




你可能感兴趣的:(【测试】系列,【测试】工具,【技术】研发,【测试】性能测试)