前言
知识学了又丢,不得不开个专场,把「JVM」的内容专门放在这里,常更新,欢迎收藏~
JVM
为什么要先编译
成字节码
- 为了实现跨平台「一次编写,到处运行」
JVM如何加载
.class文件
-
JVM 全称是Java Virtual Machine ,Java 虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare 不一样,那个虚拟的东西你是可以看到的,这个JVM 你是看不到的,它存在内存中。
-
内存中的虚拟机,JVM 的命令集则是可以到处运行的,因为JVM 做了翻译,根据不同的CPU ,翻译成不同的机器语言。
-
JVM的组成部分
-
Class Loader:依据特定格式,加载class文件到内存
-
Execution Engine:对命令进行解析
-
Native Interface:融合不同开发语言对原生库为Java所用
-
Runtime Data Area:JVM内存空间结构模型
-
参考
谈谈反射
native方法(Class.forName)
-
对反射的最初接触是学习jdbc时,加载数据库驱动时会这样写:
Class.forName("com.mysql.jdbc.Driver")
-
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
-
通过代码理解原理
-
创建类com.dai.Robot
- 该类创建的目的,是方便类com.dai.ReflectSample的反射调用
package com.dai; public class Robot { private String name; static { System.out.println("I am first"); } public Robot() { this.name = "robot"; } public void sayHi(String helloSentence) { System.out.println(helloSentence + " " + name); } private String throwHello(String tag) { return "Hello " + tag; } }
-
创建类com.dai.ReflectSample
- 引入一个类"com.dai.Robot",反射实例化该类;
- forName()这个静态方法调用了启动类加载器(就是加载javaAPI的那个加载器,也就是下文的ClassLoader)时,VM会执行该类的静态代码段。
- newInstance()只能调用无参构造函数(new():强类型。相对高效。能调用任何public构造函数)。
- newInstance()是实现IOC、反射、依赖倒置 等技术方法的必然选择,new 只能实现具体类的实例化,不适合于接口编程。类里面就是通过这个类的默认构造函数构建了一个对象,如果没有默认构造函数就抛出InstantiationException, 如果没有访问默认构造函数的权限就抛出IllegalAccessException
- 引入一个类"com.dai.Robot",反射实例化类方法,
- getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。
- getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。
- 对于private的函数,需要设置可访问为true,再唤醒invoke执行实例对应的方法
package com.dai; import java.lang.reflect.Method; /** * author daioo * create 2019-03-05 00:20 */ public class ReflectSample { public static void main(String[] agrs) throws ReflectiveOperationException { Class rc = Class.forName("com.dai.Robot"); System.out.println("Class name is " + rc.getName()); Robot r = (Robot) rc.getDeclaredConstructor().newInstance(); r.sayHi("hello"); Method getHello = rc.getDeclaredMethod("throwHello", String.class); getHello.setAccessible(true); Object str = getHello.invoke(r, "Bob"); System.out.println("getHello result is " + str); } }
- 引入一个类"com.dai.Robot",反射实例化该类;
-
-
参考
谈谈ClassLoader
创建一个自定义ClassLoader
-
Java的核心组件。ClassLoader主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流,通过上文说的反射技术,将其装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
-
IDEA中,MAC默认按键COMMAND+O查找类"classloader"源文件,我们将要用的函数"findClass"、“loadClass”
-
通过代码理解原理
-
创建系统外部Class二进制文件
-
在桌面创建一个外部引用文件Wali.class(具体操作是:先写wali.java文件,在用命令javac编译为.class文件
-
先写wali.java文件
public class Wali{ static{ System.out.print("hello Wali"); } }
-
编译java文件为class文件
-
-
创建类com.dai.MyClassLoader
-
函数findClass,重写ClassLoader类的函数,读取二进制流,返回类文件
-
函数loadClassData,从系统外部获得Class二进制数据流
package com.dai; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; /** * author daioo * create 2019-03-05 14:33 */ 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; byte[] out = null; try{ in = new FileInputStream(new File(name)); out = new byte[in.available()]; in.read(out); } catch (Exception e) { e.printStackTrace(); } finally { try { in.close(); } catch (Exception e) { e.printStackTrace(); } } return out; } }
-
-
创建类com.dai.ClassLoaderChecker
-
测试类,通过反射调用外部系统.class实例化的类的函数
package com.dai; /** * author daioo * create 2019-03-05 14:43 */ public class ClassLoaderChecker { public static void main(String[] args) throws Exception{ MyClassLoader m = new MyClassLoader("/Users/daioo/Desktop/", "myClassLoader"); Class c = m.loadClass("Wali"); System.out.println(c.getClassLoader()); c.getDeclaredConstructor().newInstance(); } }
-
-
谈谈类加载器的双亲委派机制
-
双亲委派模型的源码实现
主要体现在ClassLoader的loadClass()方法,思路很简单:先检查是否已经被加载,若没有被加载则调用父类的LoadClass()方法,若父类加载器为空,则默认使用启动类加载器作为父类加载器,如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载
-
举个栗子
例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。 -
通过代码理解原理
-
依然用上文用到的类com.dai.ClassLoaderChecker文件
package com.dai; /** * author daioo * create 2019-03-05 14:43 */ public class ClassLoaderChecker { public static void main(String[] args) throws Exception{ MyClassLoader m = new MyClassLoader("/Users/daioo/Downloads/", "myClassLoader"); Class c = m.loadClass("Wali"); System.out.println(c.getClassLoader()); System.out.println(c.getClassLoader().getParent()); System.out.println(c.getClassLoader().getParent().getParent()); System.out.println(c.getClassLoader().getParent().getParent().getParent()); c.getDeclaredConstructor().newInstance(); } }
-
-
参考
loadClass和forName的区别
装载:通过ClassLoader加载class文件字节码,生成Class对象
链接:其中解析步骤是可以选择的
- 检查:检查载入的class文件数据的正确性
- 准备:给类的静态变量分配存储空间 并设置类变量初始值
- 解析:JVM将常量池的符号引用转成直接引用
初始化:对静态变量,静态代码块执行初始化工作
- 透过源码分析
Class.forName(className)
方法,内部实际调用的方法是Class.forName(className,true,classloader);
ClassLoader.loadClass(className)
方法,内部实际调用的方法是ClassLoader.loadClass(className,false);
- 第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行
- 参考
你了解Java的内存模型么
-
内存简介
- 需要你了解 32位机器和64位机器的区别
-
地址空间的划分
- 内核空间
- 用户空间——java运行时实际运行的空间
-
内存模型
- Runtime Data Area 运行时数据区
-
线程私有:程序计数器、虚拟机栈、本地方法栈
-
线程共享:MetaSpace、Java堆
-
程序计数器
- 当前线程所执行的字节码行号指示器(逻辑),不是物理计数器
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即“线程私有”,即每一个线程都有一个独立的程序计数器
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄露
-
局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
-
虚拟机栈
- java方法执行的内存模型,包含多个栈帧
-
递归为什么会引发java.lang.StackOverflowError异常
- 递归过深,栈帧数超出虚拟栈深度,如Fibonacci(1000000)
-
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
public void stackLeakByThread(){ while(true){ new Thread(){ public void run(){ while(true){ } } }.start(); } }
-
本地方法栈
- 本地方法栈和Java栈所发挥的作用非常相似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
- 本地方法栈与虚拟机用到的 Native 方法相关,区别就是:
- Java栈为JVM执行Java方法服务
- 本地方法栈为JVM执行Native方法服务。
-
元空间(MetaSpace)
- 元空间(MetaSpace)与永久代(PermGen)的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存
java.langOutOfMemoryError:PermGen space
- MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出(看下文不同JDK版本之间的intern()方法的区别)
- 类和方法信息大小难以确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot与其他JVM如Jrockit的集成
- 元空间(MetaSpace)与永久代(PermGen)的区别
-
Java堆(Heap)
- 在JVM启动时创建;堆是存储Java对象的地方;
- GC管理的主要区域,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代
-
JVM三大新能调优参数-Xms -Xmx -Xss的含义
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
-
Java内存模型中堆和栈的区别
-
内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配,比如可变长度串、对象实例
-
堆和栈的联系
- 引用对象、数组时,栈里定义变量保存堆中目标的首地址。使得可以在栈中的变量使用堆中的数组
-
-
总结
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高;栈操作简单只有push/pop,堆却灵活,动态分配
-
元空间、堆、线程独占部分间的联系 - 内存角度
-
不同JDK版本之间的intern()方法的区别—JDK6 VS JDK6+
String s = new String("a"); s.intern();
-
JDK6:调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用。否则,将此字符串对象添加到常量池中(即在池中创建该字符串),并且返回该字符串对象的引用。*
-
JDK6+:调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
-
通过代码理解区别
public class InternDifference { public static void main(String[] args) { String s = new String("a"); s.intern(); String s2 = "a"; System.out.println(s == s2); String s3 = new String("a") + new String("a"); s3.intern(); String s4 = "aa"; System.out.println(s3 == s4); } }
- jdk6输出
falase false
- jdk6+输出
false true
- jdk6输出
-
通过代码理解「字符串常量池存在永久代中,容易出现性能问题和内存溢出」
package com.dai; import java.util.Random; /** * author daioo * create 2019-03-06 22:21 */ public class PermGenErrTest { public static void main(String[] args) { for(int i = 0; i <= 1000; i++) { gerRandomString(1000000).intern(); } System.out.println(); } private static String gerRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } }
-
如果使用IDEA编译器,可以配置JAVA虚拟机内存大小,操作:菜单栏run->Edit Configurations,在VM options写入
-XX:MaxPermSize=6M -XX:PermSize=6M
-
输出
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at com.dai.PermGenErrTest.main(PermGenErrTest.java:12) Process finished with exit code 1
-
-
参考
-
-
参考