您的位置:首页 > 编程语言 > Java开发

java 虚拟机学习笔记整理001--运行时的数据区域+垃圾收集算法

2017-03-10 17:40 633 查看

1.java的内存区域与内存溢出异常

      内存动态分配和垃圾收集技术是java和c++之间的高墙。

1.1 运行时的数据区域
     程序计数器:线程私有,字节码的行号指令器,分支,循环,跳转,异常处理,线程恢复。线程私有(多线程轮流切换后恢复正确位置);如果在执行一个java方法,记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器值为空(undefined),唯一一个在java虚拟机规范中没有定义任何outofmemoryError的区域;
    java虚拟机栈: 线程私有,与线程相同的生命周期;java方法执行的内存模型;栈贞:局部变量表,操作数栈,动态链接,方法出口等;【通常说的堆栈中栈是指与对象内存密切的:局部变量表】,局部变量表存放:编译器可知的基础数据类型,对象引用,returnAddress类型。64位的long和double占用2个局部变量表空间。局部变量表空间在编译器进行分配。
      线程请求栈深度大于虚拟机锁允许的深度,StackOverflowError;扩展时无法申请到足够的内存,OutMemoryError.
             本地方法栈:线程私有,与虚拟机栈作用类似,区别:虚拟机栈为虚拟机执行java方法(字节码)服务;本地方法栈则为虚拟机用到的native方法服务。
                           
上述三个随线程生而生,随线程灭而灭。
    java堆:线程共享,java虚拟机管理内存中最大的一块,唯一目的:存放对象实例,(所有的对象实例和数据都要在堆上分配【JIT编译器技术发展,逃逸分析技术,栈上分配,标量替换优化技术将会导致不一定对象都在堆上分配】)。GC堆。
java堆:新生代和老年代。无论如何划分,存放的都是对象实例,只是为了更好的回收或者更快的分配内存。能处理物理不连续的内存。
方法区:线程共享,存储加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;堆的一个逻辑部分,也叫非堆(Non-Heap)。永久代。目标:对常量池的回收和对类型的卸载。
--------------------------------------------------------------------------------------------
运行时常量池:方法区的一部分,Class文件:类的版本,字段,方法,接口,常量池等
                       常量池用于存放编译期生成的各种字面量和符号引用。
java虚拟机对Class文件每一部分的格式严格规定,每一个字节用于存储哪种数据都必须符合规范上要求。
但对运行时常量池没有细节规定。运行时常量池保存:class文件描述的符号引用,还会存储翻译出来的直接引用。
运行时常量池与Class文件常量池区别:具有动态性,并非预置入Class文件中常量池的内容才能进入方法区运行,运行期也可能将新的常量放入池中。String类的intern()方法【将字符串对象放入常量池中】
---------------------------------------------------------------------------------------------------
直接内存:不是虚拟机运行时数据区的一部分。不是java虚拟机规范中定义的内存区域。但频繁使用,也可能会OutMemoryError。jdk1.4中加入了NIO(new
input/output)类,引入一种基于通道与缓冲区的I/O方式。使用Native函数库直接分配堆外内存。通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免java堆和Native堆来回复制数据。
-------------------------------------------------------------------------------------------------
2.垃圾收集器与内存分配策略
    程序计数器,虚拟机栈,本地方法栈三个区域随着线程的生灭而生灭。不需要关注内存的回收。
   java堆和方法区则不同,一个接口的不同实现类需要的内存不同,一个方法的多个分支需要的内存也不同。运行时才知道。

判断对象是否已死:
    1. 引用计数算法:给对象添加一个引用计数器,有一个地方引用,加1,引用失效就减1.引用对象为0的则为不会再被引用的对象。问题:很难解决对象之间相互循环引用的问题。现在虚拟机一般不用。
    2.可达性分析算法:从一系列GC
Roots的对象作为起始点,从这些节点开始往下搜索。当一个对象到GCRoot没有任何引用链相连接。

四种引用:
   
强引用:只要强引用还存在就不可能被回收;Object 0=new Object( );
    软引用:有用但是并非必须的对象,内存将要溢出之前回收。
    弱引用:只能生存到下一次垃圾收集发生之前。
    虚引用:幽灵引用或幻影引用。目的:能在这个对象被收集的时收到一个系统通知。无法通过虚引用取得一个对象实例;

任何一个对象的finalize()方法只会被系统调用执行一次。不可达的对象被判死缓,需要经历两次标记。
不可达被标记一次,并筛选是否需要执行finalize()(已经被调用一次或者没有被重写则不需要执行),需要执行时可能发生自救。将自己移除“即将回收”集合。

public class FinalizeEscapeGC{
    public
static  FinalizeEscapeGC SAVE_Hook=null;

    public void isAlive(){
            System.out.println(" yes,I am alive ");
        }

