引用
第一部分:JVM基本结构
1.什么是JVM
一个java的application对应了一个java.exe/javaw.exe(java.exe和javaw.exe你可以把它看成java的虚拟机,一个有窗口界面一个没有)。你运行几个application就有几个java.exe/javaw.exe。或者更加具体的说,你运行了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。通过java.exe或者javaw.exe来启动一个虚拟机实例。
2.JVM的生命周期
java的虚拟机中有两种线程,一种叫叫守护线程,一种叫非守护线程,main函数就是个非守护线程,虚拟机的gc就是一个守护线程。java的虚拟机中,只要有任何非守护线程还没有结束,java虚拟机的实例都不会退出,所以即使main函数这个非守护线程退出,但是由于在main函数中启动的匿名线程也是非守护线程,它还没有结束,所以jvm没办法退出。
package com.jvm.sym;
public class TestJVM {
/**
* @param args
*/
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.currentThread().sleep(i * 10000);
System.out.println("睡了" + i * 10 + "秒");
} catch (InterruptedException e) {
System.out.println("干嘛吵醒我");
}
}
}
}).start();
for (int i = 0; i < 50; i++) {
System.out.print(i);
}
}
}
/*打印结果:0睡了0秒
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849睡了10秒
睡了20秒
睡了30秒
睡了40秒
*/
引用
3.JVM的体系结构
如下图所示:图一
4.JVM体系各个模块之间的调用情况:
图二
当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader
5.方法区的功能
如图三
//举例方法区功能
package test;import java.io.Serializable;public final class ClassStruct extends Object implements Serializable {//1.类信息
//2.对象字段信息
private String name;
private int id;
//4.常量池
public final int CONST_INT=0;
public final String CONST_STR="CONST_STR";
//5.类变量区
public static String static_str="static_str";
//3.方法信息
public static final String getStatic_str ()throws Exception{
return ClassStruct.static_str;
}}
引用
其实内存的字节码块就是完整的把整个类装到了内存而已
a.类信息:修饰符(public final)
是类还是接口(class,interface)
类的全限定名(Test/ClassStruct.class)
直接父类的全限定名(java/lang/Object.class)
直接父接口的权限定名数组(java/io/Serializable)
也就是 public final class ClassStruct extends Object implements Serializable这段描述的信息提取
b.字段信息:修饰符(pirvate)
字段类型(java/lang/String.class)
字段名(name)
也就是类似private String name;这段描述信息的提取
c.方法信息:修饰符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
参数需要用到的局部变量的大小还有操作数栈大小(操作数栈我们后面会讲)
方法体的字节码(就是花括号里的内容)
异常表(throws Exception)
也就是对方法public static final String getStatic_str ()throws Exception的字节码的提取
d.常量池:
d.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR="CONST_STR";
1.3CONSTANT_DOUBLE_INFO浮点型直接常量池
等等各种基本数据类型基础常量池(待会我们会反编译一个类,来查看它的常量池等。)
d.2.方法名、方法描述符、类名、字段名,字段描述符的符号引用
也就是所以编译器能够被确定,能够被快速查找的内容都存放在这里,它像数组一样通过索引访问,就是专门用来做查找的。
编译时就能确定数值的常量类型都会复制它的所有常量到自己的常量池中,或者嵌入到它的字节码流中。作为常量池或者字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量作为他们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存
e.类变量:
就是静态字段( public static String static_str="static_str";)
虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。
f.一个到classLoader的引用,通过this.getClass().getClassLoader()来取得为什么要先经过class呢?思考一下,然后看第七点的解释,再回来思考
g.一个到class对象的引用,这个对象存储了所有这个字节码内存块的相关信息。所以你能够看到的区域,比如:类信息,你可以通过this.getClass().getName()取得
所有的方法信息,可以通过this.getClass().getDeclaredMethods(),字段信息可以通过this.getClass().getDeclaredFields(),等等,所以在字节码中你想得到的,调用的,通过class这个引用基本都能够帮你完成。因为他就是字节码在内存块在堆中的一个对象
h.方法表,如果学习c++的人应该都知道c++的对象内存模型有一个叫虚表的东西,java本来的名字就叫c++- -,它的方法表其实说白了就是c++的虚表,它的内容就是这个类的所有实例可能被调用的所有实例方法的直接引用。也是为了动态绑定的快速定位而做的一个类似缓存的查找表,它以数组的形式存在于内存中。不过这个表不是必须存在的,取决于虚拟机的设计者,以及运行虚拟机的机器是否有足够的内存
4.总结:
首先,当一个程序启动之前,它的class会被类装载器装入方法区(不好听,其实这个区可叫做Permanent区),执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统API等。
第二部分:JVM的各个模块的功能
在这个之前首先考虑下Java的内存管理系统,由于Java的内存管理系统实际包含两方面的内容:内存的分配和垃圾的回收。由于Java采用的自动的内存管理方式,程序员不用考虑内存管理的细节,那为什么需要去了解呢?原因有三个:
a.由于在新的线程创建时,JVM会为每个线程创建一个专属的栈(stack),这样就要求不能用过多的递归,容易导致stack overflow。
b.一旦内存出现问题,容易找到根源。
c.有优化JVM,使得优化应用。
下面就分别将各个模块:
1.类装载子系统:负责把类从硬盘上中装入内存的方法区;
2.垃圾收集器:主要是收回不再允许的程序引用对象所占用的内存,它可能负责还在使用的对象,以减少堆对象。
3.执行引擎:有四种方式:
a、最简单的:一次性解释字节码。
b、快,但消耗内存的:“即时编译器”,第一次被执行的字节码会被编译成机器代码,放入缓存,以后调用可以重用。
c、自适应优化器,虚拟机开始的时候会解释字节码,但是会监视运行中程序的活动,并记录下使用最频繁的代码段。程序运行的时候,虚拟机只把使用最频繁的代码编译成本地代码,其他的代码由于使用的并不频繁,继续保留为字节码--由虚拟机继续解释他们。一般可以使java虚拟机80%~90%的时间里执行被优化过的本地代码,只需要编译10%~20%对性能优影响的代码。
d、由硬件芯片组成,他用本地方法执行java字节码,这种执行引擎实际上是内嵌在芯片里的。
4.pc寄存器:JVM的PC寄存器的功能是存放将要执行指令的地址。每个JVM支持多个线程同时进行,每个JVM都有自己的PC寄存器。在任何一个点,每个JVM线程执行单个方法的代码,这个方法是线程的当前方法。如果方法不是native的,程序计数器寄存器包含了当前执行的JVM指令的地址,如果方法是 native的,程序计数器寄存器的值不会被定义。
5.栈
JVM是基于栈的虚拟机.JVM为每个新创建的线程都分配一个栈.也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。
6.堆:
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
6.1栈和堆的比较:
栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方 。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
栈的优势是,存取速度比堆要快 ,仅次于直接位于CPU中的寄存器。
堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。
7.方法区:
VM有一个被所有的线程共享方法区。方法区类似于传统语言的编译后代码的存储区,或者UNIX进程中的text段。它存储每个类结构例如常量池(constant pool),成员字段域和方法和构造函数,包含类和实例初始化和接口类型类型中用到的特殊方法的代码。
方法区在虚拟机启动时创建。尽管方法区在逻辑上时heap的一部分,简单的实现仍然可以选择对它既不回收也不压缩。
引用
JVM监控
1.jstatd
启动jvm监控服务。它是一个基于rmi的应用,向远程机器提供本机jvm应用程序的信息。默认端口1099。
2.jps
列出所有的jvm实例
3.jconsole
一个图形化界面,可以观察到java进程的gc,class,内存等信息。虽然比较直观,但是个人还是比较倾向于使用jstat命令
4.jinfo(linux下特有)
观察运行中的java程序的运行环境参数:参数包括Java System属性和JVM命令行参数
5.jmap(linux下特有,也是很常用的一个命令)
观察运行中的jvm物理内存的占用情况。
6.jstat
这是jdk命令中比较重要,也是相当实用的一个命令,可以观察到classloader,compiler,gc相关信息