深入理解java虚拟机笔记(第二章:java内存区域)

最近在看《深入理解java虚拟机》,记录了一些笔记。根据原文用自己的语言进行记录的,有不准确的地方。如感兴趣请看原著。

一,java虚拟机运行时内存区域介绍:
java虚拟机运行时内存区域包括: 方法区(method area),虚拟机栈(vm stack), 本地方法栈(native method stack), 堆(heap), 程序计数器(program counter register)


1.程序计数器:
它是当前线程执行字节码的行号指示器。字节码的解释器工作就是通过改变程序计数器的值来选取下一条需要执行的指令。分支,循环,异常等基础功能都是依赖于程序计数器。
java虚拟机的多线程是通过处理器轮询线程分配时间的方式进行的。一个时刻里处理器只能处理一个线程的一条指令。因此为了切换后能恢复到正确的位置,每个线程需要拥有自己的程序计数器。每个线程的程序计数器相互独立,互不影响,所以该内存称为“线程私有”内存。
当线程执行的是java方法,程序计数器里记录的是字节码指令地址。如果执行的是native方法。程序计数器记录的是undefined.

2.java虚拟机栈
java虚拟机栈也是线程私有,它的生命周期和线程相同。java虚拟机栈是方法执行的内存模型。方法在执行的时候会创建一个栈帧,用于存储局部变量表,方法出口,动态链接等信息。从一个方法调用到执行完成的过程就是栈从入栈到出栈的过程。
局部变量表中存储了基础数据类型,对象引用和returnAddress(一条字节码指令的地址)。其中64位长度的double和long占用两个局部变量空间(slot),其他类型只占用一个。在编译期局部变量表在栈中分配的空间已经确定。方法执行时局部变量表的大小不会改变。
该区域存在两个异常:1,当线程请求的栈的深度大于虚拟机允许的深度。则抛出stackoverflowexception。2, 如果栈可以扩展,但是申请的内存不足则抛出outofmemoryexception

3.本地方法栈
本地方法栈于虚拟机栈的功能类似。区别在于虚拟机栈为虚拟机执行java方法服务。而本地方法栈为虚拟机执行native方法服务。虚拟机规范规定本地方法栈中使用的语言和数据结构不受限制。虚拟机自由实现。

4.java堆
java堆是虚拟机里最大的一块内存,它是线程共享的,在虚拟机启动时创建。所有的对象和数组等都创建在该区域上。
java堆是垃圾回收器工作的主要区域,所以有时称为“GC堆”。java堆分为新生代,老年代。从分配角度看。线程共享的java堆有时会分配多个线程私有分配缓冲区。无论如何分配与分配对象无关。只是为了更方便的存储,分配。
根据虚拟机规范,在物理上java堆可以是不连续的。只要逻辑上连续即可。当堆没有内存进行分配也不能进行扩展后,就会抛出outofmemoryerr.

5.方法区
方法区与java堆一样都是线程共享的区域。它用于存储被虚拟机加载的类信息,常量,静态变量,被及时编译器编译的代码等。虚拟机规范里把它作为堆的一个逻辑部分。但它有一个名字非堆,为了与堆区分开。
习惯上把方法区称为永久带。本质上两者不同。因为hotspot虚拟机把方法区设计到老年带上,这样hotspot垃圾回收器就会像管理java堆一样管理方法区,但这会带来内存泄漏的问题。
java虚拟机规范对方法区的要求很宽松,可以像java堆一样物理上内存不连续。还可以选择不进行垃圾回收。此区域上的垃圾回收目的是回收常量池和卸载类型。当该方法区无法满足内存分配时回出现outofmemoryerr异常。
5.1. 运行时常量池:
运行时常量池属于方法区的一部分。它用来存放编译器定义的字面量和符号引用。这部分内容在类加载后放入到常量池。
运行时常量池相对于class文件常量池的一个特征是动态性。并不是预置到class文件常量池的常量才会进入到运行时常量池,在运行时过程中产生的常量也会加入到运行时常量池中。
该内存会受到方法区大小的限制,当常量池无法申请到内存时会抛出outofmemoryerr.

6.直接内存
直接内存不是java虚拟机运行时内存,也不在虚拟机规范之内。但是也会频繁的被使用。
jdk1.4中引入了nio,引入了通过channel和buffer的io方式。它可以通过native函数直接分配堆外内存。然后通过java堆上的一个引用指向该内存。这样避免了java堆和native堆之间来回复制数据。直接内存大小不会受到java虚拟机内存的大小限制,但是会受到总体内存大小的限制。当分配内存不能满足时,会抛出outofmemoryerr异常。

