个人小结
java的内存管理
任何语言的目的都是对一块磁盘的操作,所以开篇准备小讲一下内存管理机制,这篇作为一个基础篇;
jvm一共有九大部分构成,分别是:类加载子系统、方法区、堆、栈、直接内存、本地方法栈、垃圾回收系统、pc寄存器、执行引擎
先用一张图来展示下这5个常用模块:
JVM内存分配
共享内存
- 1.堆(Heap)
几乎所有的对象都存放在其中, 根据垃圾回收机制不同,java堆中可以有不同的结构,最常见的就是整个java堆分为新生代和老年代,其中新生代存放这新生的对象或者年龄不大的对象,老年代则存放老年对象,(可以按以下方式理解:定时任务来扫描,根据某个时间设定来区分该对象是属于老年代还是新生代)新生代又分为eden区、s0区、s1区;s0和s1也被称作from和to区域,他们是两块大小相等并且可以互换角色的区域,绝大多数情况下,对象分配在eden区,在一次新生代回收后,如果对象还存活着,则会被进入s0和s1区,之后每一次新生代回收,年龄就+1,当年龄一点后,则进入老年代; - 方法区
和堆一样,属于所有线程共享的区域,它保存着系统中的类信息;比如类的字段,方法,常量池等,方法区的大小决定了系统可以保存多少个类,如果定义的太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误,方法区可以被理解为永久区
- 方法区
私有内存
- 1.栈
一块私有的内存空间,一个栈一般由三部分组成,局部变量表、操作数栈、帧数局区;
局部变量表:用于报错函数的参数以及局部变量;
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时空间;
帧数据区:除了局部变量表和操作数栈以外,栈还需一些数据来支持常量池的解析,这里帧数据保存着访问常量的指针,方便程序访问常量池,当函数返回出现异常时,虚拟机必须有个异常处理表,方便发送异常的时候找到异常的代码,因此:异常处理表也是帧数据区的一部分 - 2.本地方法栈:和java栈非常类似,最大不同之处为本地方法栈用于本地方法调用,java虚拟机允许java直接调用本地方法(C编写:看的一脸懵逼)
- 3.pc寄存器:寄存器也是每个线程私有的空间,java虚拟机会为每个线程创建pc寄存器,在任意时刻,一个线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,pc寄存器会执行当前正在执行的指令,如果是本地方法,pc寄存器为undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等信息;
其他子模块的功能
- 1.类加载子系统:加载子系统使用的一个模块
- 2.直接内存:java的nio库,java程序直接使用直接内存,从而提高性能,通常直接内存速度回优于java堆,读写频繁的场合可能会考虑使用
- 3.执行引擎:负责执行虚拟机的字节码,一般先进行编译成机器码后执行
- 4.垃圾回收系统:java核心之一,下面简单介绍下4种算法
1:引用计数法:这个是比较古老的垃圾回收算法,其核心算法就是在对象被其他所引用时计数器+1,当引用失效时则减1,但这种方式有非常严重的问题,即:无法处理循环引用的情况
2:标记清除法,就是分为标记和清除两个阶段处理内存中的对象,当然这种方式也有非常大的弊端,即空间碎片问题,如果把内存比作一张纸,那么这种回收机制会造成磁盘空间不连续的操作,而不连续的内存空间工作效率要低于连续的内存空间的
3:复制算法,核心思想是将内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到另外一块没有被使用的内存块中,之后清除之前正在使用的内存中的对象,反复去交换这个内存中的角色,完成垃圾回收(s0和是s1就是使用的这个算法)
4:标记压缩法,标记压缩法,在标记清除法基础上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理,(老年代中的清理就是使用的标记压缩法)
5:分代算法:根据对象的特点把内存分成N块,根据每个内存的特点使用不同的算法,对于新生代而言每次回收耗时都很短,而老年代回收的频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC
6:分区算法:其主要就是将整个内存分为N个小的独立空间,每个小空间都可以独立使用,这个细粒度控制一次回收多少个空间和那些小空间,而不是针对整个GC,从而提升性能,并减少GC的停顿时间
常用的JVM参数配置
堆分配参数
-XX:+PrintGC在eclipse中使用这个参数后;当虚拟机启动后,遇到GC就会打印相关日志
-XX:+UseSerialGC:配置串行回收器(当然:还有并行的回收器)
-XX:+PrintGCDetails可以查看详情信息,包括各个区的情况
-Xms:设置java程序启动时初始堆的大小
-Xmx:设置java程序能获得的最大堆大小
-Xmx20m -Xms5m -XX:+PrintCommandLineFlags:可以将隐式或者显示传给虚拟机的参数输出
eclipse参数配置:Run-Run Configurations-Arguments Vmarguements
将配置好的jvm参数贴上去
-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails
含义:初始堆内存5M,最大堆内存20M
可以看下下面demo的实验效果
public static void main(String[] args) {
System.out.println("最大内存:"+Runtime.getRuntime().maxMemory());
System.out.println("可使用内存:"+Runtime.getRuntime().freeMemory());
System.out.println("总内存:"+Runtime.getRuntime().totalMemory());
byte[] b1=new byte[1*1024*1024];
System.out.println("分配了1M后的内存");
System.out.println("最大内存:"+Runtime.getRuntime().maxMemory());
System.out.println("可使用内存:"+Runtime.getRuntime().freeMemory());
System.out.println("总内存:"+Runtime.getRuntime().totalMemory());
byte[] b2=new byte[4*1024*1024];
System.out.println("分配了4M");
System.out.println("最大内存:"+Runtime.getRuntime().maxMemory());
System.out.println("可使用内存:"+Runtime.getRuntime().freeMemory());
System.out.println("总内存:"+Runtime.getRuntime().totalMemory());
}
思考:根据jvm的垃圾回收机制,如何配置初始化内存和最大内存才能将jvm调优性能最好呢?
新生代参数配置修改
-Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响
-XX:SurvivorRatio:用来设置新生代中的eden空间和from到to空间的比例,即:-XX:SurvivorRatio=eden/from=eden/to;
-XX:NewRatio=老年代/新生代
推荐值:新生代大小一般设置为整个堆空间的1/3到1/4左右
修改jvm内存设置为:
-Xms20m -Xmx20m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
含义:初始为20M,最大为20M,新生代/老年代=2
观察GC的详细信息输出
public static void main(String[] args) {
byte[] b=null;
for(int i=0;i<10;i++){
//分了10M的内存
b=new byte[1*1024*1024];
}
}
jvm中的运行流程
classloader:动态加载某个class文件到内存(jvm)当中,我们日常编程通常是.java文件,而通过jdk中的javac指令可以将.java文件编译为.class文件,这个时候这个类还不能被其它类引用,还需要classloader,将class文件加载到内存中,方可以被其他类使用,
类的字节码文件被加载器(classloader)先装到运行时数据区,然后执行引擎,经过调用本地接口,放入本地方法库
类的装载:加载、连接(验证、准备、解析) 初始化,使用、卸载
,class 在堆中保存类的定义或者结构,
类的初始化:执行类的构造器(clinit):为类的静态变量赋予正确的初始值,其中如果这个类中有静态块或者静态变量,则执行顺序已该类中排列的顺序前后,在静态块中的变量可以赋值但是不能读,先执行构造器,再执行构造方法,构造方法的作用就是实例化对象
构造器包括:
1:static 变量
2:静态块
public class Demo {
public static int temp=1;
static{
temp=3;
System.out.println(temp);
}
public static void main(String[] args) {
temp=5;
System.out.println(temp);
}
}
上述类可以分别打上断点,实验一下,可以观察下类的执行过程,先执行的是静态变量赋值,而后执行的是静态块,再进行的是main方法;
classLoader
jdk的类加载器:
-
1.BootStrap ClassLoader
jvm启动加载器,由c++语言编写,所有类的父类,加载JRE System Library下面所有的jar,其中最主要的是rt.jar -
2.Extension ClassLoader
扩展类加载器,继承classLoader,主要加载%JAVA_HOME%/lib/ext/*.jar -
3.AppClassloader
应用类加载器,也继承classLoader,从classpath下面加载,我们常用的eclipse中所有的main方法都是属于应用加载器 -
4.自定义加载器
自定义加载器,也继承classLoader,加载路径需要自己定义
下面demo是查看类加载器
public class Demo {
public static void main(String[] args) {
//打印当前的类加载器
System.out.println(Demo.class.getClassLoader());
//sun.misc.Launcher$AppClassLoader@73d16e93
//打印父加载器
ClassLoader loader=Demo.class.getClassLoader();
while(loader!=null){
System.out.println(loader);
loader=loader.getParent();
}
// sun.misc.Launcher$AppClassLoader@73d16e93
// sun.misc.Launcher$AppClassLoader@73d16e93
// sun.misc.Launcher$ExtClassLoader@15db9742
//null:当为null时,说明指向的是BootStarp加载器
System.out.println(loader);
}
}
我们可以看下classLoader的源码,把jdk源码导入eclispe中后,可以看到,这个类位于rt.jar中,是个抽象类,真正的逻辑是在loadClass(String name,boolean resolve)中处理,该方法的逻辑是:先去看下这个类是否已经加载过了,如果加载过了,就返回加载过的类,如果没有的话,则看下父类加载器是否由加载过,如果父类加载器也没有加载,则去Bootstreap中看下是否有加载,这个判断,在jvm中有个学名叫做:双亲委派模式,如果都没有加载过,则选择自定义的加载器,开始加载这个类,如果刚开始都没有发现这个类的全名,就会抛出著名的classNotFroundExcetpion
而点击findClass方法,可以看到:没有具体逻辑,why?
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这个方法是需要子类重写的,重写这个方法可以自定义加载器,你也可以写个~
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;
}
}
暂时理解这么多,再有新的理解再更新喽