Understanding the JVM:内存区域与内存溢出异常,Understanding the JVM:走进Java

C++编写的程序,编写的程序上,要为每个对象分派内存、回收内存;而Java编写的程序,JVM负责进行内存回收(又称垃圾回收,Garbage Collector,简称GC),不过JVM有可能出现内存泄漏和溢出方面的问题。

内存泄漏:不再使用的对象,一直占用内存空间,无法回收。

上面提到JVM对其分配的内存进行GC,那就涉及几个问题:

  1. JVM内存分为几个区域?
  2. 每个区域存储的内容?
  3. 每个区域可能引发的问题?

最终,需要回答一个问题:

JVM获取的内存空间:

  1. 为什么要分区域?
  2. 每个区域什么用途?没有行不行?

先来一张图:

JVM上运行一个program时,要存储很多东西:字节码文件、实例化的类对象、方法的传入参数、方法的返回值、局部变量、运算的中间结果等。JVM将这些需要存储的内容,以runtime data areas(运行时数据区)的形式进行划分。The Java Virtual Machine Specification(Java SE 8 Edition)中指出runtime data areas,具体包括:

  • PC(Program Counter) Register
  • JVM Stack
  • Native Method Stack
  • Java Heap
  • Method Area
  • Run-time Constant Pool

整体上,这些data areas可以划分为两类:thread私有空间和thread共享空间,具体如下图:

简单的说,PC Register是字节码文件的行号指示器,标识当前执行的字节码位置;具体:如果正在执行Java method,则计数器记录的是正在执行的VM字节码指令的地址;如果正在执行native method,则计数器为空(Undefined)。需要说明的是,在某一指定时刻,一个thread只能在执行一个method,即thread的current method只有一个,PC Register就是指向这一method的字节码。

PC Register为什么是thread私有的?CPU资源分配的最小单元是thread,多个thread轮流占用CPU的内核,这样thread的换入换出时,要求保存每个thread的执行状态,这样就无法共用PC Register。

JVM Stack,Java Virtual Machine Stack,是thread私有的,其用于存储frames(下文会详细讲解)。其中frame是method在runtime时的基本结构,其用于存储:局部变量表、操作数栈、动态链接、方法出口等信息(这些都是什么信息?)Method从调用到执行完成,对应frame的入stack和出stack动作。(请尽快整理JVM stack中存储的frame是什么东西,因为JVM specification中frame的介绍就是安排在JVM stack之后的)

notes(ningg):普通程序员,将JVM占用的内存空间,简单划分为:堆、栈;这是粗粒度的简单划分,实际要复杂的多;而,普通程序员所说的“栈”就是JVM stack,特别是其中的局部变量表。

局部变量表中存放的内容可能有:各种基本类型数据、对象引用(不是对象本身)、returnAddress类型(指向一条字节码指令的地址)。

针对JVM Stack可能出现两类错误:

  • 当需要的JVM stack深度大于VM所允许的最大深度时,StackOverflowError;
  • 若JVM stack设置为可动态扩展,则当扩展时,若无法申请到足够的内存,则OutOfMemoryError;

启动一个JVM process,其中会用到native method,此时,存储native method的执行状态,就需要一个native method stack,这与JVM stack类似,有一点差异:

  • JVM stack为执行java method(字节码)服务;
  • native method stack为指向native method服务;

JVM specification中并没有对native method stack的实现细节做出规定,无论是实现方式、数据结构都可以自由发挥;设置Sun HotSpot VM 将native method stack与JVM stack合二为一。与Native method stack相关的Error也有两种:StackOverflowError和OutOfMemoryError。

存放内容:对象实例、数组。实际上,最近JIT编译器技术,例如栈上分配、标量替换等技术导致并不是所有的对象实例和数组都必须在java heap中分配空间。Java Heap是GC的重点区域,从GC角度来看:当前流行分代收集算法,Java heap可以细分为:新生到、老年代,更细致一点,有Eden区、From Survivor区、To Survivor区;从内存分配角度,thread共享的Java Heap可以划分出许多thread私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),无论如何划分,目的只有一个:更好的回收内存、更好的分配内存。

通过两个参数可以设定Java Heap大小:-Xmx\-Xms。当Java Heap空间已满,并且无法扩展时,会抛出OutOfMemoryError。