二,hotspot虚拟机上java堆的对象分配,布局和访问。
1.对象的创建:
当虚拟机遇到一条new指令后,首先会检查指令的参数,看该对象在常量池中是否已经有了一个符号引用。并且检查该引用代表的类是否已经加载,解析,初始化。如果没有则进行加载。
类加载后,接下来给该对象分配内存。类加载完成后为对象分配的内存大小就确定了。1.如果内存是规整的,已使用的内存在一边,未使用的在另一边,中间是分界指针。为对象分配内存就是将指针想空闲区域移分配空间大小的位置。该情况称为指针碰撞。2,如果内存空间不规则,空闲和使用空间交错。那么需要一个表进行记录空间使用情况。分配空间时,在表中取一块足够大的内存进行分配,然后更新该表。这种情况称为空闲列表。内存是否规整取决于垃圾回收器是否带压缩(compact)功能。如果带压缩功能则使用指针碰撞算法。否则使用空闲列表算法。
由于对象在堆上的创建非常频繁,所以分配内存时指针的修改也不是线程安全的。例如A对象使用指针分配内存后还没来的及修改指针,B对象就使用原来的指针分配对象造成冲突。解决改问题有两种方法:1.使分配内存的动作进行同步处理。2.给每个线程预分配一块内存,称为本地线程分配缓冲(TLAB thread local allocaion buffer),每个线程分配内存时首先在TLAB上进行分配。只有当TLAB使用完后分配新的TLAB时,才需要同步锁定。虚拟机是否使用同步锁定,可以通过-XX:+/-UseTLAB.
内存分配完后,虚拟机需要把内存的初始值都置为0,使用TLAB时,该操作可以在分配TLAB时进行。这样保证在程序执行时不给变量赋初值可以直接使用数据类型对应的0值。
接下来虚拟机会对对象进行一些设置。如该对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象GC时处于的年龄带等,都会记录在对象头中。
以上的步骤都完成后,虚拟机层面上对象已经完成,但是程序层面上创建对象才刚刚开始。init方法还没有执行。所有初始值还都是0,需要按程序员的意愿全部初始化后,一个对象才算真正完成。

2.对象的内存布局:
对象在内存中的存储分为3块:对象头,实例数据, 对齐填充。
对象头分为两部分:一部分:存储对象运行时信息。如哈希码,GC分带年龄,线程持有的锁,锁状态,偏向线程ID等。这部分空间在32位和64位虚拟机上分别对应32bit和64bit。官方称为“mark  word”,它被设计为非固定的数据结构,在不同的状态下会复用自己的空间。例如32位的虚拟机里,25bit存储哈希码,4位存储对象分带年龄,2位存储锁状态,1位固定为0. 
二部分:存储对象的类型指针,及指向类元数据的指针。用于确定该对象是哪个类的实例。但是并不是所有对象都需要存储类型指针,换言之并不需要通过对象本身查找它的元数据。另外如果对象是一个java数组,还对象头还需要存储数组的大小,因为普通对象虚拟机可以通过元数据确定对象的大小,但是数组不行。
接下来存储的是对象的实例数据,继承下来的父类的字段和子类定义的字段都会被记录下来。字段存储的顺序取决于(FieldAllocationStyle)参数和程序中定义的顺序。虚拟机默认的顺序是把宽带相同的变量放在一起。然后父类放在子类之前。如果FieldCompact参数为true,那么子类中较窄的
对齐填充不是必须的,只是起一个占位符的作用。由于虚拟机规定内存存储是8字节的整数倍,所以空间有空闲的用填充补齐。

3.对象的访问定位
java对象访问,是使用栈上的reference指向堆上的对象进行访问的。进行指向的方式一般有两种:1.句柄访问。2直接指针
句柄访问:java堆上会分配一个句柄池,reference里存储的句柄池的地址。句柄池里存放着指向实例数据的指针和方法区上数据类型数据的指针。该方法的好处是reference的值稳定,当对象移动时(垃圾回收器工作时经常使对象移动),只改变实例对象的指针,reference的值不用变。
直接指针访问:reference中存储的是实例的地址。而实例中有指向方法区上对象类型数据的指针。该方法的好处是,节省了一次指针定位消耗的时间。由于java对象的访问很频繁,积累下来的时间也是很大的成本。hotspot 虚拟机使用的改种方式。

三,实战:outofmemoryerror异常。
1.工具的安装和使用:
a> 分析内存溢出可以使用eclipse memory analyzer。该插件的安装步骤如下:
help-》install new software -》work with :  这里填写eclipse版本:  Kepler - http://download.eclipse.org/releases/kepler(注意这里不同版本可能地址不同!!!)--》General Purpose Tools--》找到”Memory Analyzer“和”Memory Analyzer(Charts)“,并选取。安装完后重启eclipse.
b>设置虚拟机参数:设置为20m为方便溢出测试。
Run as-->Run Configurations-->Arguments-->VM arguments:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError。
-XX:+HeapDumpOnOutOfMemoryErro可以生成堆转储快照,使用MA进行内存溢出分析
c>以java堆溢出为例,执行内存溢出程序,例如:

