JVM虚拟机原理

 

 

上面画住的是线程私有的,每启动一个线程,它都会在我们jvm中拿出这三个来,被我们这个线程私有,当我们启动一个线程的时候,我们会为这个线程分配三个区域,java栈,本地方法栈,程序计数器

 

下面就是反编译后的文件

Compiled from "TestJvaa.java"
public class com.luban.test.TestJvaa {
  public com.luban.test.TestJvaa();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int testMath();
    Code:
       0: iconst_4                                                             //将整形常量4放入操作数栈中
       1: istore_1                                                              //将操作数栈顶的类型值存到局部变量表的第n个槽中   

       2: iconst_2
       3: istore_2
       4: iload_1                                                               //将局部变量表中的第一个槽,压入操作数栈顶(这里是复制,这里的复制其实是为了局部变量在这个栈振中能够共享)
       5: iload_2                                                                //将局部变量表中的第二个槽,压入到操作数栈顶

      bipush:讲一个8位带符号整数压入栈      sipush:将一个16位带符号整数压入栈
       6: iadd                                                                    //每执行一个idd它都会弹出栈顶的两个操作数,然后进行相加,再把相加的结果再重新的压入到这个操作数栈中
       7: iconst_4、
       8: imul                                                                   //把栈顶的两个数出栈然后相乘,得出的结果再入栈
       9: istore_3
      10: iload_3
      11: ireturn        记录到出口记录,然后把结果返回到了main函数当中去了

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/luban/test/TestJvaa
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method testMath:()I
      12: pop
      13: return
}

程序计数器的作用就是记录jvm指令执行到哪条了,一位cpu不可能一直为你自己工作,它还要为其它线程工作,等它切回来再为你工作的时候,你得记录到你执行到哪条jvm指令了?

public static void main(String[] args) {
        String s1 ="luban";
        String s ="abc"+"bcd";  //这行代码会产生一个对象为什么呢,下面看下反编译后的字节码

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String luban
       2: astore_1
       3: ldc           #3                  // String abcbcd  
       5: astore_2
       6: return

    }一看就清楚了是几个对象了  目前高版本jvm会对这个字面值进行优化

String和StringBuilder的区别


    public static void main(String[] args) {
        String s1 ="luban";
        String s2 ="abc"+"bcd";
        s1=s2+"aaaaaa";   //加了这行代码之后再去反编译
    }

}

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String luban
       2: astore_1
       3: ldc           #3                  // String abcbcd
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_2
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String aaaaaa
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_1
      26: return
}  
说白了String和StringBuilder在使用级别上是没什么区别,底层把它优化成了StringBuilde

 

