Java 虚拟机 JVM

解析JAVA虚拟机开发

                                                                                                                    --《深入理解Java虚拟机-第二版》

第二章:Java内存区域与内存溢出异常

一、java的内存分配管理

1java内存分配的几个区域

a.寄存器:在程序中无法控制

b.栈:存放基本类型数据( int , short ,long ,byte, float ,double , boolean ,char)和对象的引用,但是对象的本身不存放在栈中,而存放在堆中。

             在函数中定义的一些基本类型的变量数据,还有对象的引用变量都在函数的栈内存中分配。当在一段代码中定义一个变量时,java就在栈中为这个引用变量分配空间,当该变量退出该作用域后,java会自动释放掉为该变量分配的空间。

           栈也叫栈内存,是Java的运行区,是在线程创建的时候创建,线程结束栈内存也就释放。栈中的数据都是以栈帧(Stack Frame)的格式存在,主要保存本地变量(包括输入参数和输出参数以及方法内的变量)、栈操作(记录出栈、入栈的操作)、栈帧数据(包括类文件、方法等).

         当一个方法A被调用时就产生了一个栈帧F1,被压入到栈中,在A方法中又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,F2先被弹出,遵循“先进后出”的原则。

                        

 

c.堆:用来存放由关键字new创建的对象和数组。在堆内存中分配,由java虚拟机的自动垃圾回收器来管理。堆内存分为永久存储区(用于存放jdk自身携带的Class   Interface的元数据)、新生区(是类的产生、成长、消亡的区域)、养老区(保存新生区筛选出来的java对象)

d.静态域:存放在对象中用static定义的静态成员。

e.常量池:存放常量,在编译期被确定,在内存中以表的形式存在,并被保存在已编译的.class文件中的一些数据。对于String, integer, floating, point 的值放在常量池中。在程序执行时常量池会存储在方法区域中,而不是堆中。

f.RAM存储:硬盘等永久存储空间

2JVM堆和JVM栈区分的优点:aJVM栈代表了处理逻辑,而JVM堆代表了数据。b、可以使得JVM堆中的数据被多个JVM栈共享(可以理解为多线程访问同一个对象)c、面向对象就是JVM堆和JVM栈的完美结合。

3int a =3;

      int   b=3;

编译器会先处理int  a=3;它会首先在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没有找到,就将3存放进来,然后将a指向3。接着处理b=3,因为栈中有3这个值,便将b直接指向3。如果这是我们将a=4,那么编译器会为a建立一个指向4的引用。所以改变a不会影响到b

4String  str0=new String(“abc”); String str1=”abc”;       str0==str1  false

第一种是用new()来新建对象的,它会存放在堆中。每调用一次就会创建一个新的对象。而第二种是先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池里面找有没有”abc”,如果没有,则将"abc"存放进字符串常量池,并令str指向"abc",如果已经有”abc”则直接令str指向”abc”。

5String  str0=”vkill”; String str1=”vkill”;String str3=”vk”+”ill”     str0==str1==str3   true

String str0=new String(“abc”);  String str1=new String(“abc”)    str1==str2  false     

字符串的intern()方法:

String str0="kvill";

String s2=new String("kvill");

s2=s2.intern();

去寻找常量池中有没有"kvill"这个字符串,如果有,就把s2的引用指向常量池的位置。


二、运行时的数据区域    

                                           

                                

1、程序计数器:是一块较小的内存空间,改变这个计数器的值来选取下一条需要执行的字节码指定、分支、循环、异常处理、线程恢复等基础功能。每条线程都有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,这类内存区域被称作”线程私有”  。

2JAVA的虚拟机栈VM Stack :Java虚拟机栈为线程所私有,它的生命周期与线程相同。每个方法被执行时都会产生一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法在调用的过程,就对应着一个栈帧在虚拟机里的入栈出栈的过程。java虚拟机规范规定虚拟机栈的大小可以是固定的或者是动态分配大小。java虚拟机可以实现向程序员提供对java栈的初始大小的控制,以及在动态扩展或者收缩java栈的情况下,控制java栈的最大值和最小值。

      在这里经常会出现:Stack Over flow Error (线程请求的栈深度大于虚拟机所允许的深度),Out  of Memory Error (不能得到足够的内存为一个新线程创建初始Java)

3、本地方法栈(Native Method Stack):在本地方法栈中执行的是非Java语言编写的代码。而虚拟机栈执行的是java方法字节码服务。

4Java堆  Java Heapjava堆是类实例和数组的分配空间,是一块所有线程共享的内存区域。内存泄漏溢出问题大多都出现在堆区域。java堆是垃圾收集器管理的主要区域,被称作GC(Collection  Heap)堆。同栈一样java虚拟机可以实现向程序员提供对堆的初始大小的控制,以及在动态扩展或者收缩堆的情况下,控制堆的最大值和最小值。java堆中可以分为新生代(其中新生代又可分为Eden空间和Survivor空间,一般对象都是直接分配在这个空间)和老年代两个部分。

