文章目录
1、平台无关性
Compile Once, Run Anywhere如何实现
-
编译 java
javac java全路径
-
javap -c 反汇编class文件
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
-
为什么JVM不直接将源码解析成机器码去执行
- 准备工作:每次执行都需要各种检查
- 兼容性:也可以将别的语言解析成字节码
2、JVM如何加载.clas文件
JVM 是一个内存中的虚拟机,JVM 的存储就是内存,程序中的所有类、常量、变量等都在内存中。
- JVM 架构
- Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析
- Native Interface:融合不同开发语言的原生库为Java所用,Java native 关键字修饰的 native 方法 是调用本地原生方法,提高程序运行效率。
- Runtime Data Area:JVM内存空间结构模型
3、反射
-
概念
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
-
Robot.java
public class Robot {
static {
System.out.println("Hello Robot");
}
private String name;
public void sayHi(String helloSentence){
System.out.println(helloSentence + " " + name);
}
private String sayHello(String tag){
return "Hello " + tag;
}
}
- 反射小例子
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
// Class 类的实例表示正在运行的 Java 应用程序中的类和接口。
// 枚举是一种类,注解是一种接口。
Class rc = Class.forName("com.moc.reflect.Robot");
Robot r = (Robot) rc.newInstance();
System.out.println("Class name is " + rc.getName());
// getDeclaredMethod 获取类的所有方法,不包括继承和实现接口的方法
Method getHello = rc.getDeclaredMethod("sayHello", String.class);
getHello.setAccessible(true);
Object str = getHello.invoke(r, "Bob");
System.out.println("getHello result is " + str);
// getMethod 只能获取类公有的方法,包括继承和实现接口的方法
Method sayHi = rc.getMethod("sayHi", String.class);
sayHi.invoke(r, "Welcome");
// getDeclaredField 获得类的所有属性
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r, "Welcome");
}
}
4、ClassLoader
ClassLoader 在Java中有着非常重要的作用,它主要工作在 Class装载的加载阶段,其主要作用是 **从系统外部获得 Class二进制数据流 **。它是Java的核心组件,所有的 Class都是由 ClassLoader进行加载的,ClassLoader 负责通过将 Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
- 类从编译到执行的过程
- 编译器将 Robot.java源文件编译为 Robot.class字节码文件
- ClassLoader 将字节码转换为 JVM中的 Class<Robot> 对象
- JVM 利用 Class <Robot>对象 去实例化 Robot对象
- ClassLoader的种类
- BootStrapClassLoader :C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载用户程序所在目录
- 自定义ClassLoader:Java编写,定制化加载
// ExtClassLoader 加载的路径
System.out.println(System.getProperty("java.ext.dirs"));
// AppClassLoader 加载的路径
System.out.println(System.getProperty("java.class.path"));
- 自定义ClassLoader的实现
// 关键函数
// findClass 根据位置去加载 .class字节码
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException (name);
}
// defineClass 解析定义 .class字节流,返回 Class对象
protected final Class<?> defineClass (byte[] b, int off, int Len) throws ClassFormatError {
return defineClass(null, b, off, Len, null);
}
- Person.class
// 编译成 .class 文件放入 D盘根目录
public class Person {
static {
System.out.println("Hello World!");
}
}
- 自定义类加载器
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) {
// loadClassData 中可以对二进制流进行改造,(字节码增强技术)
// 应用:远程获取二进制流来获取类,对类的字节码加密解密、
// 给类添加一些信息(AOP实现方式之一)
// JAVAssist: 一个开源的分析、编辑和创建Java字节码的类库
byte[] b = loadClassData(name);
// 直接调用 java提供的 defineClass方法即可
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 (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
- 测试自定义类加载器
public class ClassLoaderChecker {
public static void main(String[] args)
throws ClassNotFoundException,
IllegalAccessException,
InstantiationException {
// 用自定义类加载器加载 D盘里的 Person.class 字节码文件
MyClassLoader m = new MyClassLoader("D:\\", "myClassLoader");
Class c = m.loadClass("Person");
c.newInstance();
// 下小节,类加载器的双亲委派
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());
}
}
5、类加载器的双亲委派机制
- ClassLoader.java 源码
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 {
// 为null时,其父 类加载器为 BootStrap 类加载器,代码由c语言实现
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;
}
}
-
java 开源部分的源码
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/cecd70d27b27
-
为什么要使用双亲委派机制去加载类
避免多份同样字节码的加载。即避免同一分字节码被多次加载,占用系统内存。
6、类的加载方式
隐式加载: new
显式加载: loadClass,forName
- loadClass 和 forName 的区别
- Class.forName 得到的class 是已经初始化完成的,会执行静态代码块;
- Classloder.loadClass 得到的class 是只完成了类的加载,还没有链接、初始化;可以加快加载速度,把类链接、初始化留到真正使用的时候再完成,如 spring IOC 的 lazy load (延迟加载)。
public class LoadDifference {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader cl = Robot.class.getClassLoader();
System.out.println("-------");
Class r = Class.forName("com.moc.reflect.Robot");
System.out.println("-------");
//Class.forName("com.mysql.jdbc.Driver");
}
}
7、java 内存模型
内存简介:
32位处理器:2^32的可寻址范围
64位处理器:2^64的可寻址范围
- JVM内存模型 - JDK8
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:MetaSpace.Java堆
1. 程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即“线程私有”
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄露
2. Java虚拟机栈(Stack)
- Java方法执行的内存模型
- 虚拟机栈包含了单个线程每个方法执行的栈帧,调用每个方法都会创建一个栈帧
- 局部变量表 和 操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
package com.moc.jvm;
public class ByteCodeSample {
public static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
}
- 反编译 javap -c ByteCodeSample.class
Compiled from "ByteCodeSample.java"
public class com.moc.jvm.ByteCodeSample {
public com.moc.jvm.ByteCodeSample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
Code:
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn
}
- 更易读的方式反编译 javap -verbose ByteCodeSample.class
// 文件信息
Classfile /D:/java/advanced-stage/jvm/src/com/moc/jvm/ByteCodeSample.class
Last modified 2019-4-28; size 282 bytes
MD5 checksum 812fa09debbad037b9dac0fba2906a0d
Compiled from "ByteCodeSample.java"
// 描述类信息 公有、继承Object
public class com.moc.jvm.ByteCodeSample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/moc/jvm/ByteCodeSample
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 add
#9 = Utf8 (II)I
#10 = Utf8 SourceFile
#11 = Utf8 ByteCodeSample.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/moc/jvm/ByteCodeSample
#14 = Utf8 java/lang/Object
// 方法区
{
// 初始化方法
public com.moc.jvm.ByteCodeSample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
// 自定义 add方法
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC // 方法为 public、static
Code:
// 操作数栈深度2 本地变量容量3 参数个数2
stack=2, locals=3, args_size=2
// 方法指令
0: iconst_0 // 常量 int 0 压入操作数栈
1: istore_2 // 将操作数栈栈顶元素0弹出,存入局部变量表第2的位置
2: iload_0 // 将局部变量表第0位置的元素1,压入操作数栈顶
3: iload_1 // 将局部变量表第1位置的元素2,压入操作数栈顶
4: iadd // 从操作数栈弹出两个元素2和1,进行加法运算,将结果3压入操作数栈栈顶
5: istore_2 // 将操作数栈的栈顶元素3弹出到局部变量表第2的位置
6: iload_2 // 将局部变量表第2位置的元素3,压入操作数栈顶
7: ireturn // 将操作数的栈顶元素返回,销毁栈帧
// 行号对应表
LineNumberTable:
line 5: 0 // 代码第5行对应字节码指令第0行
line 6: 2
line 7: 6
}
SourceFile: "ByteCodeSample.java"
-
内存执行方法过程分析
-
内存过程
-
递归为什么会引发 java.lang.StackOverflowError 异常
》递归过深,栈帧数超出虚拟栈深度
-
虚拟机栈过多会引发 java.lang.OutOfMemoryError 异常
public void stackLeakByThread(){
while(true) {
new Thread() {
public void run() {
while(true) {}
}
}
}
}
// Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
3.本地方法栈
- 与虚拟机栈相似,主要作用于标注了native方法。
4. 元空间(MetaSpace)与永久代(PermGen)
元空间和永久代都是方法区的实现,方法区是 JVM 的一种规范,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。
Java 7 之后,原先位于方法区里的字符串常量池已被移动到 java 堆当中,并且jdk 8 之后使用 元空间替代了永久代, 永久废弃永久代。
- 元空间与永久代的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存,从而不存在 java.lang.OutOfMemoryError : PermGen space 异常
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难易确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot(Sun JDK和OpenJDK中所带的虚拟机)与其他JVM(如Jrockit)的集成
5. Java 堆( Heap)
对象实例的分配区域,JVM 内存管理的最大一块,被所有线程共享,JVM启动时创建,唯一目的是存放对象实例。
Java 堆可以处于物理上不连续的内存空间中,只要逻辑地址连续即可,主流的都设置为可以扩展其大小。
Java 堆是 GC管理的主要区域。分代回收机制下,java堆的结构如下:
6、JVM 三大性能调优参数-Xms -Xmx -Xss
java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar
-Xss:规定了每个线程虚拟机栈(堆栈)的大小, 一般256k,此配置将会影响程序进程并发线程数的大小。
-Xms:堆的初始值,即程序进程刚创建时 Java Heap的大小,一旦对象容量超过初始值将进行扩容。
-Xmx:堆能达到的最大值,即 Java Heap能够扩容到的最大值。
通常将 Xms 与 Xmx 设置为一样的,因为当Java Heap容量不够用时,进行扩容时会发生内存抖动,影响程序运行的稳定性。
8、Java内存模型中堆和栈的区别
- 内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求;要求程序中不允许可变数据结构的存在,也不允许有嵌套或递归的结构存在,否则无法计算准确的存储空间。
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定;动态存储分配,由类似于堆栈的运行栈实现,进入一个程序模块前,必须知道其模块的数据区大小,按照先前后出原则分配。
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配;如可变长度串、对象实例,由大片的可利用块和空闲块组成,堆中的内存可以按照任意顺序创建和释放。
-
堆和栈的联系
引用对象、数组时,栈里定义变量(引用变量)保存堆中目标的首地址。
就和 C 中的 指针变量 指向 malloc 出来的空间一样。
- 堆和栈的区别
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态分配(编译器分配的)和动态分配,而堆仅支持动态分配。
- 效率:栈的效率比高。栈效率高、堆灵活。
9、元空间、堆、线程独占部分间的联系-内存角度
10、JDK6 与 JDK6+ 中 String 的 intern() 方法的区别
String s = new String("abc");
s.intern(); // 把字符串对象s加入常量池中,不改变s的指向
-
JDK 6
当调用intern方法时,如果字符串常量池先前己创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
-
JDK6+
当调用intern方法时,如果字符串常量池先前己创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
public class InternDifference {
public static void main(String[] args) {
// "a" 在常量池中创建字符串常量,new 同时也堆中创建字符串对象并返回给s
String s = new String("a");
// jDK6和6+,将堆中的String Obj放入常量池,但在常量池中已存在
s.intern();
// 获取 常量池中的字符串对象 "a"
String s2 = "a";
System.out.println(s == s2); // 比较的是地址
// 在堆中创建aa 字符串对象并返回给s3, 没有"aa",所以不会再常量池中创建
String s3 = new String("a") + new String("a");
// JDK6 将堆中 aa字符串对象的副本放入常量池,和堆中字符串的地址不一样
// JDk6+ 将 "aa"字符串对象的引用放入常量池
s3.intern();
// 获取 常量池中的字符串对象 "aa", JDK6拿到的是副本,JDK6+拿到的是引用
String s4 = "aa";
System.out.println(s3 == s4);
}
}
// JDK6 的执行结果 false false
// JDK6+ 的执行结果 false true
JDK6
JDK6+