https://www.bilibili.com/video/BV1yE41187A3?p=2
JDK
Java Development ToolKit
Java Development ToolKit(Java开发工具包),包括了JRE,一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)
Java Runtime Enviromental
Java Runtime Enviromental(java运行时环境),与JDK相比,它不包含开发工具——编译器、调试器和其它工具,如javac
Java Virtual Mechinal
Java Virtual Mechinal(JAVA虚拟机),JVM是JRE的一部分,JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用
Java 跨平台与C/C++的区别
- c/c++: 源码跨平台,不同平台需要重新编译
- Java:字节码跨平台,一次编译在不同平台只需要安装jvm即可运行字节码
JVM生命周期
- 启动
启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
- 运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程
- 销毁
当程序中的所有非守护线程都终止时,JVM才退出
JVM体系结构
执行引擎
执行字节码,或者执行本地方法
类加载器
作用:加载.class文件
三种类加载器
- AppClassLoader:应用类加载器
负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)
- ExtClassLoader:扩展类加载器
负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)
- BootstrapClassLoader:启动类加载器
负责加载JDK中的核心类库,由c++来写的,加载的是Javahome/jre/lib/rt.jar等
JVM什么时候加载.class文件
- new
实例化对象时会触发类加载器去类加载对应的路径去查找对应的.class文件,并创建Class对象
- 反射
反射时,加载字节码到内存后生产的只是一个Class对象,要得到具体的对象实例还需要使用Class对象的newInstance()方法来创建具体实例
双亲委派机制
ClassLoader源码
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) {
// 获取父类加载器直到父类为null
c = parent.loadClass(name, false);
} else {
// 使用根加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类都未找到,则交给自定义类加载器
c = findClass(name);
}
}
即委派给父类加载器(AppClassLoader =》 ExtClassLoader =》 BootstrapClassLoader)加载。这样就不允许用户串改jdk的源码,也保证了代码的安全
- Class.getClassLoader():获取当前Class类的 类加载器
public class ClassLoader {
public static void main(String[] args) {
ClassLoader myclass = new ClassLoader();
Class<? extends ClassLoader> aClass = myclass.getClass();
/**
* 1. 类加载器收到加载请求,加载ClassLoader类
* 2. AppClassLoader将这个请求委托给父类加载器去完成,一直向上委托,直到BootstrapClassLoader
* 3. BootstrapClassLoader、ExtClassLoader都未找到ClassLoader
* 4. 最终由AppClassLoader在用户目录找到该类,并完成加载
*/
System.out.println(aClass.getClassLoader()); // AppClassLoader
System.out.println(aClass.getClassLoader().getParent()); // ExtClassLoader
System.out.println(aClass.getClassLoader().getParent().getParent()); // BootstrapClassLoader,返回null,java不可见
}
}
自定义类加载器
- 继承ClassLoader
- 重写findClass方法,使用双亲委派模式,委托其父类去加载(因此需要删除父加载器工作目录中的class,让自定义加载器加载)
- (重写loadClass:不使用双亲委派)
重写findClass
- 获取.class字节码文件的字节数组
- this.defineClass将.class的字节数组转化为Class类实例,并返回
public class MyClassLoad extends ClassLoader{
// 字节码存储地址
private String path;
public MyClassLoad(String path) {
this.path = path;
}
public MyClassLoad(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File(path);
try {
// 获取字节码文件的字节数组
byte[] bytes = getClassBytes(file);
// 将.class的字节数组转化为Class类实例
Class<?> myclass = this.defineClass(name, bytes, 0, bytes.length);
return myclass;
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
// 将文件转换为字节数组
private byte[] getClassBytes(File file) throws IOException {
// 获取.class 文件的字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true)
{
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
测试
- 需要删除父类加载器加载目录中的.class文件(双亲委派会使用父类进行加载)
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoad myClassLoad = new MyClassLoad("D:\Person.class");
Class<?> aClass = myClassLoad.loadClass("com.reflect.Person");
System.out.println(aClass.getClassLoader());
Object o = aClass.newInstance();
}
Java内存区域(运行时数据区域)
Java运行时数据区域和内存模型是不一样的东西,内存区域是指Jvm 运行时将数据分区域存储,强调对内存空间的划分,而内存模型定义了JVM 在计算机内存(RAM)中的工作方式
Java堆
Java堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例
堆内存划分
- 年轻代
主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区
新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中)
Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空
- 老年代
随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常
- 永久代(元空间)
值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度
设置堆参数
设置初始化大小为500M,最大可用内存为500M,并打印GC详细信息
- -Xms:设置初始分配大小,默认为物理内存的“1/64”
- -Xmx:最大分配内存,默认为物理内存的“1/4”
VM options:-Xms500m -Xmx500m -XX:+PrintGCDetails
在内存溢出时Dump文件
- -XX:+HeapDumpOnXXX:在xx时生成dump文件
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
设置年轻代阈值,到达此阈值进入老年代,默认15
-XX:MaxTenuringThreshold=20
虚拟机栈
- 线程隔离
- 每个方法都会产生一个栈帧(Stack Frame)
- 每个栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息
本地方法栈
和虚拟机栈区别是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
public class Thread implements Runnable {
...
/**
* Thread类中的native方法
* 测试是否有线程被中断
*/
private native boolean isInterrupted(boolean ClearInterrupted);
...
}
元空间
元空间用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,线程共享
- JDK8之前,方法区的实现是永久代(Perm),JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之
- 线程共享
- 存储类的基本信息(运行时常量池、静态变量、接口信息等)
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
运行时常量池
运行时常量池(Runtime Constant Pool)是元空间的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器
- 线程私有
- 占少量内存空间
- 提供当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置
* 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
- 可以通过 -XX:MaxDirectMemorySize 参数来设置最大可用直接内存,如果启动时未设置则默认为最大堆内存大小,即与 -Xmx 相同
JVM参数
参数 | 含义 | 默认值 |
---|---|---|
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) |
-Xmx | 最大堆大小 | 物理内存的1/4(<1GB) |
-XX:NewSize | 设置年轻代大小(for 1.3/1.4) | |
-XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的1/64 |
-XX:MaxPermSize | 设置持久代最大值 | 物理内存的1/4 |
-Xss | 每个线程的堆栈大小 | |
-XX:MaxTenuringThreshold | 垃圾最大年龄 | |
-XX:+CollectGen0First | FullGC时是否先YGC | false |
-XX:+PrintGCDetails | 打印GC详细 |