java虚拟机学习资料

173 阅读16分钟

java虚拟机学习资料

谈谈你对java的理解

  • 平台无关性
  • GC
  • 语言特性
  • 面向对象
  • 类库
  • 异常处理

java如何实现平台无关性

  • 编译时

  • 运行时

java找不到主类

在包下的类,在Java源文件的地方编译后,需要到最外层包的上一级目录下运行,而且类前面需要带包名,以.隔开。

查看字节码

java -p

Compiled from "test.java"
public class connector.test {
  public connector.test(); //这是一个无参构造函数
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V 调用父类的构造方法
       4: return                            // 退出构造函数

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1                  //常量1放到栈顶
       1: istore_1                  //将栈顶的值放到局部变量1中
       2: iconst_1                  //将常量5放到栈顶
       3: istore_2                  //将栈顶的值放到局部变量2中
       4: iinc          1, 1        //将变量1加上1
       7: iinc          2, 1        //将变量2加上1
      10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream; //静态变量加入1
      13: iload_1                           //将变量1的值压入栈顶
      14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      17: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      20: iload_2
      21: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      24: return
}

1.编写java文件,.java文件 2.生产字节码,就是.class文件。 3.有了.class文件,jvm加载JVM来进行执行

java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令

jvm如何加载.class文件

  1. class loader:依据特定格式,加载class文件到内存
  2. execution Engine:对命令进行解析
  3. Native Interface:融合不同的开发语言的原生库为java所用
  4. Runtime Data Area:JVM内存结构模式

主要是由class loader将符合规范的class文件加载到内存,然后通过execution engine来进行解析,最后提交给本地接口来进行使用

谈谈反射

java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制

        Class clazz=Class.forName("basic.reflection.Robot");
        Robot   r=(Robot)clazz.newInstance();
        System.out.println("class name is "+clazz.getName());
        Method getHello=clazz.getDeclaredMethod("helloPerson",String.class);
        getHello.setAccessible(true);
        Object str=getHello.invoke(r,"Bob");
        System.out.println("getHello result is " +str);
        Method printName=clazz.getMethod("printName",String.class);
        printName.invoke(r,"wangming");
        Field name=clazz.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r,"Alice");
        printName.setAccessible(true);
        printName.invoke(r,"welcome");

getmethod和 getdeclearmethod区别

getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。

getMethod:获取当前类和父类的所有public的方法。这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。

类从编译到执行的过程

  1. 编译器将Robot.java源文件编译为Robot.class字节码文件
  2. 类加载器将字节码转换为JVM中的Class对象

类加载器(class Loader)

  1. ClassLoader主要在Class的装载阶段,主要作用是从系统获得Class二级制数据流。它是Java的核心组件,所有的Class是由classLoader来进行加载的。classloader负责通过将class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化操作。

ClassLoaderd的种类

  • BootStrapClassLoader:C++编写,加载核心库java.*
  • ExtClassLoader:Java编写,加载扩展库javax.*
  • AppClassLoader: Java编写,加载程序所在目录
  • 自定义classLoader:java编写,定制化加载
自定义ClassLoader的实现
  • findclass
  • 修改二进制流的代码,给类添加点信息。asm
  • 字节码
  • AOP
public class MyClassLoader extends  ClassLoader {
   private  String path;
   private String classLoaderName;
   public  MyClassLoader(String path,String classLoaderName){
       this.path=path;
       this.classLoaderName=classLoaderName;
   }
   //用于寻找类文件
   @Override
   public  Class findClass(String name){
       byte[] b=loadClassData(name);
       return  defineClass(name,b,0,b.length);
   }

   //用于加载类文件
   private byte[] loadClassData(String name) {
       name=path+name+".class";
       InputStream in =null;
       ByteArrayOutputStream out=null;
       try{
           in=new FileInputStream(new File(name));
           out=new ByteArrayOutputStream();
           int i=0;
           while ((i=in.read())!=-1)
           {
               out.write(i);
           }
       } catch (FileNotFoundException e) {
           e.printStackTrace();
       } catch (IOException e) {
           e.printStackTrace();
       }finally {
           try {
               out.close();
               in.close();
           } catch (Exception e)
           {
               e.printStackTrace();
           }

       }
       return  out.toByteArray();
   }

}