    @override
protected void finalize() throws
Throws Throwable{
        super.finalize();
        System.out.println("
finalize method executed ! ");
        FinalizeEscapeGC.SAVE_Hook=this;
    }
    
    public static void main(String[] args)throws Throwable{
        SAVE_Hook=new FinalizeEscapGC();
        //对象第一次成功拯救自己
        SAVE_Hook=null;
        System.gc();

        Thread.sleep(500);//finalize()优先级低,所以等等0.5秒;
        if(SAVE_Hook!=null){
            SAVE_Hook.isAlive();
            }else{
             System.out.println("
no,i am dead! ");
            }
        //对象自己拯救失败。

       SAVE_Hook=null;
        System.gc();

        Thread.sleep(500);
        if(SAVE_Hook!=null){
            SAVE_Hook.isAlive();
            }else{
             System.out.println("
no,i am dead! ");
            }

    }
}

回收方法区:
在堆中,尤其是新生代中回收率很高;
在永久代中,回收废弃常量,无用的类回收条件苛刻。(该类实例被回收,类加载器被回收,class对象没有被引用,无法再任何地方通过反射访问该类)
类卸载功能保证永久代不会溢出;

垃圾收集算法:
1.标记--清除算法:
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
问题:1.效率问题:标记和清除两个过程的效率都不高;
          2.空间问题:标记清除过后会产生大量不连续的内存碎片;

2.复制算法:

1:1将内存分为两块,每次使用其中的一块。当一块内存用完了,就将还存活的对象复制到另外一块上面。然后把使用过的空间清理掉。分配内存时候只需要移动堆顶指针,按照顺序分配即可。实现简单运行高效。

商业虚拟机都用这种方法收集新生代,Java 堆内存分为新生代和老年代,新生代中又分为1个Eden区域
和两个 Survivor区域。
8:1分配Eden,Survivor。每次使用一个Eden,一个Survivor。当回收时,将还存活对象一次性复制到没用的一个Survivor上。新生代可用内存空间为新生代总容量的90%。需要依赖老年代内存空间分配的担保。

3.标记--整理算法:
对象存活率较高,较多复制,不想浪费50%空间,复制算法就需要额外空间担保。老年代不适用复制算法。
标记整理算法,让所有存活的对象移向一端。清理端边界以外的内存。

4.分代收集算法:

现代商业虚拟机都采用分代收集(Generational
Collection),Java 堆内存分为新生代和老年代,新生代中又分为1个Eden区域 和两个 Survivor区域。新生代使用复制算法,老年代使用标记清理或者标记整理算法。

3.类文件结构

1. 类文件结构

无关性的基石:字节码存储格式+虚拟机。

平台无关性和语言无关性。

一个Class文件都对应着唯一的一个类或者接口;但是类或接口并不一定都得定义在文件里。(类或者接口也可以通过类加载器直接生成)

Class文件:一组以8位字节为基础单位的二进制流,各数据项紧凑排列,无分隔符,超过8字节的高位在前Big-Endian。
类似于C语言结构体的伪结构;
两种数据类型:1.无符号数:基本的数据类型(u1,u2,u4,u8)(对应1,2,4,8个字节),

2.表:复合数据类型;以 _info 结尾;【无论是顺序,数量,还是字节序都限定死。】

--------------------------------------------------------------------------------------------------------------------------------------------

Class文件:最开始的四个字节:魔数(magic number)确定这个文件是否为一个虚拟机接受的class文件;

第5.6字节:次版本号;第7,8字节:主版本号;版本号从45开始。

接着是:常量池入口,资源仓库,与其他项目关联最多的数据类型,占用空间最大之一,第一个出现表类型数据项目;由于常量的数量不确定,入口放一个u2类型的表示常量数量的数据。计数特殊从1开始,ox0016,为十进制22,表示有21个常量(1----21)。【从1计数原因:为满足后面某些指向常量池的索引值的数据在特定情况下需要表示“不引用任何一个常量池项目”】两大类常量:字面量和符号引用。

字面量:文本字符串,final型的常量值等;

符号引用:类名和接口全限定名;字段的名称和描述符;方法的名称和描述符;

4.类的加载机制
5.字节码执行引擎
6.java的内存模型

    

-------------------------------------------------

以下是他人总结,大意差不多。

----------------------------------------------------------------------------------------------------------------------------------------------------

1. Java JVM:垃圾回收(GC 在什么时候,对什么东西,做了什么事情)?

在什么时候

GC又分为minor GC 和 Full Gc(也称为Major GC)。
Java 堆内存分为新生代和老年代,新生代中又分为1个Eden区域
和两个 Survivor区域。

1. 对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域(新生代)没有足够的空间,
那么就会发起一次 Minor GC;

2. 对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的

话,那么就会进行一次 Full GC。

Ps:上面所说的只是一般情况下,实际上,需要考虑一个空间分配担保的问题:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor
GC,

如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor
GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。

但是,具体到什么时刻执行,这个是由系统来进行决定,是无法预测的。

对什么东西

主要根据可达性分析算法,如果一个对象不可达,那么就是可以回收的;如果一个对象可达,那么这个对象就不可以回收。对于可达性分析算法,它是通过一系列称为“GC
Roots” 的对象作为起始点,当一个对象到 GC Roots 没有任何引用链相接的时候,那么这个对象就是不可达,就可以被回收。如下图:

这个GC Root 对象可以是一些静态的对象,Java方法的local变量或参数, native 方法引用的对象,活着的线程。

做了什么事情

主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。

例如新生代采用了复制算法,老年代采用了标记整理法。
在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。
那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。

JDK7 整体内存结构

摘录自《深入理解Java虚拟机》,针对 JDK7 的整体结构图如下:



如图,JVM内存区域分为 PC寄存器,JVM方法栈,本地方法栈,JVM方法区,JVM堆。
PC寄存器:存放下一条指令在方法中的偏移量。也可以看做是线程所执行的字节码的行号指示器,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的指令。
JVM虚拟机栈:PC寄存器,JVM的虚拟机栈,和本地方法栈都是线程私有。JVM虚拟机栈主要由栈帧来组成,每一个方法的调用就相当于一个栈帧,所以说,当出现无限递归这种情况的时候,栈帧就可能会过多的创建,从而导致栈内存溢出。栈帧中又包含了局部变量区,操作数区等,用于具体对数据进行操作。
本地方法栈:主要用来支持native方法,记录native方法调用的状态。可以把native 方法看成是 java 调用 非 java 代码的一个接口。主要用于允许Java
和其他语言,比如 C 语言进行交互。
JVM方法区:主要存储已经加载的类的信息,比如构造函数的信息,方法的信息,常量的信息。Class对象提供的getXXX()方法取得的类的信息就是从JVM方法区中得到。Ps:JVM方法区是永久代的一个子集,常量池也是放在JVM方法区中。
JVM堆:主要目的是用来存放数组和对象。同时,JVM
堆也是 内存溢出和垃圾回收的主要区域。

JDK7 堆内存结构

堆内存结构图如下:



如图,在JVM堆中,又分为新生代,老生代。
可以看到,新生代中,又分为eden区域和两个Survivor区域。默认比例为8:1:1,也就是说,可以用的内存为90%。
当然,可以用-XX:SurvivorRatio设置eden和Survivor的比值,默认为8:1。

Ps:这样的一个分类也有利于垃圾收集的复制算法。

JDK8 内存结构变动

在JDK8中,最主要的就是元空间代替了永久代(PermGen Space),由于
上面结构图中的 JVM方法区是永久代的子集,那么就是说这部分会没有了,取而代之的是元空间(Metaspace)。这里有一篇比较好的文章介绍了元空间:点这里
主要的意义在于:Metaspace 的内存大小可以动态增长,仅受限于本地内存大小。 当然,
-XX:MetaspaceSize
 和 
-XX:MaxMetaspaceSize
 可以设定大小。

JVM的栈与堆:

Java中的堆栈: https://my.oschina.net/u/1464779/blog/225590

栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,因此,用堆保存数据时会得到更大的灵活性。变量所需的存储空间只有在运行时创建了对象之后才能确定。Java的垃圾收集器会自动收走这些不再使用的数
据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和引用对象,栈里存的却是堆的首地址名;就像引用变量。 

Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在栈中分配的内存只是一个指向这个堆对象的指针(引用变量)而已。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。 

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。

引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)

1.在Java语法层面上创建一个对象,使用一个简单的new关键字即可,但是在JVM中细节的实现相当复杂,而且过程繁多。

2.当Java语法层面使用new关键字创建一个Java对象时,JVM首先会检查相对应的类是否已经成功经历加载、解析和初始化等步骤;当类完成装载步骤之后,就已经完全确定出创建对象实例时所需的内存空间大小,才能对其进行内存分配,以存储所生成的对象实例。

3.实例化之后,进行初始化(初始化对象头和实例数据)。

4.内存分配方式有:指针碰撞(Bump
the Pointer)、快速分配策略、空闲列表(Free
List)。

5.在并发环境下从堆中划分内存空间是非线程安全的,new运算符具有-------数据操作的原子性;也就是说创建一个Java对象分配内存,要么所有步骤都成功,返回对象的引用,要么回归到创建之前的内存状态,返回为NULL。

6.通过new创建一个Java对象,如果成功则返回这个对象的引用,开发者不可直接操作对象实例,需要通过这个引用“牵引”。

String是一个特殊的包装类数据。可以用: 
String str = new String("abc"); 
String str = "abc"; 
两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。 
而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

String str=null
      表示str还没有被“new”,就是对象的引用还没有创建,也就是还没有分配内存给他。此时不能用比较的函数进行操作,如if(str.equals("")),会抛出空指针异常。if("".equals(str))不会抛出异常,返回false。str!=""返回true,str ==null返回true。
       System.out.println(str);输出结果为:null。因为null值可以强制转换为任何java类类型,输出时null值被转换成String类型。
String str ;     String str =new String()。
       表示str已经被“new”了,只是内容为空。str不是一个空引用,要分配内存空间。
第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。 
而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: