jvm基础知识—垃圾回收机制

1、首先类的实例化、static、父类构造函数执行顺序

我们来看下面的程序代码:

public class A {

    int a1 = 8;
    {
        int a3 = 9;
        System.out.println("top of A() a1=" + a1 + " a2=" + "  a3=" + a3);
    }

    
    int a2 = getA2();

    public A() {
        this(66);
        System.out.print("A 构造函数
");
    }

    

    public A(int num) {
        System.out.print("A 带参数构造函数: " + num + "
");
    }

    static {
        System.out.println("I`m a static {} from class A..");
    }

    int getA2() {
        System.out.println("getA2..");
        return 7;
    }

    public void methodA() {
        System.out.println("methodA");
    }
    
    
    {
        System.out.println("below A()..has start");
    }
    
    int a3 = getA2();

}
public class B extends A {

    int b1 = 0;
    int b2 = getB2();
    {
        int b3 = 5;
        System.out.println("top of B() b1=" + b1 + " b2=" + b2 + " b3=" + b3);

    }

    public B() {
        this(33);
        // super(44);//添加super语句,会导致实例化时直接执行父类带参数的构造函数
        System.out.print("B 构造函数
");
    }

    public B(int num) {
        // 添加super语句,会导致实例化时直接执行父类带参数的构造函数
        // 前提是带参数的构造函数B会被运行(new实例化或this)
        // super(77);

        System.out.print("B 带参数构造函数:" + num + "
");
    }

    {
        System.out.println("below B()..has start");
    }
    static {
        System.out.println("I`m a static {} from class B..");
    }

    int getB2() {
        System.out.println("getB2..");
        return 33;

    }

    @Override
    public void methodA() {
        System.out.println("methoaA int class B");
        super.methodA();

    }

}
public class mymain {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        System.out.println("main app run..");
        B b = new B();
//        B b = new B(22);
        b.methodA();
    }

}

程序的运行结果是:

main app run..
I`m a static {} from class A..
I`m a static {} from class B..
top of A() a1=8 a2= a3=9
getA2..
below A()..has start
getA2..
A 带参数构造函数: 66
A 构造函数
getB2..
top of B() b1=0 b2=33 b3=5
below B()..has start
B 带参数构造函数:33
B 构造函数
methoaA int class B
methodA

总结:

1、先静态 先执行父类的静态代码块,然后执行子类的静态代码块
2、然后执行父类的普通成员变量的初始化,执行父类的普通代码块,执行按照顺序依次执行,先初始化成员变量a1,再按照顺序继续执行

{
int a3 = 9;
System.out.println("top of A() a1=" + a1 + " a2=" + " a3=" + a3);
}


3、然后执行父类的构造函数,把父类创建出来
4、然后执行子类的然后执行子类类的普通成员变量的初始化,执行子类的普通代码块,执行按照顺序依次执行
5、然后然后执行子类的构造函数,把子类创建出来
6、最后调用子类对应的函数

七.jvm内存结构
1.方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
2.Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
3.方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4.程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
5.JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
6.本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

九.GC算法
GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
1.标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
2.复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
3.标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
4.分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

 

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

1. 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

2.标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

3、.复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。比如内存块a和b,内存a中要进行垃圾回收

将内存a中不需要被回收的对象复制到内存b中,然后将内存a中的所有对象全部回收,就是这样的一个效果

java堆区域分为新生代带区域和老年区域,新生代区分又分为eden区域,From Survivor空间,To Survivor空间,其中from又叫s0区域,to区域又叫s1区域,s0区域和s1区域大小相等,所有新创建的对象都会存储在eden区域,对象如果经过垃圾

在JVM中共享数据空间划分如下图所示

上图中,刻画了Java程序运行时的堆空间,可以简述成如下2条

1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代

2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)

3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )

4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。

复制(Copying)算法

将内存平均分成A、B两块,算法过程:

1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。
2. 清理A块所有对象。
3. 新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。
4. 清理B块所有对象。
5. goto 1。

优点:简单高效。缺点:内存代价高,有效内存为占用内存的一半。

图解说明如下所示:(图中后观是一个循环过程)

对复制算法进一步优化:使用Eden/S0/S1三个分区

平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。

算法过程:

1. Eden+S0可分配新生对象;
2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
3. Eden+S1可分配新生对象;
4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
5. goto 1。

默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,这样做的原因大概有以下几种

1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中,设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放,你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系,是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了~~~

在复制交换的过程仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中

参看:http://www.cnblogs.com/SaraMoring/p/5713732.html

下面这篇文章也相当的经典:

http://blog.csdn.net/wy5612087/article/details/52369677

聊聊JVM的年轻代
1.为什么会有年轻代
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
2.年轻代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

3.一个对象的这一辈子
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
4.有关年轻代的JVM参数
1)-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
2)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