谈谈类的加载器的双亲委派机制

1.自底向上检查类是否已经加载 2.自顶向下尝试加载类

有可能多个线程加载同一个类,所以会加一个同步锁

主要是发生在loadclass这个方法中,首先先加个锁,然后判断findloadedclass,当前类是否已经加载,如果为空的话就判断parent是否存在。parent是app-classloader.app-classloader,app-classloader又会调用这个loadclass这个方法。去看看app-classloader是否已经装载过。判断parent是否存在,如果存在的话 就掉用app-classloader的父类,extclass-loader。如果依旧没有装载的话,就继续判断parent是否存在,因为bootstrapclasscloader是用C++写的。肯定为空,就直接调用findbootstrapclass方法。如果findbootstrap这个方法还没有的,他就自己去调用findclass方法。 java6

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

为什么要使用双亲委派机制去加载类

  • 避免多分同样字节码的加载

JDK代码闭源, openjdk开源。

hg.oepnjdk.java.net/jdk8u/jdk8u/jdk/file

类的加载方式

  • 隐式加载:new
  • 显示加载:loadClass,forName等。

loadCLass和forName的区别

类的装载过程

  • 加载 通过ClassLoader加载class文件字节码,生成Class对象
  • 链接 1. 校验:检查加载的class的正确性和安全性 2. 准备:为类变量(static)分配存储空间并设置类变量初始值 3. 解析: JVM将常量池内的符号引用转换为直接引用
  • 解析 执行类变量赋值和静态代码块
  1. loadclass是加载类,是在链接之前。是还没有进行连接的。只完成了加载,链接和解析都完成了。loadclass在spring AOC 要读取一些配置的时候,如果是以classpath的方式来。 和spring aoc的lazy loading有关。spring aoc为了加快这个加载速度,大量使用延迟加载技术。使用classloader不需要使用链接和解析等步骤。这样可以加快加载速度
  2. class.forname得到的class是已经初始化完成之后的。要使用静态代码块,一般使用forname

JAVA内存模型

线程私有和线程共享
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:MetaSpace、Java堆 
  • 线程的私有区域 程序计数器

    • 当前线程锁执行的字节码行号指示器(逻辑),逻辑计数器
    • 改变计数器的值来选取吓一跳需要执行的字节码指令
    • 和线程是一对的关系及"线程私有"
    • 对java方法计数,如果是Native方法则计数器值为Undefined
    • 不会发送内存泄露

    Java虚拟机栈(Stack)

    • Java方法执行的内存模型
    • 包含多个栈帧- 局部变量表 :包含方法执行过程中的所有变量 操作数栈:入栈、出栈、复制、交换、产生消费变量

    javap verbose 口语化来描述.class文件。 istore 是从操作数栈到局部变量表 iload 是从局部变量表load到操作数栈

  • 本地方法栈

    • 与虚拟机栈类似,主要作用于标注了native的方法
  • 线程的共享区域 1.6才有永久代,1.8没有永久代

  • 元空间(metaspace)与永久代(PermGen)的区别 元空间使用本地内存,而永久代使用的是JVM的内存

MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问和内存溢出

  • 类和方法信息大小难易确定,给永久代的大小指定带来困难

  • 永久代会为GC带来不必要的复杂性

  • 方便hotspot与其他JVM如Jrockit的集成

  • Java堆(Heap)

    • 对象实例的分配区域
    • GC管理的主要区域
  • metaspace(元空间)

JVM三大性能调优参数-Xms -Xmx -Xss的含义

- Xss:规定了每个线程虚拟机栈(堆栈)的大小
- Xms:堆的初始值
- Xmx: 堆能达到的最大值

Java内存模式中堆和栈的区别

  1. 内存分配策略
  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
  • 栈式存储:数据区需要在编译时未知,运行时模块入口前确定
  • 堆式存储:编译时或运行时候模块入口都无法确定,动态分配
  1. 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
  2. 管理方式:栈自动释放,堆需要GC
  3. 空间:栈比堆小
  4. 碎片相关:栈产生的碎片远小于堆
  5. 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  6. 效率:栈的效率比堆高

