目录
2.1. 引导类加载器(Bootstrap ClassLoader)
2.2. 扩展类加载器(Extension ClassLoader)
2.3. 系统类加载器(System ClassLoader)
3.4. 本地方法栈(Native Method Stack)
3.5. 程序计数器(Program Counter Register)
JVM(Java Virtual Machine)即Java虚拟机,是Java语言的运行环境。它是一个虚构出来的计算机,用于运行Java程序。JVM将Java字节码转换成机器语言,使Java程序在不同平台下运行。JVM的基本概念包括:
1. Class文件
编译 Java文件被转换为.class文件,包含JVM指令,这些指令被称为Java字节码。
Java的class文件是编译后的字节码文件,由JVM执行。class文件具有以下结构
1.1. 魔数
前4个字节为固定值0xCAFEBABE,用于确定这个文件是否为一个class文件。
1.2. 版本号
接下来的4个字节表示Class文件的版本号,用于兼容不同版本的JVM。
1.3. 常量池
存放编译期生成的各种字面量和符号引用,相当于class文件的资源仓库。
1.4. 访问标志
用于描述该Class文件trigram的访问权限及属性。
1.5. 类索引、父索引和接口索引
分别给出该Class引用的类名、父类名和接口名称。
1.6. 字段表集合
用于描述类内所定义的变量。包括字段名、类型和访问标志。
1.7. 方法表集合
用于描述类内的方法,包括方法名、返回值类型、参数类型、代码等。
1.8. 属性表集合
用于描述一些附加信息,比如注释、调试信息等。
1.9. 总结一下
- 魔数和版本号:确定是否为class文件以及兼容性。
- 常量池:包括数字、字符串、类名、字段名、方法名等常量,是解析class文件的基础。
- 访问标志:描述访问权限如public、private等。
- 类索引、父索引和接口索引:描述类的继承关系。
- 字段表:描述类的属性,包括属性名、类型和访问标志。
- 方法表:描述类的方法,包括方法名、返回值类型、参数列表、异常表、访问标志以及方法体(Java字节码)。
- 属性表:描述附加属性,如注解、调试信息等,不是必须的。
class文件由以上几部分构成,这就是Java程序在编译后代表的样子,存储在磁盘上。
当执行Java程序时,JVM会读取class文件,解析其中信息,然后执行其中的方法体部分,这就是Java代码运作的基础。理解class文件格式,有助于我们进一步理解JVM的工作机制,以及Java虚拟机是如何加载和执行我们的Java程序的。这也是成为一名合格的Java工程师必备的知识。
2. 类加载器
Java类加载器负责将class文件加载到JVM中,类加载器使用委托模型进行加载。
2.1. 引导类加载器(Bootstrap ClassLoader)
负责加载Java的核心库,如rt.jar、resources.jar、charsets.jar等。由C++实现,无法直接获取。
2.2. 扩展类加载器(Extension ClassLoader)
负责加载Java的扩展库,默认加载lib/ext目录中的jar包。
2.3. 系统类加载器(System ClassLoader)
负责加载Java应用的类路径(classpath)所指定的jar和类文件。一般加载环境变量CLASSPATH或-cp/-classpath选项指定的位置。
2.3. 类加载器工作过程
类加载器使用父委托模型进行加载:每个类加载器都有一个父类加载器,在加载某个类时,会先请求父类加载器进行加载,每个类加载器都会沿着父类链最终请求引导类加载器进行加载。
类加载器加载类的过程如下:
1. 首先,它查找该类是否已经被加载,如果被加载则直接返回。
2. 然后,它将请求父类加载器进行加载,直到引导类加载器。如果被加载则返回,否则继续下一步。
3. 最后,系统类加载器将查找CLASSPATH中是否存在该类,存在则将其读取进内存,并将其加载到JVM中。
类加载器采用上述加载规则有两个主要目的:
1. 避免类的重复加载。如果一个类被加载了两次,可能会产生问题。
2. 委托模式使得Java类按着父类链排序的反序进行加载。 父类先加载,子类后加载。 这样可以确保一个类在被子类引用前已经被加载。
类加载器在Java类加载和运行机制中起着核心作用。理解Java的类加载机制,有助于我们更好地设计程序结构,编写出更健壮的代码。同时,也能够在程序出现NoClassDefFoundError、ClassNotFoundException等异常时,更好地定位问题原因。
类加载器是Java程序动态加载类的关键,是JVM架构中最基础的一部分。
3. 运行时数据区
Java运行时数据区(Runtime Data Area)是JVM为每条运行线程分配的内存空间。它由多个部分组成,用于存储类信息、对象实例、方法信息等。主要有以下几个区:
3.1. 方法区(Method Area)
用于存储类信息、常量、静态变量等。对每个JVM运行实例只有一个方法区。
3.2. 堆(Heap)
用于存储new出来的对象实例,由垃圾回收器管理。对每个JVM运行实例只有一个堆。
3.3. 栈(Stack)
用于存储方法的调用栈。每个线程有一个栈,用于存储局部变量表、操作栈、动态链接、方法出口等信息。
3.4. 本地方法栈(Native Method Stack)
与本地方法相关,用于JNI。每个线程有一个本地方法栈。
3.5. 程序计数器(Program Counter Register)
每条线程都有一个PC寄存器,用于存储当前线程正在执行的JVM指令的地址。
3.6. 总结
方法区和堆是各个线程共享的,其余区域对每个线程都独立分配。这些区域的详细作用如下:
- 方法区: 用于存储Class信息、静态变量和常量等。可以由多个线程共享,且在JVM运行期只有一个方法区。
- 堆: 用于存储对象实例,由GC回收管理。多个线程共享,且只有一个堆。
- 栈: 用于存储方法的调用栈,包括局部变量表、操作栈、动态链接和方法出口等信息。每个线程有一个栈。
- 本地方法栈: 用于存储JNI相关信息。每个线程有一个本地方法栈。
- 程序计数器: 当前线程所执行的字节码的行号指示器。每个线程都有一个PC寄存器。
理解Java运行时数据区的作用和功能,有助于我们分析调试程序,解决内存溢出等问题。这也是成为一名合格的Java工程师必备的知识。对JVM运行时数据区有一个清晰的认识,可以更好地设计程序并发现潜在问题。
4. 垃圾收集器
Java垃圾收集器负责回收Java堆中不再使用的对象,释放内存空间。Java提供了多种垃圾收集器,主要包括:
4.1. Serial收集器
单线程收集器,会暂停整个应用程序执行GC。适用于单CPU小型应用。
4.2. ParNew收集器
Serial收集器的多线程版本,适用于多CPU系统。
4.3. Parallel Scavenge收集器
同样多线程,吞吐量优先。新生代收集器。
4.4. CMS收集器
Concurrent Mark Sweep,并发标记清除。多线程,可进行内存碎片整理。老年代收集器。
4.5. G1收集器
Garbage First,混合收集器。可用于新生代和老年代,将堆分为多个独立的子区域进行收集。
上述收集器可以搭配使用:
- Serial + CMS:新生代Serial,老年代CMS,用于小内存应用。
- ParNew + CMS:新生代ParNew,老年代CMS,用于多CPU和大内存应用。
- Parallel Scavenge + CMS:新生代Parallel Scavenge,老年代CMS,吞吐量优先的方案。
- G1:可独立使用,全堆收集器,适用于大容量内存和多CPU的场景。
收集器 throughput(吞吐量)和 latency(延迟)之间需要权衡。两者的区别在于:
- Throughput: running time中的非GC时间/(GC时间 + 非GC时间)。吞吐量越高越好。
- Latency: GC暂停时间,应尽量短。
CMS和G1是以Latency为主的收集器; Parallel Scavenge注重Throughput。
选择合理的垃圾收集器对系统性能至关重要。需要根据应用场景选择,考虑到程序堆大小、GC暂停时间要求和吞吐量需求等因素。
理解Java垃圾收集机制和收集器之间的区别,有助于我们根据实际应用场景选择最优的配置,调优程序的运行效率。这也是成为一名高级Java工程师必备的知识。
5. 执行引擎
负责执行指令,将Java字节码转化为机器语言。使用栈式架构,通过PC寄存器获取指令,然后执行。
5.1 JVM执行引擎的组成
1. 类加载器(ClassLoader):负责将class文件加载到JVM中,并将其转换成JVM可以识别的格式。
2. 方法区(Method Area):存储已加载的类信息,常量,静态变量等。
3. 堆(Heap):用于存储对象实例,是GC的主要区域。
4. 栈(Stack):用于存储方法调用的信息,本地变量等。每个线程都有自己的栈空间。
5. 程序计数器(Program Counter Register):用于存储正在执行的方法的地址,并指向下一条指令。
6. 寄存器(Registers):用于缓存局部变量和计算结果。
7. 垃圾收集器(Garbage Collector):用于回收垃圾对象,释放堆空间。
5.2 JVM执行一段Java代码的过程
1. 类加载器将class文件加载到方法区中,生成Class对象。
2. JVM创建对象实例并存放在堆中。
3. 每个线程在自己的栈空间中创建一个栈帧用于存储方法调用的信息。
4. 程序计数器记录当前执行到的字节码指令地址。
5. 解释器依次执行每条字节码指令:
- 访问常量池和方法区
- 操作堆中的对象
- 访问寄存器和栈帧
- 更新程序计数器
6. GC线程定期执行垃圾收集,回收垃圾对象,释放堆空间。
7. 执行引擎重复上述流程,直到所有的线程执行结束。
这就是JVM执行Java程序的整体过程,通过各个组件的配合完成代码的解释和执行。
6. Native接口
JNI(Java Native Interface),将Java程序和本地应用程序连接,是Java调用Native方法的机制。它允许Java代码和Native代码(如C/C++)相互调用。
6.1 JNI的主要用途
1. 与平台相关的功能:像注册事件处理等。
2. 已有的库的再利用:比如 need 执行复杂的数字处理,可以调用C/C++的科学计算库。
3. 性能关键部分:将关键部分用Native语言实现,可以获得更好的性能。
6.2 JNI的主要组成部分
1. Native方法:在Java代码中使用native关键字声明,实现在Native代码中。
2. JNI框架:包含jni.h头文件,各种JNI函数等。用于在Native代码中调用Java方法,访问Java对象等。
3. JNI接口:JNIEnv接口,代表JNI接口指针,用于调用JNI函数。每个Native线程都有自己的JNIEnv指针。
4. Java VM接口:JNI_CreateJavaVM函数用于创建JVM,获得JNIEnv接口指针。
6.3 JNI的开发步骤
1. 在Java中声明Native方法,使用native关键字。
2. 使用javah工具生成Native方法的头文件。
3. 实现Native方法,调用JNI函数。
4. 动态加载Native库,调用System.loadLibrary。
5. 编译Native库,生成so文件或dll文件。
6. 在Java程序运行时,会调用System.loadLibrary加载Native库,然后执行Native方法。
JNI是Java调用Native功能的重要机制,但影响Java的可移植性,使用时需慎重考虑。仅在必要时使用JNI,性能关键情况或需要使用平台特性时使用。
7. 安全管理器
JVM安全管理器(SecurityManager)是Java安全机制的基石,它限制Java代码执行潜在危险的操作。
用于实现Java的安全机制,防止恶意程序干扰Java程序或访问机密数据。
当一个Java程序启动时,默认情况下是没有安全管理器的。如果需要启用安全机制,需要设置一个安全管理器。
7.1 安全管理器主要安全控制
1. 文件访问:限制读写文件,目录列表等操作。
2. 网络连接:限制发起网络连接,监听端口等。
3. 类加载:限制加载类,限制package访问等。
4. 系统属性访问:限制获取,设置系统属性。
5. AWT访问:限制窗口关闭,鼠标键盘操作等。
6. 包访问权限:限制跨域的package访问。
安全管理器通过检查权限来控制这些敏感操作。开发人员需要显式地请求所需的权限,安全管理器才会授予执行权限。
7.2 启用安全管理器的方式
1. 在命令行启动Java程序时,指定-Djava.security.manager选项。
2. 在代码中调用System.setSecurityManager()方法设置。
7.3 请求操作权限
1. 如果操作在安全管理器检查之前执行,则无需权限(如在main方法前)。
2. 调用SecurityManager的checkXXX()方法来请求权限,如果权限允许,check方法返回正常,否则抛出SecurityException。
3. 权限可以在policy文件中配置,也可以在代码中调用Policy.setPolicy方法设置。
policy文件格式如下:
grant {
permission java.io.FilePermission "myfile", "read,write";
};
这是一个非常简单但功能强大的安全框架。Java领先于其它语言之处在于,它从一开始就考虑到了安全问题,并提供面向开发者的简单易用的安全框架。
安全管理器为Java应用提供了不错的安全性保护,我们开发安全敏感的Java应用时,应该积极使用它。