3、对于老年代的的gc采用的是标记-压缩算法 

原理:第一阶段标记活的对象,第二阶段把为标记的对象压缩到堆的其中一块,按顺序放。
优点:1、避免标记扫描的碎片问题;2、避免停止复制的空间问题。

具体使用什么方法GC,Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-扫描”方式;同样,Java虚拟机会跟踪“标记-扫描”的效果,要是堆空间碎片出现很多碎片,就会切换回“停止-复制”模式。这就是自适应的技术。

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。而标记--清除算法会产生内部碎片,所以JVM的设计者们在此基础上进行了改进,标记--压缩算法由此诞生,被应用于老年代内存回收。

标记--压缩算法的标记阶段和标记--清除算法的标记阶段是一致的,就不再重复。使用标记--压缩算法时,标记完可达对象之后,我们不再遍历所有对象清扫垃圾了,我们只需要将所有存活对象向“左”靠齐,让不连续的空间变成连续的,这样就没有内存碎片了。不仅如此,因为不再连续的空间变成连续的,内存分配也更快速了。

对于标记--清除算法来说,因为内存中有碎片,空闲内存不再连续,为了分配内存,系统内可能要维护着一个空闲内存空间的链表。当需要分配内存时,会遍历这个链表,找到一个够大的内存块,然后将其分成两份,一份用作当前的分配,另一份放回链表(这样有造成更多的内存碎片,也有一些策略并不是按顺序查找,找到够大的就好,有可能是找到一个更好的空闲内存块为止)。而对于标记--压缩算法,内存空间是连续的,我们只需要一个指针标记出下一次分配工作要从哪里开始就可以了,分配后将指针递增所分配对象的大小,这个工作是非常快速的,而且不用维护那个空间内存链表了。

这样一看好像标记--压缩算法绝对的优于标记--清除算法,那标记--清除还有啥存在的必要了呢?不过要记住的一点是标记--压缩算法为了达到压缩的目的,是需要移动对象的,这会有性能消耗的,这样所有对象的引用都必须更新。看来有利必有弊。

6. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms,g1

对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。

我们来看下面的代码:

package com.bjsxt.base001;

import java.util.HashMap;
import java.util.Map;

public class Test06 {

    public static void main(String[] args) {
        
        //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000*1024
        Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
        //每次创建1M数据,因为设置了PretenureSizeThreshold=1000*1024,该数据是小于1M(1024*1024)的,所以产生的数据是不能放在新生代的,只能存储在老年区中
        for(int i=0; i< 5; i++){
            byte[] b = new byte[1024*1024];
            m.put(i, b);
        }
    }
}


我们来看下面的案例:
package com.bjsxt.base001;

import java.util.HashMap;
import java.util.Map;

public class Test06 {