不同JDK版本之间的Intern()方法的区别--JDK6 VS JDK 6+

JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池种的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。

JDK6+ :

    String s = new String("1");  
    String s2 = "1";  
    s.intern();  
    System.out.println(s == s2);  
    String s3 = new String("1") + new String("1");  
    String s4 = "11";  
    s3.intern();  
    System.out.println(s3 == s4);  
    String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。
    String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象,但是发现已经存在了,那么就直接指向了它。

    s.intern(),这一行在这里就没什么实际作用了。因为"1"已经存在了。
    结果就是 s 和 s2 的引用地址明显不同。因此返回了false。
    String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
    String s4 = "11", 这一行代码会直接去生成常量池中的"11"。
    s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。

JDK 6以后是能够把常量池的引用放进去,jdk6以前是只能放副本进去,放引用的话地址就有可能一样

结果就是 s3 和 s4 的引用地址明显不同。因此返回了false 和堆空间中的字符串对象。当调用intern方法时候,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于JAVA堆中,则将堆中堆此对象的引用添加到字符串常量池中,并且返回该引用。如果堆中不存在,则在池中创建该字符串并返回其引用。

字符串常量池在 Java 内存区域的哪个位置

  1. 在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区(也就是方法区)中,此时常量池中存储的是对象。
  2. 在 JDK7.0 版本,字符串常量池被移到了堆中了。此时常量池存储的就是引用了。在 JDK8.0 中,永久代(方法区)被元空间取代了。

JAVA 垃圾回收机制

判定对象是否被回收的依据

  • 引用计数法
    1. 通过判断对象的引用数量来决定对象是否可以被回收
    2. 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
    3. 任何引用计数为0的对象实例可以被当做垃圾收集
    4. 执行效率比较高,程序执行受影响较小。无法检测出循环引用的情况,导致内存泄露
  • 可达性分析算法
    1. 通过判断对象的引用链是否可达来决定对象是否可以被回收
    2. 判断GC ROOT对象和该对象是否关联。如果是关联的就代表可达.
    3. 可以作为GC ROOT的对象
      1. 虚拟机栈中引用的对象(栈帧中的本地变量表)
      2. 方法区中的常量引用的对象
      3. 方法区中的类静态属性引用的对象
      4. 本地方法栈中JNI(Native方法)的引用对象
      5. 活跃线程的引用对象

几种垃圾回收算法

标记-清除算法(Mark and Sweep)

  1. 标记:从根集合进行扫描,对存活的对象进行标记
  2. 清除: 对堆内存从头到尾进行线性遍历,回收不可达对象内存。
  3. 缺点:碎片化

复制算法(copying)-适合青年代

  1. 分为对象块和空闲块
  2. 对象在对象快上创建
  3. 存活的对象被从对象块复制到空闲块上
  4. 将对象块所有对象内存进行清除
  5. 优点
    1. 解决了碎片化问题
    2. 顺序分配内存,简单高效
    3. 适用于对象存活率低的场景,在青年代

标记-整理算法(Compacting)-适合老年代

  1. 标记:从根集合进行扫描,对存活的对象进行标记
  2. 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。不分对象快和内存块
  3. 缺点:移动成本高
  4. 优点
    1. 避免内存的不连续行
    2. 不用设置两块内存互换
    3. 适用于老年代的场景

分代收集算法

  1. 垃圾回收算法的组合拳
  2. 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
  3. 目的:提高加垃圾回收的效率
GC的分类
  1. 年轻代
  • GC方式:Minor GC
    1. 复制算法
  1. 年轻代: 尽可能快速收集掉那些生命周期短的对象
    • Eden区:伊甸园,初始,如果eden区放不下 就放在 survivor区 8/10 份
    • 两个Survivor区,from区和to区是可以相互转换的:
      • from区 1/10
      • to 区 1/10
    • 年轻代占用堆1/3
    • MaxTenuringThrehold
  2. 老年代 -GC方式:Full GC和Major GC - 标记清理算法 - 标记整理算法 Full GC比Minor GC 慢,但执行频率低