5、方法区:用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也就是说方法区类似传统语言的编译后代码的存储区。是各个线程的共享的内存区域。方法区的大小也是可以控制的,所以也会抛出Out Of Memory Error异常,运行时方法区也可以称作永久区(Permanent Generation),同样也可以称作非堆(Non-Heap),常量池也是位于这个区。

5.1、运行时常量池:是方法区的一部分用于存放编译期生成的字面量和符号的引用,这部分内容将在类加载后存放到方法区的运行时常量池中。在常量区中有个很有用的特性 在String类中的intern()方法,查找常量池中有没有某个字符串常量。

 

 

 

三、对象的访问

 


 

1、对象的访问一般都要涉及:java 栈、java堆、方法区这三个最重要内存之间的关联关系。例如:

Object  obj = new  Object();假设这段代码出现在方法中,” Object  obj”  这部分语义将会被反应到java栈的本地变量表中,作为一个”reference”类型数据出现。而” new  Object()”这部分语法将会梵反映到java堆中,形成了一块存储了Object类型所有实例数据值(Instance data,对象中各个实例字段的数据)的结构化内存,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据存储在方法区中。

2JAVA虚拟机中提供了使用句柄和直接指针两种方式来访问对象。(1)句柄访问方式:java堆中会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。(2)直接指针访问方式:reference 中直接存储的数据对象地址。

3java堆溢出java.lang.Out OfMemoryError:java heap space,首先我们要弄清楚是内存泄漏还是内存溢出,如果是内存泄漏,可以通过工具查看泄露对象到GC  Roots的引用链。这样就可以找到泄露对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收。如果是内存溢出就是说明堆内存不够;

  4虚拟机栈和本地方法栈溢出:在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。在进行多线程开发时候,调用每个方法都会产生一个栈帧压入栈,这是栈溢出的话,可以通过减少栈帧的长度来减少内存;

 5、 运行时常量池溢出(java.lang.OutOfMemoryError:PermGen space)向常量池中加内容最简单的方法是使用String.intern()这个Native方法,该方法的作用是,如果池中已经包含一个等于此String 对象的字符串,则返回代表池中这个字符串的String对象;否则,将此对象包含的字符串添加到常量池中,并且返回此对象的引用。

        方法区溢出(java.lang.OutOfMemoryError:PermGen space)方法区用于存放Class的相关信息,产生的原因是,一个类如果要被垃圾收集器回收掉,判断条件就非常苛刻的。所以在经常动态生成大量class的应用中,需要特别注意类的回收情况。

四、内存泄露

1、内存泄露的定义:一般是指堆内存泄露。堆内存是指程序从堆中分配的,使用完后必须显示释放的内存。应用程序一般使用malloc , realoc , new 等函数从堆中分配到一块内存,使用完后,程序必须负责调用相应的free或者delete释放该块内存,否则这块内存就不能被再次使用,那么这就是内存泄露了。(MS C-Runtime Library,Purify,BoundsChecker三种工具检测内存泄露)

a.  Serial  Collector   任何时候都有一个线程在进行垃圾收集,这种类型的收集器更适合单CPU.

b. Parallel   Collector 

c. Concurrent   Collector  通过并行来进行垃圾收集。

 

第三章   垃圾收集器与内存分配策略

 

一、判断对象已死的方法

1、引用计数法:给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就不可能再被使用的。目前Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

2、可达性分析算法:这个算法的基本思路是通过一系列的称为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC  Roots没有任何引用链相连,则证明此对象是不可用的。

                               

3、再谈引用:如果Reference类型的数据中存储的数字代表的是另外一块内存的起始地址就称这块内存代表着一个引用。

A、强引用,类似”Object    obj=new  Object()”这类的引用,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。

B、软引用,用来描述一些还有用但是非必需的对象。

C、弱引用,也是用来描述非必需对象的,但是它的强度比软引用更加弱一些,被弱引用关联的对象只能生存到下一次垃圾收集器发生之前。

D、虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。

 

二、垃圾收集算法

1、标记-清除算法:算法分为”标记”,”清除”两个阶段:首先标记出所有需要回收的对象,在标记完后统一回收被标记的对象。它的不足主要有两个,标记和清除两个过程的效率并不高;标记清除后会产生大量不连续的内存碎片。

2、复制算法:这种算法的代价是将内存缩小到原来的一半。老年代一般不能直接选用这种方法。

3、标记-整理算法:让存活的对象都向一段移动,然后再清理掉边界以外的内存。

4、分代收集算法:Java堆分为新生代和老年代,这样就可以根据各个年代的特点来采用最适当的收集算法。

 

三、垃圾收集器