    public static void main(String[] args) {
        
        //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000
        Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
      
        for(int i=0; i< 5*1024; i++){
            byte[] b = new byte[1024];//每次产生1K数据
            m.put(i, b);
        }
    }
}

 初始化堆的大小是30M,最大堆是30M,使用串行的垃圾回收器,详细打印出GC的回收信息,

 每次创建1K数据,因为设置了PretenureSizeThreshold=1000,该数据是大于PretenureSizeThreshold=1000的,所以产生的数据是不能放在新生代的,只能存储在老年区中,但是我们来看看运行的结果

Heap
def new generation total 9216K, used 6426K [0x00000000f9000000, 0x00000000f9a00000, 0x00000000f9a00000)
eden space 8192K, 78% used [0x00000000f9000000, 0x00000000f96468e8, 0x00000000f9800000)
from space 1024K, 0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9900000)
to space 1024K, 0% used [0x00000000f9900000, 0x00000000f9900000, 0x00000000f9a00000)
tenured generation total 20480K, used 16K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
the space 20480K, 0% used [0x00000000f9a00000, 0x00000000f9a04010, 0x00000000f9a04200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb077198, 0x00000000fb077200, 0x00000000fc2c0000)
No shared spaces configured.

发现数据都存在新生代区的eden中,而没有存储在老年区中

出现上面的问题的原因在于虚拟机会把体积不大的对象分配到TLAB,1K对象很小就默认分配到了TLAB区域了,我们把配置改成下面的就可以了

-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB

-XX:-UseTLAB表示禁用TLAB区域,我们来看看效果


Heap
def new generation total 9216K, used 760K [0x00000000f9000000, 0x00000000f9a00000, 0x00000000f9a00000)
eden space 8192K, 9% used [0x00000000f9000000, 0x00000000f90be040, 0x00000000f9800000)
from space 1024K, 0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9900000)
to space 1024K, 0% used [0x00000000f9900000, 0x00000000f9900000, 0x00000000f9a00000)
tenured generation total 20480K, used 5416K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
the space 20480K, 26% used [0x00000000f9a00000, 0x00000000f9f4a070, 0x00000000f9f4a200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb077198, 0x00000000fb077200, 0x00000000fc2c0000)
No shared spaces configured.

老年代的使用效率就已经上来了

Java内存模型的主要目标: 定义程序中各个变量的访问规则。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制。
所有变量的存储都在主内存,每条线程还都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存完成,而不能直接读取主内存中的变量。不同的线程直接无法访问对方工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

voliate能够保证变量在线程中的可见性,一个线程修改了该变量,在另外一个线程中可以立刻得到最新的值,不清楚的看voliate关键字的使用

 在JDK 6之后支持对象的栈上分析和逃逸分析,在JDK 7中完全支持栈上分配对象。 其是否打开逃逸分析依赖于以下JVM的设置:

-XX:+DoEscapeAnalysis

import java.lang.management.ManagementFactory;
import java.util.List;


/**
 * 逃逸分析优化-栈上分配
 * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
 * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
 * 虚拟机参数设置如下,表示做了逃逸分析  消耗时间在10毫秒以下
 * -server -Xmx10m -Xms10m
   -XX:+DoEscapeAnalysis -XX:+PrintGC
 * 
 * 虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
 * -server -Xmx10m -Xms10m
   -XX:+DoEscapeAnalysis -XX:+PrintGC
 * @author 734621
 *
 */
public class OnStack {
  public static void alloc(){
      byte[] b = new byte[2];
      b[0] = 1;
  }
  
  public static void main(String [] args){
      long b = System.currentTimeMillis();
      for(int i=0;i<100000000;i++){
          alloc();
      }
      long e = System.currentTimeMillis();
      System.out.println("消耗时间为:" + (e - b));
      
      List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
      for(String p : paramters){
          System.out.println(p);
      }
     
  }
}
进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从JVM内存模型中的堆来分配。

 不清楚的看马士兵的java虚拟机视频在哔哩哔哩上面

原文地址:https://www.cnblogs.com/kebibuluan/p/7736720.html