触发FULL GC的条件
  1. 老年代的空间不足

  2. 永久代空间不足

  3. CMS GC时出现 promotion failed,concurrent mode failure

  4. Minor GC晋级到老年代的平均大小大于老年代的剩余空间

  5. 调用system.gc()

  6. 使用RMI来进行RPC或管理的JDK应用,每小时执行1次FULL GC

对象如何晋升到老年代
  1. 经历一定Minor GC次数依然存活的对象

  2. Survivor区中存放不下的对象

  3. 新生成的大对象(-xx :+PreenureSizeThreshold)

常用的性能调优的参数
  1. -XX:SurvovorRatio:Eden和Survivor,默认8:1
  2. -XX:NewRatio:老年代和年轻代内存大小的比例
  3. -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值
STOP-THE-World
1. JVM由于要执行GC而停止了应用程序的执行
2. 任何一种GC算法都会发生
3. 多数GC优化通过减少Stop-the-world发生的时间来提高程序性能
safepoint
1. 分析过程中对象引用关系不会发生变化的点
2. 产生Safepoint的地方:方法调用;循环跳转;异常跳转等
3. 安全点数量得适中
常见的垃圾收集器

JVM的运行模式

  • server
    • 启动慢,但是使用要比client块。
  • client
    • 启动块,但是使用比server慢
年轻代常见的垃圾收集器
  • Serial收集器(-XX:+UseSerialGC,复制算法)

    • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效,Client模式下默认的年轻代收集器
  • ParNew收集器(-xx:+UseParNewGC,复制算法)

    • 多线程收集,其余的行为、特定和Serial收集器一样
    • 单核执行效率不如Serial,在多核执行才有优势
  • Paralle Scavenge收集器(-xx:+UseParallelGC,复制算法)

    • 吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
    • 比起关注用户线程停顿时间,更关注系统的吞吐量
    • 在多核执行才有优势,Server模式下默认的年轻代收集器
    • -XX:+UseAdaptiveSizePolicy
老年代常见的垃圾收集器
  • Serial Old收集器

    • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效,Client模式下默认的老年代代收集器
  • Parallel Old收集器(-xx:+UseParallelOldGC,标记整理算法)

    • 多线程收集,吞吐量优先
  • CMS收集器(-xx:+UseConcMarkSweepGc,标记-清除算法)

    • 硬件厉害,建议CMS

    • 初始化标记:STOP-the-world,只查看直接关联GC root的对象

    • 并发标记:并发追溯标记,程序不会停顿

    • 并发预清理:查找执行并发标记节点从年轻代晋级到老年代的对象,减少下一阶段标记的工作量

    • 重新标记:STOP-THE-WORLD,暂停虚拟机,扫描CMS堆中的剩余对象

    • 并发清理:清理垃圾对象,程序不会停顿

    • 并发重置:重置CMS的数据结构

垃圾的产生如果在标记后产生,就只能等到下次再清理。标记清除算法。会出现内存空间碎片化的问题

G1收集器(-XX:+UseG1Gc,复制+标记-整理算法)

  • 并行和并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

garbage First收集器

  • 将整个Java对内存划分为多个大小相等的region

  • 年轻代和老年代不再物理隔离

object的finalize()方法的作用是否与C++的析构函数作用相同

- 与c++的析构函数不同,析构函数调用确定,而它是不确定的
- 将未引用的对象放置于F-Queue队列
- 方法执行随时可能会被终止
- 给与对象最后一次重生的机会

java中的强引用、软引用,弱应用,虚应用

    强引用:new ,即时oom也不会回收
    软引用:对象处在有用非必须的状态,只有当内存空间不足时,GC会回收该引用的对象内存。用来实现高速缓存。
    弱引用:非必须的对象,比软应用更弱一些。GC时会被回收。
    虚引用:跟踪对象被垃圾收集器回收的活动,起哨兵作用,必须和referenceQueue联合使用

引用队列(referenceQueue)
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用以及虚引用