public class TestHeadOverFlow {
    public static void main(String[] args) throws InterruptedException {
        List<byte[]> list = new ArrayList<byte[]>();
        for(;;){
            list.add(new byte[1024*1024*500]);
            Thread.sleep(200);
        }
    }  这段代码可以导致heap溢出
public class TestHeadOverFlow {
    public static void main(String[] args) throws InterruptedException {
     
     for(;;){
         byte[] b = new byte[1024*1024*500];
     }
    }
}    而这段代码却不能导致堆溢出,  为什么呢? 我们new出来的这个数组 b是它的引用  但是for循环下次再来的胡又重新给它赋值了

之前得那个数组就变成不可达了,所以就具备回收的条件了,jvm可能随时把它回收掉,那如果我把对象放到集合里面去,那不一样,集合里面是持有引用的,我以后可以随时通过集合找到你,所以不具备回收的条件,所以会存在heap溢出

-Xms  初始java堆大小     初始值是物理内存的  1/64
-Xmx  最大java堆大小    -X选项是非标准选项,如有改动,数不另行通知
-Xss 为jvm启动时候每个线程分配的内存大小,默认是1M

public class TestStackOverFlow {
     @Test
    public void stackOverFlow(){
        print(0);
    }
    public  void print(int n){
        System.out.println(n);
        n++;
        print(n);
    }
}

-Xmn  年轻代 如果调大的话那个地方就长了

java直接访问离堆(0ff-heap)即jvm之外的内存,用的是NIO

我们的通过编译生成的.class文件,知道你的类名以后,通过classLoader首先把类加载到方法区里面,首先我們要知道,lei类加载它并不是实例化的过程,类加载时把类放到jvm的过程,而不是创建对象的过程,创建对象的过程涉及到栈区里面方法执行的过程,这是方法在执行过程用到那个类就去加载哪个类,

很多人不知道这个类是怎么被加载的即我圈的CC那个类,

当你运行java程序的时候,你在dos里运行时肯定要输入java  com.changchang.cn.CC  肯定会指定这个类名的,类加载器首先就会把这个CC类加载进method area 里面,但是你要记得 在启动虚拟机的时候就把java核心类库(rt.jar)全部都加载进来了,

在执行main的时候,碰到没有被加载的类,就看下方法区有没有,如果没有就加载到方法区

类加载器的分类:Bootstrap类加载器,ExtClassLoader,AppClassLoader 每种加载器都设定好从哪里加载类

BootstrapClassLoader--------->JRE/lib/rt.jar

ExtClassLoader-------->JRE/lib/ext或者

 

AppClassLoader ------>CLASSPATH环境变量,由classpath或-cp选项定义

类加载器的工作原理有1.委托机制,加入你要加载Person.class这个类,首先加载这个类的请求由AppClassLoader 委托给它的父类ExtClassLoader,然后再委托给BootstrapClassLoader,BootstrapClassLoader先看看rt.jar里面有没有这个类,如果没有,就返回给ExtClassLoader 查看jre/lib/ext目录下有没有这个类,如果还没有就由AppClassLoader   从classpath中寻找, 记住

classpath定义的是类文件的加载目录,而PATH定义的是可执行程序如javac java等的执行路径

2.可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行,如果ABC.class已经被AppClassLoader 加载过了,再用

ExtClassLoader的话就会抛出java,lang.ClassNotFountException

 

3.单一性:父类加载器加载过的类不能被子类加载器加载第二次

 

 

 

由于方法区里面存放的是类类型(Class),运行时加载类是你用到哪个类先去method area区找如果没有,你就把他加载进来存进方法区,你想呀 我们从main方法进去的时候肯定会遇见没有加载过的类,jam发现这个类没有被加载,就开始查找某某类.class文件,从类文件中抽取类型信息并放在了方法区 ,这个类一旦被加载,它就常驻内存里面了,

方法区溢出是加载太多导致的,堆溢出是加载对象太多导致的      栈溢出是方法太多导致的,如方法的死递归

类型信息:

对于每个加载的类型,jvm必须在方法区存储以下类型信息: 

方法区是线程共享的,当两个线程同时要加载一个类型的时候,只有一个类会请求Classloader加载,另一个线程会等待。

1:这个类型的完整有效名

2.这个类型直接父类的完整有效名(除非这个类是interface或者是java.lang.Object,这两种情况都没有父类

3.这个类型的修饰符(public,abstract,final)

4.即时编译后的代码等信息

除了这些基本信息外,jvm还要为每个类型保存一下信息:

类型的常量池:

field信息

方法信息

所有static变量 ,在这里提一点就是可以把全局变量放在静态代码块里面,

 

在这之前先讲一下Class这个类的概念

                           class Person{

                            }这里有个Person类,这个类的实例对象如何表示呢?下面这句话就是了

                          Person   p   =     new Person();    p就是Person这个类的实例对象,

                        class Student{

                            }

                         Student student = new Student();    student就是Student这个类的实例对象,

由于java里面万事万物皆对象  这个Person,Student类 也是一个实例对象,它是大写Class类的实例对象 ,也就是说我们平常所见到的类都是Class的实例对象   那么改如何表示呢?有三种表示方法:我们就举上面的Person类吧

 

    1:Class c1 = Person.class(即类名.class)

    2:  Class c2 = p.getClass(即对象.getClass)    这里的p是 大写Person类的实例对象

 写到这里可能你们会有些混淆:我再重申一下  上面的 p是Person类的实例对象,c1 c2代表 Class类的实例对象,

但是这个c1实例对象说的是什么玩意呢,说的是Person这个类本身就是一个实例对象

官网给出的准确说法是: c1 c2是 Person类的类类型(class type) ,类的类类型怎么理解呢,你就这样理解,Person类本身就是Class类的一个实例对象 ,你要清楚一点Person这个类本身也有实例对象,就是p, 我们以后就可以这么叫了 p是Person类的实例对象,c1是Person类的类类型

    3:c3=Class.forName("类的路径");      这里c1==c2==c3的

我们完全可以通过类的类类型创建该类的实例--->通过c1,c2,c3创建Person类的实例对象  

如果是Person类的类类型,创建出来的是Person对象,如果是Student类的类类型,创建出来的是Student对象,所以要进行强转

即:Person person=(Person)c1.newInstance();   这里newInstance前提是要有要有无参构造的

 

上面讲到的Class.forName(); 不仅表示了类的类类型,还代表了动态加载类,大家要区分编译和运行,

编译时刻加载类是静态加载类,运行时刻加载类是动态加载类,这里说下静态加载类

我们在dos里面输入 javac Office.java  来进行编译,会报错,因为缺少Word类跟Excel类

我们如果建一个Word类,然后 进行编译后,再去编译javac.Office.java就会报只缺少Excel这个类的错误了,在这里我们已经建好了Word类,想用却因为没有Excel类的原因用不了,这显然不是我们想要的,我们要的是,我建一个Word类我就能用,大家想想,如果程序里有100个类,有一个用不了其余的99都用不了的话你将会有多么的崩溃呀,这就是静态加载类的弊端,new创建对象是静态加载类,在编译的时候就需要加载所有可能使用到的类,不管你用不用,

 通过动态加载类我们可以解决此问题,Class.forName,就是动态加载类,就不会在编译时候报错了,它只会在你用到这个类的时候才会报没有类的错误,

 

 

 

 java虚拟机栈就是java方法执行的内存模型,每调用一个方法,就会生成一个栈帧 用于存储方法的本地变量表,方法出口等信息

每次方法的调用都会对应栈帧的压栈跟出栈,如果请求的栈的深度过大,就会抛出stackOverflowError

 

 

 

热部署其实是也可以通过java的热加载来实现的

上面这个图的准备阶段做一下描述,假如说private static int num=80; 在这个准备阶段,它并不是把这个num初始化成为80 ,而是赋初始值,int类型的初始值为所以准备阶段是num的值是0,初始化阶段num的值才为80

什么时候初始化呢,5个初始化的时机,

需要先触发类初始化(加载-验证-准备自然需要在此之前): 
1)1.使用 new 关键字实例化对象 2.读取或设置一个类的静态字段 3.调用一个类的静态方法。 
2)使用 java.lang.reflect 包的方法对类进行反射调用时,若类没有进行初始化,则需要触发其初始化 
3)当初始化一个类时,若发现其父类还没有进行初始化,则要先触发其父类的初始化。 
4)当虚拟机启动时,用户需要制定一个要执行的主类(有 main 方法的那个类),虚拟机会先初始化这个类。

final  修饰的类会在编译期的时候把结果放在常量池,即使调用也不会触发初始化  ,final修饰的是个常量,它会把常量放在常量池中,调用常量它不会出发初始化的这个阶段 

 

 

       

展开阅读全文

没有更多推荐了,返回首页