Method Area,称作“方法区”,其用于存储:类信息、常量、静态变量、JIT编译器编译后的代码等。实际上,JVM specification将Method Area看作存储compiled code的区域,其中还将method area表述为java heap的一个逻辑部分,实际上HotSpot VM的实现中,将Method Aera称为java heap的“永久代”(Permanent Generation),不过Method Area与Permanent Generation并不等价,只是HotSpot利用Permanent Generation的方式来实现Method Area而已,利用Permanent Generation方式来实现Mehtod,并不完美,容易遇到内存溢出(OutOfMemoryError)错误;目前JDK 1.7 中,已经将字符串常量池从Permanent Generation中移除。

Runtime Constant Pool是Method area的一部分。需要简要说明一下class文件包含的内容:类的版本、字段、方法、接口等描述信息,还有常量池(Constant Pool Table)。其中,常量池,用于存放编译期产生的各种字面常量和符号引用。(前面什么意思?)

notes(ningg):在JDK 1.7、JDK 1.8中Runtime Constant Pool还是Method area的一部分吗?

  1. 不是的,JDK 1.7 开始,常量池迁移到了直接内存中;
  2. JDK 1.8 开始,永久代的概念也去掉了,方法区,从永久代迁移到了直接内存

特别提一下:class文件中常量池(Constant Pool Table)与运行时常量池(Runtime Constant Pool)的区别。执行程序的时候,会将class文件的constant pool table中内容存放到Runtime constant pool中,但runtime constant pool具有动态性,其中内容并不仅限于class文件的constant pool table,程序运行期间,可将新的常量放入池中,常见的比如String的intern()方法。

Direct Memory,不是JVM runtime data areas的一部分,也不是JVM specification中定义的内存区域。但这一内存区域,也被频繁使用,也会导致OutOfMemoryError,因此,将Directory放到此处一并进行说明。

常用场景

JDK 1.4 引入NIO(New Input/Output)类,引入基于通道(channel)和缓冲区(buffer)的I/O方式,可使用Navtive函数库直接分配堆外内存,然后通过java heap中的DirectByteBuffer对象作为这块内存的引用,进行操作。这一实现方式,可以避免Java Heap与Native Heap之间来回复制数据带来的性能损耗。

Note:JVM GC 过程中,Full GC 时,会顺便清理一下直接内存的空间。

上面提到的只是大致的JVM 内存区域划分情况,具体:对象是如何创建的、内存中如何分布、如何访问这个对象,需要进一步参考深入理解Java虚拟机的2.3 “HotSpot虚拟机对象探秘”章节。还有,书中每个章节的实战部分,需要用心去实践一下,理论要有,实际操作上:参数配置、问题定位与修正,这个能力是工作能力,也要有。

看看JVM最具权威的官方文档(Java Language and Virtual Machine Specifications)吧,这是所有JVM相关信息的最初来源,其他绝大多数知识都是对其的理解和重新表述,当然不同的人的理解也有误差。

预告:下一篇文章,介绍GC,大概想了一下,几个问题:如何确定对象可以被回收?如何进行回收?

Understanding the JVM:走进Java

从Java语言的几个特点说起:

Java:Write Once,Run Anywhere,归根结底是在JVM上运行的。整体几个过程:

  • 编写.java源文件
  • 编译为.class字节码文件 (javac 命令)
  • .class文件在JVM上运行

即,“一次编写,到处执行”的口号,实际是,有JVM的地方才能执行,因为其依赖于JVM。关于上述3个过程,有几个小问题:

  • 直接用JVM执行.java文件不行吗?为什么需要.class文件?
  • java语法
  • class文件格式
  • JVM怎么运行的class文件(加载过程)
  • 利用class文件能否得到java文件

Java提供相对安全的内存管理和访问机制,主要是几个术语:

  • 垃圾回收
  • 内存模型(数据摆放)
  • 指针越界(引用,reference)

疑问:程序占用的内存,不用主动释放?例如:obj = new Class();

Java实现了热点代码检测、运行时编译及优化,能够实现:Java应用程序随应用运行时间的增加而获得更高的性能。

疑问:

  • 一边运行,一边编译?JIT(Just In Time)及时编译器?
  • 编译器的作用是什么?.class文件转换为二进制机器代码?

Java有丰富多样的第三方类库,实现各种功能。(举几个例子?)

