Java虚拟机知识介绍(一)

来源:互联网 发布:java 线程状态 编辑:程序博客网 时间:2024/05/21 03:57

虚拟机是什么?

我们的Java代码在运行之前,会先由Javac进行编译,将其转换为字节码,然后经由另一个程序来进行读取和运行,这个程序便是虚拟机JVM,它将字节码转为二进制的机器语言由机器运行。(为什么要用一个程序即虚拟机来运行这些代码而不是直接在机子上跑呢?个人理解是为了使不同平台都能运行相同的Java代码,只改变不同平台虚拟机的实现,而Java代码原封不动即可,这样便实现了Java的跨平台特性)

虚拟机中的内存结构

虚拟机运行在一段内存上,并将该内存分成了不同的数据区域

程序计数器:当前线程执行的字节码的行号指示器,通过改变计数器的值,指示下一条语句的位置,如循环语句再次循环,计数器又循环体尾部指回循环体头部。每条线程都有一个独立的程序计数器。如果正在执行Java方法,计数器值为字节码指令地址,如果是Native方法,计数器值为空。
注:该区域没有OOM。

虚拟机栈(简称”栈”):Java方法调用时会在该栈中创建一个栈帧,存放着局部变量表、操作数栈、动态链接、方法出口等,其中局部变量表是我们主要关心的。当某个方法执行完成时出栈。该栈线程私有,生命周期与线程相同。当超过最大栈深度时会报StackOverflow,申请不到足够内存时报OOM。

本地方法栈:与虚拟机栈十分相似,区别在于存放的是Native方法

线程共享的一块较大的内存,存放着对象实例以及数组,为了更好的管理堆,有了新生代和老生代的区域区分,新生代又细分为:Eden、FromSurvivor、ToSurvivor区。当堆中没有内存可以完成实例分配,又不可以再扩展内存时,将会报OOM异常。

方法区线程共享的一块内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。类的class文件中有一项信息是常量池,存放编译期生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池中存放。

直接内存:直接内存不是虚拟机运行时数据区,不过被JDK1.4新加入的NIO(New Input/Output)使用到,它使用Native函数库直接分配堆外内存,也会报OOM异常。

虚拟机创建对象的过程

我们写Java代码时,创建一个对象只要使用new关键字就可以了,但是对象的创建是一个怎样的过程我们可能一概不知……

当我们使用了new关键字创建一个对象时,虚拟机将通过这个指令的参数,找到该类的符号引用,检查这个类是否被加载、解析、初始化。没有的话,就会去加载该类。然后,将为对象分配内存,不同的虚拟机,有不同的内存清理方式,也对应了不同的内存分配方式。如带Compact整理过程的回收机制,对应了Bump the Pointer指针碰撞的分配方式;Mark-Sweep过程的回收机制,对应了FreeList空闲列表的分配方式。内存分配完成,虚拟机要将内存空间都初始化零值,然后再对对象的对象头进行必要设置,如:所属的类、类元数据信息、哈希码、GC年龄。最后,会执行< init >指令,进行对象初始化(构造函数)。

注意:分配内存也是线程不安全的,像指针碰撞,如果在分配一个对象内存时还未重设指针位置,此时另一个线程又分配了一个对象,则该指针位置将发生错乱。所以,虚拟机采用CAS配上失败重试的方式保证操作的原子性,或者让每个线程拥有自己的缓冲区,分配对象内存时在该缓冲区上面分配,即可避免内存段的冲突。

对象的内存结构

对象的内存结构分为三大部分:对象头、实例数据、对齐填充

对象头:对象头也包含了两个部分。第一部分包括运行时数据:哈希码、GC年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳。第二部分是类型指针,即指向它的类元数据的指针。如果是数组,则对象头中存了数组大小。

实例数据:存储实例字段的内容,无论是父类的还是自己的,但存储顺序父类先于子类,然后字段类型宽度长到短进行分配,也有可能根据JAVA源码中的定义顺序,不同虚拟机可能有不同的实现。

对齐填充:起占位符作用,因为对象都要求是8字节的整倍数,对象头已经是8bit的整倍数,因此当实例数据不满足条件时,会由对齐填充来补齐。

对象的定位

我们都知道Java中拿到一个对象都是通过引用的方式来获取,引用存放在虚拟机栈中,有两种方式来实现引用定位:句柄池直接指针

句柄池:在堆中划分一块内存来作为句柄池,每个句柄存放着对象实例数据的指针和类型数据的指针,优点在于,当对象实例的位置改变后,只需改动句柄中的对象实例数据的指针即可。

直接指针:虚拟机栈中的引用直接指向该对象实例,对象实例数据中存放了类型数据的指针。优点在于比句柄池少了一次指针定位,在频繁访问对象时可以提高不少效率。

Out Of Memory Error异常

虚拟机中除了程序计数器外,其它的区域都是有可能会发生OOM异常的。OOM异常顾名思义就是内存溢出,即当某个内存区域,所申请的内存加上已用内存超过了虚拟机限制的最大内存时,会抛出该异常。(粗疏的讲,就是内存不够用了)。

堆溢出:不断创建对象直到超过限制的大小,可通过Eclipse Memory Analyzer看内存信息:内存泄露对象和GC Roots引用链,若没有内存泄露,则考虑增加最大内存的限制。报错提示Java heap space

虚拟机栈和本地方法栈溢出:单线程时,无论是超过最大深度,还是分配内存不够了,都会报StackOverflow,而多线程时,则都会报OOM,提示unable to create new native thread。因为每个线程都分配好了栈的内存,当开启线程过多,新开的线程申请不到所需的内存,则会抛出OOM,解决方法可以考虑增大栈内存,如果不能增大,可以考虑减少每条线程的虚拟机栈内存大小换取更多线程的使用。
注:减少最大堆内存也可以,因为虚拟机栈内存=总内存-Xmx-MaxPermSize

方法区和常量池溢出:原因考虑常量过多且无法被回收,或者加载的类过多没有及时卸载。报错提示PermGen space

直接内存溢出:unsafe类的allocateMemory方法申请的内存超过系统限制,主要出现在NIO的使用中,该原因导致的报错在HeapDump文件中看不到明显的异常

原创粉丝点击