本文共 8727 字,大约阅读时间需要 29 分钟。
.java文件–>(javac编译生成字节码)–>.class文件---->JVM解析,转换成特定平台的机器指令---->
java不直接将源码解析成机器码执行原因:省去了每次直接解析成机器码需要执行各种词法语法编译虚拟机通过Classloader加载.class文件到内存(Runtime Data Area), 由Excution Engine解析文件里的字节码,再交给操作系统去执行。另外,本地库接口也在加载期间提供一些java的原生方法(如Class.forName())
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jfom8BK-1582447361841)(JVM虚拟机_files/index.htm)]反射是让java在运行期间,可以动态地获取任意类的方法和属性,或动态地调用对象方法的功能
写一个例子:public class Robot{//定义一个有属性有方法的类来反射 private String name; public void sayHi(String hi){System.out.println(hi+""+name);} private String throwHi(String tag){return "Hi"+tag;}}public class reflectSample{//获取前面类的方法和属性 public static void main(String[] args){ Class rc = Class.forName("com...Robot");//robot类全路径 Robot r = (Robot)rc.newInstance(); //创建Class对象rc的实例.并强转成Robot类型对象 rc.getName(); r.sayHi(String hello);//调用public方法,hello就可以 }}
从例子也可看出类从编译到执行的过程:
1.javac将Robot.java源文件编译成Robot.class字节码文件 2.ClassLoader将字节码转换为JVM中的Class对象 3.JVM利用Class对象实例化Robot对象由 C++ 语言实现,是虚拟机自身的一部分。负责将存在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类库加载到虚拟机内存中。
由 Java 语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader,由Sun包里的Launcher类来加载。扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录中的,或系统变量指定路径中的类库,开发者可以直接使用。父类加载器为null
即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
具体实现:加载时,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。以下是ClassLoader.java源码private final ClassLoader parent; protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查请求的类是否已经被加载过 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理 c = parent.loadClass(name, false); } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //抛出异常说明父类加载器无法完成加载请求 } if (c == null) { 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; } }
为什么用双亲委派?1.可以避免类的重复加载(名字相同的类文件被不同的类加载器加载产生的是两个不同的类)2.也保证了 Java 的核心 API 不被篡改。
概念:Java用来确保多线程下内存结果正确的一套抽象规范,Java5假设每个线程都有自己的工作内存,里面存主内存的副本互不干扰,线程之间变量的传递通过主内存进行。
1 方法区(共享,元数据区)
这块“永久代”目前被称为MetaSpace,Java程序编译成的字节码文件首先就加载到方法区,主要存放静态数据。JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。 这块内存在程序整个运行期间一直存在 2 堆(共享区域) 存放对象实例的不连续的内存区域,JVM种最大的一块内存。【内存泄漏优化的关键存储区,引申GC】 3 栈(Java方法栈) 是存放方法的局部变量和字节码操作数的一块连续内存区域。执行方法时会分配栈帧来存内容,方法执行结束时这些存储单元就会自动被注释掉。大小是由操作系统决定的,先进后出,进出完成不会产生碎片,运行效率高且稳定。 4 本地方法栈 与Java栈类似,区别是负责Native方法 5 程序计数器 用于记录字节码当前执行的行号,以便线程切换后都能回到正确位置。如果线程执行Java方法,PC记录当前字节码指令地址,如果执行Native方法则为空。是唯一一块无任何OOM情况的区域。Class demo{ static int d = 0;static int add(int a, int b) { return a + b;}native int read() { }public static void(String[] args) { int a = 1; //栈区存储局部变量a,线程独占 String b = new String(); //堆区分配String对象内存 String c = "hello"; //方法区存储常量"hello" d = 2; //方法区存储静态变量d,线程共享 int e = add(1, 2); //线程执行add,栈帧负责push方法和形参,pop返回值 int f = b.length();//同上,PC记录执行位置 int g = read(); //本地方法区执行read } }
JMM模型是Java抽象出来保证多线程情况下内存结果正确的一套规范。为了应用程序不受数据竞争的干扰,Java 假设每个线程都有自己的工作内存,线程间变量的传递需要通过主内存来完成。
JMM最为重要的概念便是 happens-before 关系。happens-before 关系是用来描述两个 操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。javan内存模型课:操作 X happens-before 操作 Y,使得操作 X 之前的字节码的结果对操作 Y 之后的字节码可见。堆内存可分为eden 区、s0(“From”) 区、s1(“To”) 区都属于新生代,tentired 区属于老年代。**Eden 区远大于 S0,S1 的原因:**因为在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少量存活的对象。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,看此对象是否有必要执行 finalize() 方法。如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记,真正回收。标记清除算法
标记出所有要回收的对象,然后统一回收标记对象。 优点:不需移动对象,且只处理不存活的对象,在存活对象比较多的情况下极高效。两个不足:(1)标记和清除过程效率不高,因为要使用一个空闲列表来记录所有的空闲区域以及大小。(2)标记清除之后会有大量不连续的内存碎片。如果分配大对象(大数组)时找不到足够的连续内存空间容易OOM 复制算法 将内存空间一分为二,每次用一半空间存对象,要垃圾回收时就把存活对象放到另一半,然后对之前一半进行彻底清空。 优点:(1)标记阶段和复制阶段可以同时进行。(2)每次只对一半内存进行回收且不用考虑内存碎片问题,运行高效。(3)只需移动栈顶指针,按顺序分配内存即可。缺点:可一次性分配的最大内存缩小了一半。 +标记整理算法 基于Compacting算法,第一阶段从根节点开始标记所有被引用对象,第二阶段把存活对象“压缩”到堆的其中一块,清除掉其它空间。 优点:此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 缺点:GC暂停的时间会更长,因为将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。 分代收集算法 在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。Minor GC和Full GC
Minor GC是回收新生代,对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁。Minor GC 有一个问题:在标记时,扫描到老年代的对象引用了新生代的对象,那么这个引用也被作为 GCRoots。HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。由于 Minor GC 伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。
java虚拟机有“client”和“server”两种运行模式,用java -version可查看是Server VM 便是重量级虚拟机,启动慢运行快。垃圾收集器与JVM具体实现场景紧密相,不同JVM提供的选择也不同。
+nSerial Old 收集器
Serial 收集器的老年代版本,同样是单线程。作为 CMS 收集器的后备方案。 +nParallel Old 收集器 Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器配合 Parallel Old 收集器。一款面向服务器的垃圾收集器,主要针对配备多处理器,大容量内存的机器. 满足GC停顿时间要求的同时,还具备高吞吐量。特点如下:
(JDK1.7 中 HotSpot提出目的就是代替CMS,现已成功) 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个CPU来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 可预测的停顿:G1 收集器在后台维护了一个优先列表,将堆内存划分为多个相同的Region。每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GF 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。一般进行回收的只有软引用和弱引用?
解决技术是TLAB(Thread Local AllocationBuffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)
每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB。 这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。如果该线程 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的TLAB。转载地址:http://ryrsi.baihongyu.com/