说两个术语JDK和JRE:

  • JDK:Java Development Kit,Java开发工具集,是支持程序开发的最简环境,包含几个部分:
    • Java语言
    • JVM
    • Java API类库(自带)
  • JRE:Java Runtime Env.,Java运行环境,是Java程序运行的标准环境,包含几个部分:
    • JVM
    • Java API类库中Java Standard Edition(Java SE)API子集

再说两个称呼吧:

  • 在JDK 1.2 版本后,Java拆分为3个方向:
    • J2SE(Java 2 Plantform,Standard Edition)面向桌面应用开发;
    • J2EE(Java 2 Plantform,Enterprise Edition)面向企业级开发;
    • J2ME(Java 2 Plantform,Micro Edition)面向移动端开发;
  • JDK 1.6 版本终结了上述J2XX的命名方式,对应启用如下命名:
    • Java SE 6;
    • Java EE 6;
    • Java ME 6;
  • JAVA,1995年诞生;
  • JDK 1.0(1996),纯解释执行的Java虚拟机实现(Classic VM);
  • JDK 1.2(1998),是个里程碑:
    • 拆分为J2SE、J2EE、J2ME共计3个方向;
    • 首次在JVM中内置JIT编译器;
  • JDK 1.4(2002),新特性:
    • 正则表达式
    • 异常链
    • NIO
    • 日志类
    • XML解析器
    • XSLT转换器
  • JDK 1.5(2004),重大改进:
    • 语法层面上,增强易用性:
      • 自动装箱?
      • 泛型?
      • 动态注解(有静态注解吗?)
      • 枚举?
      • 可变长参数?
      • 遍历循环(foreach循环)
    • 虚拟机和API层面上,重大改进;
      • Java的内存模型(Java Memory Model)?
      • 并发包:java.util.concurrent?
  • JDK 1.6(2006),几个方面:
    • 命名上:
      • J2EE – JAVA EE 6
      • J2SE – JAVA SE 6
      • J2ME – JAVA ME 6
    • 语法层面上:
      • 提供动态语言支持(通过内置Mozilla JavaScript Rhino引擎实现)
      • 提供编译API?
      • 微型HTTP服务器API?
    • Java虚拟机层面上:
      • 锁与同步
      • 垃圾收集
      • 类加载等的算法
    • 开源工作进展上:
      • 2006.11开始逐步开源JDK的各个组件
      • OpenJDK 1.7 与 Sun JDK 1.7 的代码基本一致
  • JDK 1.7:
    • 语法层面:
      • switch 支持 String
      • try-with-resource 语法支持,所有实现 java.lang.AutoCloseable 接口java.io.Closeable 接口的对象,都可以使用
      • 单行 catch 多种异常
      • 创建泛型时,类型推断
  • JDK 1.8:
    • 语法层面:
      • Lambda 表达式
      • 函数式接口:接口中只包含一个方法(不包含静态方法和默认方法)
    • JVM 层面:
      • 去除了永久代的概念
      • 方法区,直接存储在直接内存

几个熟知的Java Virtual Machine:

  • Classic VM
  • Exact VM
  • HotSpot VM

重点说一下HotSpot VM,两个点:

  • 准确式内存管理(Exact Memory Management)
    • VM知道内存中某个位置的数据具体什么类型
    • 例如:123456是整型还是引用,如果将123456位置的数据移动,则知道内存中哪些123456需替换;
  • 热点代码检测,具体:
    • 执行计数器
    • 找出最具编译价值的代码
    • JIT编译器以方法(Method)为单位,进行编译

疑问:编译器、解释器,作用是什么?(以java编写、执行过程来说明)

说一个有意思的:Java语言能够实现Java语言本身的运行环境吗?几个思考:

  • 什么叫Java语言的运行环境?其提供什么作用?
  • Java语言能够实现这一功能吗?

JVM,java运行环境,语言自身实现其运行环境,元循环,Meta-Circular,两个例子:JavaInJava虚拟机,Maxine VM。

说几点吧,列一下清晰一些:

  • Dalvik VM,只能被称为”虚拟机”,而不是”JAVA 虚拟机”
  • Dalvik VM没有执行Java虚拟机规范
  • 不能直接执行Java的Class文件
  • 寄存器架构,不是JVM中常见的栈架构

思考:寄存器架构,和栈架构之间的区别?