1、概念:如果说垃圾收集算法是内存回收的方法,那么垃圾收集器就是内存回收的具体实现。

            

    

1Serial收集器:Serial收集器是最基本,发展历史最悠久的收集器。这个收集器是一个单线程的收集器,单线程的意义不仅仅在于它只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它结束。它的优点在于:简单而且高效。

2ParNew收集器:其实就是Serial收集器的多线程版本。

3Parallel Scavenge收集器:是一个新生代的收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,它与其它收集器不同的地方在于可以控制吞吐量(Throughput)。所以Parallel Scavenge也可以成为吞吐量优先收集器。

4Serial Old收集器:使用”标记-整理”算法。

5Parallel Old 收集器:使用多线程和”标记-整理”算法。

6CMS收集器:是一种获取最短回收停顿时间为目标的收集器。整个过程分为四个步骤:

a、初始标记

b、并发标记

c、重新标记

d、并发清除

7G1收集器:与其他的收集器相比,它具有如下优点:并发与并行,能充分利用多CPU、多核环境下的硬件优势;分代收集;空间整合,从整体来看基于标记-整理算法,从局部来看基于复制算法;可预测停顿。收集的整个过程分为四个步骤:

a、初始标记

b、并发标记

c、最终标记

d、筛选回收

四、内存分配和回收策略

1、对象优先在Eden分配:

2、大对象直接进入老年代

3、长期存活对象将进入老年代

4、动态对象年龄判断

5、空间分配担保

五、并发与并行

1、并发:指用户线程与垃圾回收线程同时执行。

2、并行:指多条垃圾收集器并行工作,但此时用户线程仍然处于等待状态。

 

第七章   虚拟机类加载机制

一、虚拟机加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。

二、类的加载时机:类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析三个部分统称为连接(Linking)

 

1、类进行初始化的5种情况:a、遇到new gestaticputstaticinvokestatic4条字节码指令时,如果类还没有进行初始化,则需要先触发其初始化;b、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行初始化,则需要先触发其初始化;c、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;d、当虚拟机启动时,用户需要指定一个执行的主类(包含main方法的类),需要先初始化这个主类;e、当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStaticREF_putStatic 、 REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。

三、类的加载过程

1、加载:(1)通过一个类的全限定名来获取定义此类的二进制流;(2)将这个字节流所代表的静态存储结构转化为为方法区的运行时数据结构;(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

 2、验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。从整体上看,验证阶段大致会完成文件格式验证、元数据验证、字节码验证、符号引用验证4个检验动作。
3、准备:准备阶段正式为类变量分配内存并设置类变量的初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

4、解析:这个阶段虚拟机将常量池内的符号引用替换为直接引用。

5、初始化:初始化阶段是类加载的最后一步,这一步才真正开始执行类中定义的Java程序代码。 

四、类加载器
1、概念:类加在过程中的”通过一个类的全限定名来获取描述此类的二进制字节流”,这个动作放到Java虚拟机外部来实现,以便让应用程序自己来决定如何去获取所需要的类。对于任何一个雷,都需要由加载它的类加载器和这个类本省一同确立其在Java虚拟机中的惟一性,每一个类加载器,都拥有一个独立的类名称空间。
2、双亲委派模型:从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器由C++语言来实现,是虚拟机本身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承于抽象类java.lang.ClassLoader
                        

3、破坏双亲委派模型:           

第八章 虚拟机字节码执行引擎

一、运行时栈帧结构:栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual    Machine   Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是最有效的,称为当前栈帧(Current  Stack  Frame),与这个栈帧相关联的方法叫当前方法(Current  Method)。执行引擎运行的所有字节指令都只针对当前栈进行操作。

               

二、栈帧各个部分的作用和数据结构

1、局部变量表:是一组变量值得存储空间,用于存放方法参数和方法内部定义的局部变量。在Java编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

     局部变量表的容量以槽(Variable Slot)为最小单位,每个Slot只能存放一个booleanbytecharshortint、 floatreference(表示对一个对象实例的引用)returnAddress类型的数据。reference表示一个引用,可以通过这个引用直接或间接地查找到对象在Java堆中的数据存放的起始地址的索引;或者是通过此索引直接或间接地查找到对象所属数据类型在方法区的存储类型信息。

2、操作数栈:操作数栈(Operand Stack)也常称为操作栈,它也是一个先进后出的栈。操作数栈中的每一个元素可以是任意的Java数据类型,包括longdouble。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。在大多数虚拟机的实现里面两个栈帧出现一部分重叠,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时候,就可以共用一部份数据。

3、动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用目的就是支持方法调用过程的动态连接(Dynamic  Linking)

4、方法返回地址:当一个方法执行后,只有两种方式可以退出这个方法。一种是程序正常执行完退出、另一种是产生异常退出。当时无论采用何种方式退出,都需要返回到方法被调用的位置,程序才能继续执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值