public class HeapOOM {


static class OOMObject {}

public static void main(String[] args) {

List<OOMObject> list = new ArrayList<OOMObject>();

while(true){

list.add(new OOMObject());

}

}

}

会出现如下报错:

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid50620.hprof ...

Heap dump file created [27626604 bytes in 0.178 secs]

d>刷新项目后会生成java_pid50620.hprof文件,双击打开会有内存溢出分析。


2.虚拟机栈和本地方法栈溢出:

在hotspot虚拟机上,虚拟机栈和本地方法栈是不区分的。所以使用-Xoss(设置本地方法栈)参数无效,栈容量只有 -Xss设置。

栈上存在两种异常:1,线程申请的栈的深度大于虚拟机允许的大小,导致stackoverflow异常。2,虚拟机扩展栈时申请不到足够的内存,导致的outofmemory异常。

a>设置栈的最大大小:-Xss 128k,用于减小栈最大容量

b>产生大量的本地变量,是栈贞中本地变量表不断增大。

public class JavaVMStackOverFlow {


private int stackLength = 1;

private void stackleak() {

stackLength++;

stackleak();

}

public static void main(String[] args) {

JavaVMStackOverFlow sof = new JavaVMStackOverFlow();

sof.stackleak();

}

}

结果:Exception in thread "main" java.lang.StackOverflowError

at com.pubukeji.boss.test.JavaVMStackOverFlow.stackleak(JavaVMStackOverFlow.java:8)

at com.pubukeji.boss.test.JavaVMStackOverFlow.stackleak(JavaVMStackOverFlow.java:9)


说明:单线程下无论是由于栈容量太小还是栈帧太大,都会产生stackoverflow的异常。
c>在多线程情况下,容易产生outofmemory异常。
原因:操作系统赋给进程的内存是有限的,32位的windows限制是2G,在此基础上除去堆,程序计数器,方法区和虚拟机进程本身消耗的内存外,剩下的内存由本地方法栈和虚拟机栈共享。当每个进程所占的栈内存越大,所能容纳的进程数就越少。此情况下容易出现outofmemoryerr异常。在不能减少线程数和更换64位虚拟机的情况下,可以通过减少堆内存和减少栈容量来进行缓解。
程序例子:
//vm args: -Xss2M    设置栈容量,为测试将栈容量调大。

public class JavaVMStackOverFlow {

private void dontStop() {

while(true) {

}

}

public void stackLeakByThread() {

while(true) {

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

dontStop();

}

});

thread.run();

}

}

public static void main(String[] args) {

JavaVMStackOverFlow sof = new JavaVMStackOverFlow();

sof.stackLeakByThread();

}

}

结果:会出现outofmemoryerror异常。

3.方法区和运行时常量池溢出:
jdk1.6之前intern()方法的作用:如果字符串常量池中已经存在了需要的string,则把此string的引用返回给调用者,否则将String复制一份添加到常量池中,将新引用交给调用者。
jdk1.7之后intern()与jdk1.6区别:如果常量池中没有该字符串,那么虚拟机只会把字符串的引用放到常量池中。
在jdk1.7上以下例子运行以下例子:

public class RuntimeConstantPoolOOM {


public static void main(String[] args) {

String str1 = new StringBuilder("计算机").append("软件").toString();

System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();

System.out.println(str2.intern() == str2);

}

}

结果:true, false。由于str1开始在常量池中不存在,所以调用intern()后会将该字符串的引用放入常量池中,所以第一个结果是true。由于java在常量池中已经存在,所以常量池中的’java’字符串与堆上的java 字符串不相同。结果为false.

方法区上存放着class的相关信息,如类名,访问修饰符,字段描述,常量池,方法描述等。在一些框架中如Spring会使用cglib的字节码技术动态生产很多代理类。这些代理类需要足够的方法区去装载,否则容易出现outofmemoryerr异常。

程序示例:

//vm args:-XX:PermSize=10m -XX:MaxPermSize=10m 设置方法区大小用于测试

public class JavaMethodAreaOOM {


public static void main(String[] args) {

while(true) {

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(OOMObject.class);

enhancer.setUseCache(false);

enhancer.setCallback(new MethodInterceptor() {

@Override

public Object intercept(Object object, Method method, Object[] args,

MethodProxy proxythrows Throwable {

return proxy.invokeSuper(objectargs);

}

});

enhancer.create();

}

}

static class OOMObject{}

}

结果:PermGen space OutOfMemoryError。容易出现方法区溢出的情况有:经常大量产生动态Class的应用,动态语言,已经有大量jsp文件(因为jsp文件也需要编译成java类)的应用。


4.本机直接内存:参数设置:-XX: MaxDirectMemorySize。如果不指定则与java堆的大小相同。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值