简单说一下:指令集不同

Dalvik VM与Java之间有千丝万缕的联系:

  • Dalvik VM的执行文件dex文件,可以通过class文件转换得到;
  • 使用Java语法编写应用程序
  • 可以直接使用大部分Java的API
  • Android 2.2中已经实现及时编译器;

背景

CPU硬件的发展方向,已经从单CPU高频率转向为多核心,随着多核时代来临,软件开发越来越关注并行编程领域。

JAVA API与Lambda函数式编程

JDK 1.5中引入java.util.concurrent包,实现一个粗粒度的并发框架;JDK 1.7中引入java.util.concurrent.forkjoin包,Fork/Join模式是处理并行编程的经典方法。

疑问:Fork/Join模式,是经典的并行编程方法?怎么说?

  1. Fork/Join,是分治思想的一种,跟 Map/Reduce 类似
  2. Fork/Join,以计算能力为中心,充分利用空闲的计算能力,一个任务队列执行完了,就从其他任务队列偷取任务;
  3. Map/Reduce,以数据为中心,认为所有的线程执行能力是等同的,只要给相同的数据量,处理时间就相同;

极其重要进展:Java 8 中,提供Lambda函数式编程,函数式编程的重要有点是:程序天然的适合并行运行;这将有助于Java在多核时代继续保持主流语言的地位。

疑问:函数式编程语言,为什么,天然适合并行运行

  1. 函数式编程,基本但愿是函数,函数是无状态的,始终是线程安全的
  2. 线程安全的代码,可以任意的分布在多核 CPU 上运行

背景

主流CPU开始支持64位架构,一个问题:32位、64位CPU是怎么衡量的?

效果

64位虚拟机有什么好处?更多的计算资源?更多的内存空间?超过4G的内存空间,就必须要使用64位JVM?

与32位JVM相比,64位JVM几个地方需要考虑:

  • 指针膨胀;
  • 各种数据类型对齐补白;

上面的结果是:64位JVM占用更多内存(额外10%~30%)。(具体什么原因?)

JDK的很多底层方法是本地化的(native),如何跟踪这些方法?需要编译一下JDK源码。

JAVA 命令参数详解:-D

JAVA 命令参数详解:-D_枝叶飞扬_新浪博客 (sina.com.cn)

JAVA 命令参数详解:

1、-D<name>=<value> set a system property  设置系统属性。

     java命令引入jar时可以-cp参数,但时-cp不能用通配符(多个jar时什么烦要一个个写,不能*.jar),面通常的jar都在同一目录,且多于1个。前些日子找到(发现)-Djava.ext.dirs太好。

如:

java -Djava.ext.dirs=lib MyClass  

 

可以在运行前配置一些属性,比如路径什么的。

java -Dconfig="d:/config/config.xml" Abc

这样在Abc中就可以通过System.getProperty("config");获得这个值了。

 

 

在虚拟机的系统属性中设置属性名/值对,运行在此虚拟机之上的应用程序可用
当虚拟机报告类找不到或类冲突时可用此参数来诊断来查看虚拟机从装入类的情况。

另外,javac -d <目录> 指定存放生成的类文件的位置

Standard System Properties

 

Key Meaning
"file.separator" Character that separates components of a file path. This is "/" on UNIX and "\" on Windows.
"java.class.path" Path used to find directories and JAR archives containing class files. Elements of the class path are separated by a platform-specific character specified in the path.separator property.
"java.home" Installation directory for Java Runtime Environment (JRE)
"java.vendor" JRE vendor name
"java.vendor.url" JRE vender URL
"java.version" JRE version number
"line.separator" Sequence used by operating system to separate lines in text files
"os.arch" Operating system architecture
"os.name" Operating system name
"os.version" Operating system version
"path.separator" Path separator character used in java.class.path
"user.dir" User working directory
"user.home" User home directory
"user.name" User account name

 

 

 

所谓的 system porperty,system 指的是 JRE (runtime)system,不是指 OS。

System.setProperty("net.jxta.tls.principal", "client");
System.setProperty("net.jxta.tls.password", "password");
System.setProperty("JXTA_HOME",System.getProperty("JXTA_HOME","client"));
可以利用系统属性来加载多个驱动

posted @ 2021-12-11 22:48  CharyGao  阅读(11)  评论(0)    收藏  举报