为什么Java能够跨平台
是因为Java程序编译之后的代码不是能够被硬件系统直接运行的代码,而是一种"中间码"-字节码, 然后不同的的硬件平台上安装有不同的Java虚拟机(JVM),有JVM来把字节码再翻译成对应的硬件平台能够执行的代码,所以Java是夸平台的。
Java虚拟机及内存分区
1、 jvm简介
Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
如下图所示,JVM体系中包含了几个主要的子系统和内存区
垃圾回收器(Garbage Collection):负责回收堆内存(Heap)中已经不再使用的对象,即这些对象已经没有被引用了。
类装载子系统(Classloader Sub-System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。
执行引擎(Execution Engine):负责执行那些包含在被装载类的方法中的指令。
** 运行时数据区(Java Memory Allocation Area):**又叫虚拟机内存或者Java内存,虚拟机运行时需要从整个计算机内存划分一块内存区域存储许多东西。例如:字节码、从已装载的class文件中得到的其他信息、程序创建的对象、传递给方法的参数,返回值、局部变量等等。
2、内存分区
从上节知道,运行时数据区即是java内存,而且数据区要存储的东西比较多,如果不对这块内存区域进行划分管理,会显得比较杂乱无章。根据存储数据的不同,java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。
程序计数器(Program Count Register):又叫程序寄存器。JVM支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)。如果线程正在执行的是一个Java方法(非native),那么PC寄存器的值将总是指向下一条将被执行的指令,如果方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。
方法区(Method Area):当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被所有线程共享。虽然JVM规范把方法区描述为堆得一个逻辑部分,但是他有一个别名叫Non-heap(非堆),目的应该是与Java堆区分开。
方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆(Heap):Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存,但是这个对象的引用却是在栈(Stack)中分配。
注:jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈(Stack):又叫堆栈。JVM为每个新创建的线程都分配一个栈。也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧,这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。其相关设置参数:
-Xss --设置方法栈的最大值
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
本地方法栈(Native Stack):存储本地方方法的调用状态。
JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你无法用Java代码区明确释放一个对象一样。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存,通常,虚拟机把这个任务交给垃圾收集器(Garbage Collection)。其相关设置参数:
-Xms -- 设置堆内存初始大小
-Xmx -- 设置堆内存最大值
-XX:MaxTenuringThreshold -- 设置对象在新生代中存活的次数
-XX:PretenureSizeThreshold -- 设置超过指定大小的大对象直接分配在旧生代中
3、类加载过程
启动一个Java程序时,会通过Javac编译程序调用Java启动JVM(编译过程没有深入研究)
类加载过程其实就是JVM把class文件加载到内存,并对数据进行校验,解析和初始化,最终形成JVM可以直接使用Java的过程。总共可以分为加载、链接、初始化三部。
- 加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时2进制数据结构。在堆中生成一个代表这个类的Java.lang.Class对象,作为方法区类数据的访问入口。 - 链接
将Java类的二进制代码合并到JVM的运行状态之中的过程,链接有分为三个小步
1、验证
确保加载的类的信息符合JVM规范,有没有安全方面的问题
2、准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法去中分配
3、解析
虚拟机常量池内的符号引用替换为直接引用的过程 - 初始化
初始化阶段是执行类构造器<clinit>()方法的过程,类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先对其父类初始化
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步
当访问一个类的静态域时,只有真正声明这个域的类才会被初始化。
下面通过具体的例子来分析下类加载过程
public class AppMain // 运行时, jvm 把appmain的信息都放入方法区
{
public static void main(String[] args) // main 方法本身放入方法区。
{
Sample test1 = new Sample(" 测试1 "); // test1是引用,所以放到栈区里,
// Sample是自定义对象应该放到堆里面
Sample test2 = new Sample(" 测试2 ");
test1.printName();
test2.printName();
}
}
类sample
public class Sample // 运行时, jvm 把appmain的信息都放入方法区
{
/** 范例名称 */
private String name; // new Sample实例后, name 引用放入栈区里, name 对象放入堆里
/** 构造方法 */
public Sample(String name) {
this.name = name;
}
/** 输出 */
public void printName() // print方法本身放入 方法区里。
{
System.out.println(name);
}
}
如上图所示,系统收到我们发出的指令后,启动一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把APPMain类的类信息存放到运行时数据区的方法区中,然后在栈里面执行main()方法。
如果想深入理解Java虚拟机可以阅读深入理解Java虚拟机这本书